From 57e4868c240fc889c954a356d4aa12a119f459a5 Mon Sep 17 00:00:00 2001
From: Nicole White
Date: Thu, 22 Feb 2024 13:43:54 -0500
Subject: [PATCH 1/4] Add testing example
---
.github/workflows/autoblocks-testing.yml | 40 ++
Python/testing-sdk/README.md | 112 +++++
Python/testing-sdk/my_project/__init__.py | 0
.../my_project/evaluators/__init__.py | 0
.../my_project/evaluators/has_substrings.py | 61 +++
.../my_project/evaluators/is_valid_json.py | 47 ++
Python/testing-sdk/my_project/run.py | 11 +
.../testing-sdk/my_project/tasks/__init__.py | 0
.../my_project/tasks/flashcard_generator.py | 121 +++++
.../my_project/tasks/study_guide_outline.py | 34 ++
.../my_project/test_suites/__init__.py | 0
.../flashcard_generator/__init__.py | 28 ++
.../flashcard_generator/evaluators.py | 134 ++++++
.../flashcard_generator/test_cases.py | 140 ++++++
.../study_guide_outline/__init__.py | 27 ++
.../study_guide_outline/evaluators.py | 43 ++
.../study_guide_outline/test_cases.py | 58 +++
.../my_project/test_suites/util.py | 5 +
Python/testing-sdk/poetry.lock | 451 ++++++++++++++++++
Python/testing-sdk/pyproject.toml | 18 +
README.md | 1 +
21 files changed, 1331 insertions(+)
create mode 100644 .github/workflows/autoblocks-testing.yml
create mode 100644 Python/testing-sdk/README.md
create mode 100644 Python/testing-sdk/my_project/__init__.py
create mode 100644 Python/testing-sdk/my_project/evaluators/__init__.py
create mode 100644 Python/testing-sdk/my_project/evaluators/has_substrings.py
create mode 100644 Python/testing-sdk/my_project/evaluators/is_valid_json.py
create mode 100644 Python/testing-sdk/my_project/run.py
create mode 100644 Python/testing-sdk/my_project/tasks/__init__.py
create mode 100644 Python/testing-sdk/my_project/tasks/flashcard_generator.py
create mode 100644 Python/testing-sdk/my_project/tasks/study_guide_outline.py
create mode 100644 Python/testing-sdk/my_project/test_suites/__init__.py
create mode 100644 Python/testing-sdk/my_project/test_suites/flashcard_generator/__init__.py
create mode 100644 Python/testing-sdk/my_project/test_suites/flashcard_generator/evaluators.py
create mode 100644 Python/testing-sdk/my_project/test_suites/flashcard_generator/test_cases.py
create mode 100644 Python/testing-sdk/my_project/test_suites/study_guide_outline/__init__.py
create mode 100644 Python/testing-sdk/my_project/test_suites/study_guide_outline/evaluators.py
create mode 100644 Python/testing-sdk/my_project/test_suites/study_guide_outline/test_cases.py
create mode 100644 Python/testing-sdk/my_project/test_suites/util.py
create mode 100644 Python/testing-sdk/poetry.lock
create mode 100644 Python/testing-sdk/pyproject.toml
diff --git a/.github/workflows/autoblocks-testing.yml b/.github/workflows/autoblocks-testing.yml
new file mode 100644
index 00000000..851a78fa
--- /dev/null
+++ b/.github/workflows/autoblocks-testing.yml
@@ -0,0 +1,40 @@
+name: Autoblocks Testing
+
+on:
+ push: # Run on every push.
+ schedule: # Run every day at ~7:17am PST.
+ - cron: '17 15 * * *'
+
+jobs:
+ py:
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ shell: bash
+ working-directory: Python/testing-sdk
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup python
+ uses: actions/setup-python@v5
+ with:
+ python-version-file: '3.11'
+
+ - name: Install poetry
+ run: curl -sSL https://install.python-poetry.org | python3 -
+
+ - name: Check pyproject.toml & poetry.lock are in sync
+ run: poetry lock --check
+
+ - name: Install dependencies
+ run: poetry install
+
+ - name: Run Autoblocks tests
+ run: npx autoblocks testing exec -- poetry run start
+ env:
+ # Add your OpenAI API key & Autoblocks API key to the repository secrets.
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ AUTOBLOCKS_API_KEY: ${{ secrets.AUTOBLOCKS_API_KEY }}
diff --git a/Python/testing-sdk/README.md b/Python/testing-sdk/README.md
new file mode 100644
index 00000000..f88d3203
--- /dev/null
+++ b/Python/testing-sdk/README.md
@@ -0,0 +1,112 @@
+
+
+
+
+ 📚
+ Documentation
+
+ •
+
+ 🖥️
+ Application
+
+ •
+
+ ☎️
+ Meet with Autoblocks Engineering
+
+
+## Setup
+
+### Install [`poetry`](https://python-poetry.org/)
+
+```bash
+curl -sSL https://install.python-poetry.org | python3 -
+```
+
+### Install [`npm`](https://docs.npmjs.com/about-npm)
+
+> **_NOTE:_** You might already have this installed. Check with `npm -v`.
+
+If you don't have `node` or `npm` installed, we recommend you use `nvm` to do so:
+
+#### Install [`nvm`](https://github.com/nvm-sh/nvm)
+
+```bash
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
+```
+
+#### Install `node` and `npm`
+
+```bash
+nvm install node
+```
+
+#### Set the default version when starting a new shell
+
+```bash
+nvm alias default node
+```
+
+### Install dependencies
+
+```
+poetry install
+```
+
+## Run Autoblocks tests
+
+### Set your Autoblocks API key
+
+Retrieve your **local testing API key** from the [settings page](https://app.autoblocks.ai/settings/api-keys) and set it as an environment variable:
+
+```bash
+export AUTOBLOCKS_API_KEY=...
+```
+
+### Set your OpenAI API key
+
+```bash
+export OPENAI_API_KEY=...
+```
+
+### Run the tests
+
+```bash
+npx autoblocks testing exec -m "my first run" -- poetry run start
+```
+
+You should see something like:
+
+
+
+You can click on the links next to each test name to dig into more details.
+You can also find all of your tests on the testing homepage in the [Autoblocks application](https://app.autoblocks.ai/testing/local).
+
+## GitHub Actions setup
+
+A starter workflow was added in [`.github/workflows/autoblocks-testing.yml`](./.github/workflows/autoblocks-testing.yml).
+This workflow runs the tests on every push to the repository and also
+on a daily schedule.
+
+## Repo structure
+
+```
+my_project/
+ run.py <-- imports all tests from tests/ and runs them
+ evaluators/ <-- all common evaluators are implemented here
+ some_shared_evaluator1.py
+ some_shared_evaluator2.py
+ tasks/ <-- all "tasks" are implemented here
+ task1.py
+ task2.py
+ test_suites/ <-- tests for each task
+ task1/
+ __init__.py <-- implements the runner for task1
+ evaluators.py <-- evaluators used only for task1
+ test_cases.py <-- contains test cases for task1
+ task2/
+ __init__.py <-- implements the runner for task2
+ evaluators.py <-- evaluators used only for task2
+ test_cases.py <-- contains test cases for task2
+```
diff --git a/Python/testing-sdk/my_project/__init__.py b/Python/testing-sdk/my_project/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Python/testing-sdk/my_project/evaluators/__init__.py b/Python/testing-sdk/my_project/evaluators/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Python/testing-sdk/my_project/evaluators/has_substrings.py b/Python/testing-sdk/my_project/evaluators/has_substrings.py
new file mode 100644
index 00000000..6738e554
--- /dev/null
+++ b/Python/testing-sdk/my_project/evaluators/has_substrings.py
@@ -0,0 +1,61 @@
+import abc
+from typing import Any
+from typing import List
+from typing import Optional
+
+from autoblocks.testing.models import BaseEvaluator
+from autoblocks.testing.models import BaseTestCase
+from autoblocks.testing.models import Evaluation
+from autoblocks.testing.models import Threshold
+
+
+class BaseHasSubstrings(BaseEvaluator, abc.ABC):
+ id = "has-substrings"
+
+ """
+ Can be overriden by subclassing and changing this threshold. For example:
+
+ class MyEvaluator(HasSubstrings):
+ threshold = None
+
+ run_test_suite(
+ ...
+ evaluators=[
+ MyEvaluator()
+ ],
+ ...
+ )
+ """
+ threshold: Optional[Threshold] = Threshold(gte=1)
+
+ @abc.abstractmethod
+ def expected_substrings(self, test_case: BaseTestCase) -> List[str]:
+ """
+ Required to be implemented by the subclass.
+
+ In most cases this will just return the field from the test case that contains the expected substrings,
+ but since it's a method, it can be used to calculate the expected substrings in a more complex way
+ if appropriate.
+
+ For example:
+
+ class MyEvaluator(HasSubstrings):
+ def expected_substrings(self, test_case: MyTestCase) -> List[str]:
+ return test_case.expected_substrings
+ """
+ ...
+
+ def output_as_str(self, output: Any) -> str:
+ """
+ Can be overriden by the subclass to change how the output is converted to a string.
+ """
+ return str(output)
+
+ def evaluate(self, test_case: BaseTestCase, output: Any) -> Evaluation:
+ expected_substrings = self.expected_substrings(test_case)
+ output_as_str = self.output_as_str(output)
+
+ for substring in expected_substrings:
+ if substring not in output_as_str:
+ return Evaluation(score=0, threshold=self.threshold)
+ return Evaluation(score=1, threshold=self.threshold)
diff --git a/Python/testing-sdk/my_project/evaluators/is_valid_json.py b/Python/testing-sdk/my_project/evaluators/is_valid_json.py
new file mode 100644
index 00000000..2bf82a31
--- /dev/null
+++ b/Python/testing-sdk/my_project/evaluators/is_valid_json.py
@@ -0,0 +1,47 @@
+import json
+from typing import Any
+from typing import Optional
+
+from autoblocks.testing.models import BaseEvaluator
+from autoblocks.testing.models import BaseTestCase
+from autoblocks.testing.models import Evaluation
+from autoblocks.testing.models import Threshold
+
+
+class IsValidJson(BaseEvaluator):
+ id = "is-valid-json"
+
+ """
+ Can be overriden by subclassing and changing this threshold. For example:
+
+ class MyEvaluator(IsValidJson):
+ threshold = None
+
+ run_test_suite(
+ ...
+ evaluators=[
+ MyEvaluator()
+ ],
+ ...
+ )
+ """
+ threshold: Optional[Threshold] = Threshold(gte=1)
+
+ def output_as_str(self, output: Any) -> str:
+ """
+ Can be overriden by the subclass to change how the output is converted to a string.
+
+ For example:
+
+ class MyEvaluator(IsValidJson):
+ def output_as_str(self, output: SomeCustomOutputType) -> str:
+ return output.as_json()
+ """
+ return str(output)
+
+ def evaluate(self, test_case: BaseTestCase, output: Any) -> Evaluation:
+ try:
+ json.loads(self.output_as_str(output))
+ return Evaluation(score=1, threshold=self.threshold)
+ except json.JSONDecodeError:
+ return Evaluation(score=0, threshold=self.threshold)
diff --git a/Python/testing-sdk/my_project/run.py b/Python/testing-sdk/my_project/run.py
new file mode 100644
index 00000000..0c640e9e
--- /dev/null
+++ b/Python/testing-sdk/my_project/run.py
@@ -0,0 +1,11 @@
+from my_project.test_suites import flashcard_generator
+from my_project.test_suites import study_guide_outline
+
+
+def run():
+ # Autoblocks handles running these tests asynchronously behind the scenes
+ # in a dedicated event loop, so no need to attempt to add any concurrency
+ # here or use asyncio.run() or similar. Just simply call each test suite's
+ # run() function and Autoblocks will handle the rest.
+ flashcard_generator.run()
+ study_guide_outline.run()
diff --git a/Python/testing-sdk/my_project/tasks/__init__.py b/Python/testing-sdk/my_project/tasks/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Python/testing-sdk/my_project/tasks/flashcard_generator.py b/Python/testing-sdk/my_project/tasks/flashcard_generator.py
new file mode 100644
index 00000000..cf24f9b4
--- /dev/null
+++ b/Python/testing-sdk/my_project/tasks/flashcard_generator.py
@@ -0,0 +1,121 @@
+import dataclasses
+import json
+from typing import List
+
+from openai import OpenAI
+from openai.types.chat.completion_create_params import ResponseFormat
+
+openai_client = OpenAI()
+
+
+@dataclasses.dataclass()
+class Flashcard:
+ front: str
+ back: str
+
+
+system_prompt = """Given a user's notes, generate flashcards that will allow the user to study those notes.
+
+Your first task is to identify the facts or key points in the notes.
+Then, create a flashcard for each fact or key point.
+The front of the flashcard should be a question, and the back of the flashcard should be the answer to that question.
+Each flashcard should be supported by content from the notes.
+Ignore the tone of the notes and always make the flashcards in a professional tone.
+Ignore any subjective commentary in the notes and only focus on the facts or key points.
+Return the results as JSON in the below format:
+
+```
+{
+ "cards": [
+ {
+ "front": "What is the capital of France?",
+ "back": "Paris"
+ },
+ {
+ "front": "Who painted the Mona Lisa?",
+ "back": "Leonardo da Vinci"
+ }
+ ]
+}
+```
+
+Only return JSON in your response, nothing else. Do not include the backticks.
+
+Example:
+
+Notes:
+
+'''
+Am. History Notes 🇺🇸
+Beginnings & Stuff
+Columbus 1492, "found" America but actually not the first.
+Native Americans were here first, tons of diff cultures.
+Colonies & Things
+13 Colonies cuz Brits wanted $ and land.
+Taxation w/o Representation = Colonists mad at British taxes, no say in gov.
+Boston Tea Party = Tea in the harbor, major protest.
+Revolution Time
+Declaration of Independence, 1776, basically "we're breaking up with you, Britain".
+George Washington = First pres, war hero.
+Moving West
+Manifest Destiny = Idea that the US was supposed to own all land coast to coast.
+Louisiana Purchase, 1803, Thomas Jefferson bought a ton of land from France.
+'''
+
+Flashcards:
+
+{
+ "cards": [
+ {
+ "front": "Who was the first president of the United States?",
+ "back": "George Washington"
+ },
+ {
+ "front": "What was the idea that the US was supposed to own all land coast to coast?",
+ "back": "Manifest Destiny"
+ },
+ {
+ "front": "What was the year of the Louisiana Purchase?",
+ "back": "1803"
+ }
+ ]
+}
+"""
+
+user_prompt = """Notes:
+
+'''
+{notes}
+'''
+
+Flashcards:"""
+
+
+def gen_flashcards_from_notes(notes: str) -> List[Flashcard]:
+ """
+ Generates flashcards based on a user's notes.
+ """
+ response = openai_client.chat.completions.create(
+ model="gpt-3.5-turbo-1106",
+ temperature=0.0,
+ response_format=ResponseFormat(type="json_object"),
+ messages=[
+ dict(
+ role="system",
+ content=system_prompt,
+ ),
+ dict(
+ role="user",
+ content=user_prompt.format(notes=notes),
+ ),
+ ],
+ )
+ raw_content = response.choices[0].message.content.strip()
+ parsed_content = json.loads(raw_content)
+ return [
+ Flashcard(
+ front=parsed_content["front"],
+ back=parsed_content["back"],
+ )
+ for parsed_content in parsed_content["cards"]
+ ]
diff --git a/Python/testing-sdk/my_project/tasks/study_guide_outline.py b/Python/testing-sdk/my_project/tasks/study_guide_outline.py
new file mode 100644
index 00000000..5c6f8119
--- /dev/null
+++ b/Python/testing-sdk/my_project/tasks/study_guide_outline.py
@@ -0,0 +1,34 @@
+from openai import OpenAI
+
+openai_client = OpenAI()
+
+system_prompt = """Generate a study guide outline for a given topic.
+It should be a bulleted list with just the title of each category.
+The top level bullets should be stars: *
+The second level bullets should be dashes: -
+The second level dashes should have two spaces before them.
+The study guide should be no more than two levels deep.
+There should be between five and ten top-level categories."""
+
+
+def gen_study_guide_outline(topic: str) -> str:
+ """
+ Generates a bulleted study guide outline for a given topic.
+ """
+ response = openai_client.chat.completions.create(
+ model="gpt-3.5-turbo-1106",
+ temperature=0.5,
+ max_tokens=1_000,
+ n=1,
+ messages=[
+ dict(
+ role="system",
+ content=system_prompt,
+ ),
+ dict(
+ role="user",
+ content=f"Topic: {topic}",
+ ),
+ ],
+ )
+ return response.choices[0].message.content.strip()
diff --git a/Python/testing-sdk/my_project/test_suites/__init__.py b/Python/testing-sdk/my_project/test_suites/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Python/testing-sdk/my_project/test_suites/flashcard_generator/__init__.py b/Python/testing-sdk/my_project/test_suites/flashcard_generator/__init__.py
new file mode 100644
index 00000000..6153357b
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/flashcard_generator/__init__.py
@@ -0,0 +1,28 @@
+from typing import List
+
+from autoblocks.testing.run import run_test_suite
+
+from my_project.tasks.flashcard_generator import Flashcard
+from my_project.tasks.flashcard_generator import gen_flashcards_from_notes
+from my_project.test_suites.flashcard_generator.evaluators import IsProfessionalTone
+from my_project.test_suites.flashcard_generator.evaluators import IsSupportedByNotes
+from my_project.test_suites.flashcard_generator.test_cases import TestCase
+from my_project.test_suites.flashcard_generator.test_cases import gen_test_cases
+
+
+def test_fn(test_case: TestCase) -> List[Flashcard]:
+ return gen_flashcards_from_notes(test_case.notes)
+
+
+def run():
+ run_test_suite(
+ id="flashcard-generator",
+ test_cases=gen_test_cases(),
+ evaluators=[
+ IsSupportedByNotes(),
+ IsProfessionalTone(),
+ ],
+ fn=test_fn,
+ max_test_case_concurrency=5,
+ max_evaluator_concurrency=1,
+ )
diff --git a/Python/testing-sdk/my_project/test_suites/flashcard_generator/evaluators.py b/Python/testing-sdk/my_project/test_suites/flashcard_generator/evaluators.py
new file mode 100644
index 00000000..257a8b9e
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/flashcard_generator/evaluators.py
@@ -0,0 +1,134 @@
+import asyncio
+from typing import List
+
+from autoblocks.testing.models import BaseEvaluator
+from autoblocks.testing.models import Evaluation
+from openai import AsyncOpenAI
+
+from my_project.tasks.flashcard_generator import Flashcard
+from my_project.test_suites.flashcard_generator.test_cases import TestCase
+
+openai_client = AsyncOpenAI()
+
+
+class IsProfessionalTone(BaseEvaluator):
+ id = "is-professional-tone"
+
+ prompt = """Please evaluate the provided text for its professionalism in the context of formal communication.
+Consider the following criteria in your assessment:
+
+Language Use: Formality, clarity, and precision of language without slang or casual expressions.
+Sentence Structure: Logical and well-formed sentence construction without run-ons or fragments.
+Tone and Style: Respectful, objective, and appropriately formal tone without bias or excessive emotionality.
+Grammar and Punctuation: Correct grammar, punctuation, and capitalization.
+Based on these criteria, provide a binary response where:
+
+0 indicates the text does not maintain a professional tone.
+1 indicates the text maintains a professional tone.
+No further explanation or summary is required; just provide the number that represents your assessment.
+"""
+
+ async def score_flashcard(self, flashcard: Flashcard) -> int:
+ content = f"{flashcard.front}\n{flashcard.back}"
+
+ response = await openai_client.chat.completions.create(
+ model="gpt-3.5-turbo-1106",
+ temperature=0.0,
+ n=1,
+ max_tokens=1,
+ messages=[
+ dict(
+ role="system",
+ content=self.prompt,
+ ),
+ dict(
+ role="user",
+ content=content,
+ ),
+ ],
+ )
+ raw_content = response.choices[0].message.content.strip()
+ if raw_content == "0":
+ return 0
+ elif raw_content == "1":
+ return 1
+
+ raise ValueError(f"Unexpected response: {raw_content}")
+
+ async def evaluate(
+ self, test_case: TestCase, output: List[Flashcard]
+ ) -> Evaluation:
+ # Score each flashcard asynchronously
+ scores = await asyncio.gather(
+ *[self.score_flashcard(flashcard) for flashcard in output]
+ )
+ if not scores:
+ raise RuntimeError("No scores were returned")
+
+ # Return the average score as the evaluation score
+ return Evaluation(score=sum(scores) / len(scores))
+
+
+class IsSupportedByNotes(BaseEvaluator):
+ id = "is-supported-by-notes"
+
+ prompt = """Given some notes by a student and a flashcard in the form of a question and answer, evaluate whether the flashcard's question and answer are supported by the notes.
+It's possible the question and answer aren't in the notes verbatim.
+If the notes provide enough context or information to support the question and answer, consider that sufficient support.
+Based on these criteria, provide a binary response where:
+0 indicates the flashcard's question and answer are not supported by the notes.
+1 indicates the flashcard's question and answer are supported by the notes.
+No further explanation or summary is required; just provide the number that represents your assessment.""" # noqa: E501
+
+ async def score_flashcard(self, test_case: TestCase, flashcard: Flashcard) -> int:
+ content = f"""Notes:
+
+ '''
+ {test_case.notes}
+ '''
+
+ Flashcard:
+
+ Question: {flashcard.front}
+ Answer: {flashcard.back}
+ """
+
+ response = await openai_client.chat.completions.create(
+ model="gpt-3.5-turbo-1106",
+ temperature=0.0,
+ n=1,
+ max_tokens=1,
+ messages=[
+ dict(
+ role="system",
+ content=self.prompt,
+ ),
+ dict(
+ role="user",
+ content=content,
+ ),
+ ],
+ )
+ raw_content = response.choices[0].message.content.strip()
+ if raw_content == "0":
+ return 0
+ elif raw_content == "1":
+ return 1
+
+ raise ValueError(f"Unexpected response: {raw_content}")
+
+ async def evaluate(
+ self, test_case: TestCase, output: List[Flashcard]
+ ) -> Evaluation:
+ """
+ Return the percent of flashcards whose questions and answers are supported by the notes.
+ """
+ # Score each flashcard asynchronously
+ scores = await asyncio.gather(
+ *[self.score_flashcard(test_case, flashcard) for flashcard in output]
+ )
+ if not scores:
+ raise RuntimeError("No scores were returned")
+
+ # Return the average score as the evaluation score
+ return Evaluation(score=sum(scores) / len(scores))
diff --git a/Python/testing-sdk/my_project/test_suites/flashcard_generator/test_cases.py b/Python/testing-sdk/my_project/test_suites/flashcard_generator/test_cases.py
new file mode 100644
index 00000000..cf88938f
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/flashcard_generator/test_cases.py
@@ -0,0 +1,140 @@
+import dataclasses
+from typing import List
+
+from autoblocks.testing.models import BaseTestCase
+
+from my_project.test_suites.util import md5
+
+
+@dataclasses.dataclass()
+class TestCase(BaseTestCase):
+ notes: str
+
+ def hash(self) -> str:
+ """
+ This hash serves as a unique identifier for a test case throughout its lifetime.
+ """
+ return md5(self.notes)
+
+
+def gen_test_cases() -> List[TestCase]:
+ return [
+ TestCase(
+ notes="""Bio 101 Notes
+Cells n stuff
+Cells are like, the smallest thingies that are alive.
+Some old dude named Hooke found them in 1665 by looking at cork.
+2 kinds: Prokaryotic (no nucleus, think bacteria) & Eukaryotic (has a nucleus, like us and plants).
+Cell Theory (important!!)
+Everything alive = made of cells.
+Cells = life's basic unit.
+New cells come from old ones.
+Parts of a Cell (the bits and pieces)
+Cell Membrane: kinda like a bouncer, decides what gets in and out.
+Nucleus: boss of the cell, has all the DNA.
+Mitochondria: power station, makes energy.
+Ribosomes: tiny factories for making proteins.
+ER stuff:
+Rough ER has ribosomes, makes proteins.
+Smooth ER is like, no ribosomes, makes fats.
+Golgi Thingy: packages proteins.
+Lysosomes: trash disposals for cells.
+Plants have extra stuff:
+Chloroplasts for catching sunlight.
+Cell Wall for extra toughness.
+Membrane and Moving Stuff
+Phospholipid bilayer = fancy term for the cell membrane structure.
+It's picky about what it lets in/out.
+Doing Things (Cellular Processes)
+Photosynthesis: Only in plants, turns sunlight to food.
+Breathing in Cells (Respiration): Turning food & O2 into energy.
+Cell Division: Mitosis (for growing and fixing) & Meiosis (making baby cells).
+DNA & Genes
+DNA = double helix thing, basically the recipe book for making you.
+Genes = specific recipes for traits like eye color.
+Evolution (Darwin’s big idea)
+Survival of the fittest.
+Animals change over time to become better at surviving.
+Random Notes:
+Need to remember: Cell wall = plants only.
+Mitochondria and chloroplasts have their own DNA?? Check this.
+DNA to protein = transcription and translation (need to clarify).
+Why does rough ER look bumpy under a microscope? Oh, because of ribosomes.
+Evolution examples for exam?""",
+ ),
+ TestCase(
+ notes="""Eng Lit Notes
+Random Stuff on Books & Authors
+Shakespeare (Big Deal)
+
+Wrote plays and sonnets.
+Old English (hard to read lol).
+Famous stuff: "Romeo & Juliet", "Hamlet", "Macbeth".
+Themes: love, power, betrayal, the supernatural.
+Chaucer’s "Canterbury Tales"
+
+Super old stories, like medieval road trip.
+Different people telling tales, some funny, some serious.
+Middle English (even harder to read).
+American Lit Bits
+
+Mark Twain: "Huckleberry Finn" = kid on a raft, talks about racism, freedom.
+F. Scott Fitzgerald: "The Great Gatsby", 1920s jazz age, American Dream is kinda questioned.
+Poetry Stuff
+Poems = lots of feelings in few words.
+Rhyme, rhythm, metaphors.
+Emily Dickinson: Weird punctuation, lots of dashes, wrote about death and nature.
+Robert Frost: "The Road Not Taken", about choices and life paths.
+Modern Stuff (Kinda)
+"To Kill a Mockingbird" by Harper Lee: Racism, growing up, the South.
+"1984" by George Orwell: Creepy government watching everyone.
+"The Catcher in the Rye" by J.D. Salinger: Teen angst, rebellion.
+Themes & Symbols
+Symbols: Stuff in books that stands for other stuff. Like, a road in a poem might not just be a road.
+Themes: Big ideas in a story. Freedom, identity, conflict, etc.
+Notes to Self:
+Shakespeare invented a ton of words, look up some.
+Need examples of irony from "The Great Gatsby".
+What the heck is iambic pentameter again?
+Look up what "postmodernism" means.
+Remember to find quotes for essay on "Mockingbird".
+Random Thoughts:
+Why do all old books have to be tragic?
+Need to watch some Shakespeare adaptations to get it better.
+Symbols in "The Great Gatsby"? Green light = dream??
+Is every old poem about death or what?"""
+ ),
+ TestCase(
+ notes="""Early Stuff
+Stonehenge: Big rocks in a circle, super old, no one knows why they did it.
+Romans: Came, saw, conquered. Left a bunch of baths and walls (Hadrian's Wall).
+Medieval Mayhem
+1066: Normans (French guys) invade, William the Conqueror becomes king.
+Magna Carta (1215): King John forced to sign it, basically "Kings can't do whatever they want."
+Wars & Plagues
+100 Years War: England vs. France, forever fighting.
+Black Death: Wipes out like half the population. Seriously bad.
+Tudor Drama
+Henry VIII: Marries a bunch of women, starts his own church (Church of England) because the Pope won't let him divorce.
+Elizabeth I: Virgin Queen, beats the Spanish Armada, arts and theatre flourish (Shakespeare time).
+Civil War & The Commonwealth
+1642-1651: Civil War, Charles I loses his head, literally.
+Oliver Cromwell: Becomes "Lord Protector", basically a dictator but not called a king.
+Restoration to Revolution
+1660: Monarchy's back with Charles II.
+1688: Glorious Revolution, William of Orange takes over, more power to Parliament.
+Industrial Revolution
+18th-19th Century: Everything changes, factories everywhere, British Empire expands big time.
+20th Century Stuff
+WWI & WWII: Major world wars, lots of impact.
+Decolonization: Empire shrinks, countries gain independence.
+Modern Bits
+EU & Brexit: Joining and leaving the European Union.
+Monarchs: From Elizabeth II to Charles III, royal family drama continues.
+Random Thoughts:
+Why so many Henrys and Edwards?
+Need to remember dates for exams (ugh).
+The industrial revolution = coal, steam, and smog.
+How did Britain end up ruling so much of the world?"""
+ ),
+ ]
diff --git a/Python/testing-sdk/my_project/test_suites/study_guide_outline/__init__.py b/Python/testing-sdk/my_project/test_suites/study_guide_outline/__init__.py
new file mode 100644
index 00000000..0cd0f49f
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/study_guide_outline/__init__.py
@@ -0,0 +1,27 @@
+from autoblocks.testing.run import run_test_suite
+
+from my_project.tasks.study_guide_outline import gen_study_guide_outline
+from my_project.test_suites.study_guide_outline.evaluators import Formatting
+from my_project.test_suites.study_guide_outline.evaluators import HasSubstrings
+from my_project.test_suites.study_guide_outline.evaluators import NumCategories
+from my_project.test_suites.study_guide_outline.test_cases import TestCase
+from my_project.test_suites.study_guide_outline.test_cases import gen_test_cases
+
+
+def test_fn(test_case: TestCase) -> str:
+ return gen_study_guide_outline(test_case.topic)
+
+
+def run():
+ run_test_suite(
+ id="study-guide-outline",
+ test_cases=gen_test_cases(),
+ evaluators=[
+ Formatting(),
+ NumCategories(),
+ HasSubstrings(),
+ ],
+ fn=test_fn,
+ max_test_case_concurrency=5,
+ max_evaluator_concurrency=2,
+ )
diff --git a/Python/testing-sdk/my_project/test_suites/study_guide_outline/evaluators.py b/Python/testing-sdk/my_project/test_suites/study_guide_outline/evaluators.py
new file mode 100644
index 00000000..16111f3c
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/study_guide_outline/evaluators.py
@@ -0,0 +1,43 @@
+from typing import List
+
+from autoblocks.testing.models import BaseEvaluator
+from autoblocks.testing.models import Evaluation
+from autoblocks.testing.models import Threshold
+
+from my_project.evaluators.has_substrings import BaseHasSubstrings
+from my_project.test_suites.study_guide_outline.test_cases import TestCase
+
+
+class Formatting(BaseEvaluator):
+ id = "formatting"
+
+ @staticmethod
+ def score(output: str) -> int:
+ """
+ Every line should either be blank or start with "* " or " - "
+ """
+ for line in output.splitlines():
+ if not (line.strip() == "" or line.startswith(("* ", " - "))):
+ return 0
+ return 1
+
+ def evaluate(self, test_case: TestCase, output: str) -> Evaluation:
+ return Evaluation(score=self.score(output), threshold=Threshold(gte=1))
+
+
+class NumCategories(BaseEvaluator):
+ id = "num-categories"
+
+ min_categories: int = 5
+ max_categories: int = 10
+
+ def score(self, output: str) -> int:
+ return int(self.min_categories <= output.count("* ") <= self.max_categories)
+
+ def evaluate(self, test_case: TestCase, output: str) -> Evaluation:
+ return Evaluation(score=self.score(output), threshold=Threshold(gte=1))
+
+
+class HasSubstrings(BaseHasSubstrings):
+ def expected_substrings(self, test_case: TestCase) -> List[str]:
+ return test_case.expected_substrings
diff --git a/Python/testing-sdk/my_project/test_suites/study_guide_outline/test_cases.py b/Python/testing-sdk/my_project/test_suites/study_guide_outline/test_cases.py
new file mode 100644
index 00000000..62cf3d1a
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/study_guide_outline/test_cases.py
@@ -0,0 +1,58 @@
+import dataclasses
+from typing import List
+
+from autoblocks.testing.models import BaseTestCase
+
+from my_project.test_suites.util import md5
+
+
+@dataclasses.dataclass()
+class TestCase(BaseTestCase):
+ topic: str
+ expected_substrings: List[str]
+
+ def hash(self) -> str:
+ """
+ This hash serves as a unique identifier for a test case throughout its lifetime.
+ """
+ return md5(self.topic)
+
+
+def gen_test_cases() -> List[TestCase]:
+ return [
+ TestCase(
+ topic="Introduction to Organic Chemistry",
+ expected_substrings=[
+ "Functional Groups",
+ ],
+ ),
+ TestCase(
+ topic="Fundamentals of Calculus",
+ expected_substrings=[
+ "Derivatives",
+ "Differentiation",
+ ],
+ ),
+ TestCase(
+ topic="World History: Ancient Civilizations",
+ expected_substrings=[
+ "Mesopotamia",
+ "Egypt",
+ ],
+ ),
+ TestCase(
+ topic="Basics of Programming in Python",
+ expected_substrings=[
+ "Syntax",
+ "Variables",
+ "Functions",
+ ],
+ ),
+ TestCase(
+ topic="Principles of Economics",
+ expected_substrings=[
+ "Microeconomics",
+ "Macroeconomics",
+ ],
+ ),
+ ]
diff --git a/Python/testing-sdk/my_project/test_suites/util.py b/Python/testing-sdk/my_project/test_suites/util.py
new file mode 100644
index 00000000..28979812
--- /dev/null
+++ b/Python/testing-sdk/my_project/test_suites/util.py
@@ -0,0 +1,5 @@
+import hashlib
+
+
+def md5(text: str) -> str:
+ return hashlib.md5(text.encode()).hexdigest()
diff --git a/Python/testing-sdk/poetry.lock b/Python/testing-sdk/poetry.lock
new file mode 100644
index 00000000..94032874
--- /dev/null
+++ b/Python/testing-sdk/poetry.lock
@@ -0,0 +1,451 @@
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+]
+
+[[package]]
+name = "anyio"
+version = "4.3.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+ {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
+]
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
+
+[[package]]
+name = "autoblocksai"
+version = "0.0.27"
+description = "Python client for Autoblocks"
+optional = false
+python-versions = ">=3.8.1,<4.0.0"
+files = [
+ {file = "autoblocksai-0.0.27-py3-none-any.whl", hash = "sha256:6fb8976d957503d9ff757c7f224f4fd27ef14b0a69e5456afaa4a513bd5523db"},
+ {file = "autoblocksai-0.0.27.tar.gz", hash = "sha256:d31fe964e5a5105d10913a30664a2b356073a7483addf1e23df3902683ddb78e"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+httpx = ">=0.24.0"
+orjson = ">=3.0.0"
+pyyaml = ">=6.0.0"
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+description = "Distro - an OS platform information API"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
+ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.3"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpcore-1.0.3-py3-none-any.whl", hash = "sha256:9a6a501c3099307d9fd76ac244e08503427679b1e81ceb1d922485e2f2462ad2"},
+ {file = "httpcore-1.0.3.tar.gz", hash = "sha256:5c0f9546ad17dac4d0772b0808856eb616eb8b48ce94f49ed819fd6982a8a544"},
+]
+
+[package.dependencies]
+certifi = "*"
+h11 = ">=0.13,<0.15"
+
+[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<0.24.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.26.0"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
+ {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
+]
+
+[package.dependencies]
+anyio = "*"
+certifi = "*"
+httpcore = "==1.*"
+idna = "*"
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+
+[[package]]
+name = "idna"
+version = "3.6"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+]
+
+[[package]]
+name = "openai"
+version = "1.12.0"
+description = "The official Python library for the openai API"
+optional = false
+python-versions = ">=3.7.1"
+files = [
+ {file = "openai-1.12.0-py3-none-any.whl", hash = "sha256:a54002c814e05222e413664f651b5916714e4700d041d5cf5724d3ae1a3e3481"},
+ {file = "openai-1.12.0.tar.gz", hash = "sha256:99c5d257d09ea6533d689d1cc77caa0ac679fa21efef8893d8b0832a86877f1b"},
+]
+
+[package.dependencies]
+anyio = ">=3.5.0,<5"
+distro = ">=1.7.0,<2"
+httpx = ">=0.23.0,<1"
+pydantic = ">=1.9.0,<3"
+sniffio = "*"
+tqdm = ">4"
+typing-extensions = ">=4.7,<5"
+
+[package.extras]
+datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
+
+[[package]]
+name = "orjson"
+version = "3.9.14"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
+ {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
+ {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
+ {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
+ {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
+ {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
+ {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
+ {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
+ {file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
+ {file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
+ {file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
+ {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
+ {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
+ {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
+ {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
+ {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
+ {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
+ {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
+ {file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
+ {file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
+ {file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
+ {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
+ {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
+ {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
+ {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
+ {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
+ {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
+ {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
+ {file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
+ {file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
+ {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
+ {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
+ {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
+ {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
+ {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
+ {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
+ {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
+ {file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
+ {file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
+ {file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
+ {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
+ {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
+ {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
+ {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
+ {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
+ {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
+ {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
+ {file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
+ {file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
+ {file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
+]
+
+[[package]]
+name = "pydantic"
+version = "2.6.1"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"},
+ {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.16.2"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.16.2"
+description = ""
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"},
+ {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"},
+ {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"},
+ {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"},
+ {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"},
+ {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"},
+ {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"},
+ {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"},
+ {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"},
+ {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"},
+ {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"},
+ {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"},
+ {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"},
+ {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"},
+ {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"},
+ {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"},
+ {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"},
+ {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"},
+ {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"},
+ {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"},
+ {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
+ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+]
+
+[[package]]
+name = "tqdm"
+version = "4.66.2"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+ {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.9.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.11"
+content-hash = "4925a91a5164a9e0abfb898b49ef93fdd270d5868d8002f5255df93dcceab0f4"
diff --git a/Python/testing-sdk/pyproject.toml b/Python/testing-sdk/pyproject.toml
new file mode 100644
index 00000000..4aa409de
--- /dev/null
+++ b/Python/testing-sdk/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "testing-sdk"
+version = "0.0.0"
+description = "Using the Autoblocks Testing SDK for experimentation and regression testing"
+authors = [
+ "Autoblocks Engineering ",
+]
+readme = "README.md"
+packages = [{include = "my_project"}]
+
+[tool.poetry.dependencies]
+python = "^3.11"
+autoblocksai = ">=0.0.27"
+openai = "^1.0.0"
+
+[tool.poetry.scripts]
+# This will execute the run() function in my_project/run.py
+start = "my_project.run:run"
diff --git a/README.md b/README.md
index a1580906..424f69b9 100644
--- a/README.md
+++ b/README.md
@@ -57,5 +57,6 @@
| [flask](/Python/flask) | Autoblocks tracing within a [Flask](https://flask.palletsprojects.com/) application |
| [openai-tracing](/Python/openai-tracing) | Tracing of openai calls |
| [prompt-sdk-headless](/Python/prompt-sdk-headless) | Safely integrate prompts managed in the Autoblocks platform into your application |
+| [testing-sdk](/Python/testing-sdk) | Using the Autoblocks Testing SDK for experimentation and regression testing |
From e5d57105bf35607e73f319ee58584e7fe50cb0cd Mon Sep 17 00:00:00 2001
From: Nicole White
Date: Thu, 22 Feb 2024 13:52:31 -0500
Subject: [PATCH 2/4] fix ci
---
.github/workflows/autoblocks-testing.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/autoblocks-testing.yml b/.github/workflows/autoblocks-testing.yml
index 851a78fa..3a88c995 100644
--- a/.github/workflows/autoblocks-testing.yml
+++ b/.github/workflows/autoblocks-testing.yml
@@ -21,7 +21,7 @@ jobs:
- name: Setup python
uses: actions/setup-python@v5
with:
- python-version-file: '3.11'
+ python-version: '3.11'
- name: Install poetry
run: curl -sSL https://install.python-poetry.org | python3 -
From 4d8464aa02b1b627e8a9591c46935af8a94e5fe9 Mon Sep 17 00:00:00 2001
From: Nicole White
Date: Thu, 22 Feb 2024 15:20:05 -0500
Subject: [PATCH 3/4] use dotenv
---
.github/workflows/autoblocks-testing.yml | 10 ++++---
Python/testing-sdk/README.md | 37 ++++++++++++------------
Python/testing-sdk/my_project/run.py | 5 ++++
Python/testing-sdk/poetry.lock | 16 +++++++++-
Python/testing-sdk/pyproject.toml | 1 +
5 files changed, 45 insertions(+), 24 deletions(-)
diff --git a/.github/workflows/autoblocks-testing.yml b/.github/workflows/autoblocks-testing.yml
index 3a88c995..5861ae83 100644
--- a/.github/workflows/autoblocks-testing.yml
+++ b/.github/workflows/autoblocks-testing.yml
@@ -32,9 +32,11 @@ jobs:
- name: Install dependencies
run: poetry install
+ - name: Create .env file
+ run: |
+ touch .env
+ echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env
+ echo "AUTOBLOCKS_API_KEY=${{ secrets.AUTOBLOCKS_API_KEY }}" >> .env
+
- name: Run Autoblocks tests
run: npx autoblocks testing exec -- poetry run start
- env:
- # Add your OpenAI API key & Autoblocks API key to the repository secrets.
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- AUTOBLOCKS_API_KEY: ${{ secrets.AUTOBLOCKS_API_KEY }}
diff --git a/Python/testing-sdk/README.md b/Python/testing-sdk/README.md
index f88d3203..0092ef52 100644
--- a/Python/testing-sdk/README.md
+++ b/Python/testing-sdk/README.md
@@ -1,6 +1,8 @@
+
-
+
+
📚
Documentation
@@ -12,9 +14,22 @@
•
- ☎️
- Meet with Autoblocks Engineering
+ 🏠
+ Home
+
+
+## Getting started
+
+- Sign up for an Autoblocks account at https://app.autoblocks.ai
+- Grab your Autoblocks **local testing API key** from https://app.autoblocks.ai/settings/api-keys
+- Grab your OpenAI API key from https://platform.openai.com/account/api-keys
+- Create a file named `.env` in this folder and include the following environment variables:
+
+```
+OPENAI_API_KEY=
+AUTOBLOCKS_API_KEY=
+```
## Setup
@@ -56,22 +71,6 @@ poetry install
## Run Autoblocks tests
-### Set your Autoblocks API key
-
-Retrieve your **local testing API key** from the [settings page](https://app.autoblocks.ai/settings/api-keys) and set it as an environment variable:
-
-```bash
-export AUTOBLOCKS_API_KEY=...
-```
-
-### Set your OpenAI API key
-
-```bash
-export OPENAI_API_KEY=...
-```
-
-### Run the tests
-
```bash
npx autoblocks testing exec -m "my first run" -- poetry run start
```
diff --git a/Python/testing-sdk/my_project/run.py b/Python/testing-sdk/my_project/run.py
index 0c640e9e..e2429b85 100644
--- a/Python/testing-sdk/my_project/run.py
+++ b/Python/testing-sdk/my_project/run.py
@@ -1,7 +1,12 @@
+import dotenv
+
from my_project.test_suites import flashcard_generator
from my_project.test_suites import study_guide_outline
+dotenv.load_dotenv(".env")
+
+
def run():
# Autoblocks handles running these tests asynchronously behind the scenes
# in a dedicated event loop, so no need to attempt to add any concurrency
diff --git a/Python/testing-sdk/poetry.lock b/Python/testing-sdk/poetry.lock
index 94032874..eb1e5781 100644
--- a/Python/testing-sdk/poetry.lock
+++ b/Python/testing-sdk/poetry.lock
@@ -354,6 +354,20 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
+ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
[[package]]
name = "pyyaml"
version = "6.0.1"
@@ -448,4 +462,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "4925a91a5164a9e0abfb898b49ef93fdd270d5868d8002f5255df93dcceab0f4"
+content-hash = "0930cd0da837d72aaa58df249b5c914b3855e7ef3fa97d19829c980923bd2595"
diff --git a/Python/testing-sdk/pyproject.toml b/Python/testing-sdk/pyproject.toml
index 4aa409de..024663da 100644
--- a/Python/testing-sdk/pyproject.toml
+++ b/Python/testing-sdk/pyproject.toml
@@ -12,6 +12,7 @@ packages = [{include = "my_project"}]
python = "^3.11"
autoblocksai = ">=0.0.27"
openai = "^1.0.0"
+python-dotenv = "^1.0.1"
[tool.poetry.scripts]
# This will execute the run() function in my_project/run.py
From e602da83f6905caf15303e76effd52f110a59126 Mon Sep 17 00:00:00 2001
From: Nicole White
Date: Thu, 22 Feb 2024 15:26:22 -0500
Subject: [PATCH 4/4] drop dotenv
---
.github/workflows/autoblocks-testing.yml | 9 +++-----
Python/testing-sdk/README.md | 28 ++++++++++++++----------
Python/testing-sdk/my_project/run.py | 5 -----
Python/testing-sdk/poetry.lock | 16 +-------------
Python/testing-sdk/pyproject.toml | 1 -
5 files changed, 20 insertions(+), 39 deletions(-)
diff --git a/.github/workflows/autoblocks-testing.yml b/.github/workflows/autoblocks-testing.yml
index 5861ae83..b508b711 100644
--- a/.github/workflows/autoblocks-testing.yml
+++ b/.github/workflows/autoblocks-testing.yml
@@ -32,11 +32,8 @@ jobs:
- name: Install dependencies
run: poetry install
- - name: Create .env file
- run: |
- touch .env
- echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env
- echo "AUTOBLOCKS_API_KEY=${{ secrets.AUTOBLOCKS_API_KEY }}" >> .env
-
- name: Run Autoblocks tests
run: npx autoblocks testing exec -- poetry run start
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ AUTOBLOCKS_API_KEY: ${{ secrets.AUTOBLOCKS_API_KEY }}
diff --git a/Python/testing-sdk/README.md b/Python/testing-sdk/README.md
index 0092ef52..253bbf16 100644
--- a/Python/testing-sdk/README.md
+++ b/Python/testing-sdk/README.md
@@ -19,18 +19,6 @@
-## Getting started
-
-- Sign up for an Autoblocks account at https://app.autoblocks.ai
-- Grab your Autoblocks **local testing API key** from https://app.autoblocks.ai/settings/api-keys
-- Grab your OpenAI API key from https://platform.openai.com/account/api-keys
-- Create a file named `.env` in this folder and include the following environment variables:
-
-```
-OPENAI_API_KEY=
-AUTOBLOCKS_API_KEY=
-```
-
## Setup
### Install [`poetry`](https://python-poetry.org/)
@@ -71,6 +59,22 @@ poetry install
## Run Autoblocks tests
+### Set your Autoblocks API key
+
+Retrieve your **local testing API key** from the [settings page](https://app.autoblocks.ai/settings/api-keys) and set it as an environment variable:
+
+```bash
+export AUTOBLOCKS_API_KEY=...
+```
+
+### Set your OpenAI API key
+
+```bash
+export OPENAI_API_KEY=...
+```
+
+### Run the tests
+
```bash
npx autoblocks testing exec -m "my first run" -- poetry run start
```
diff --git a/Python/testing-sdk/my_project/run.py b/Python/testing-sdk/my_project/run.py
index e2429b85..0c640e9e 100644
--- a/Python/testing-sdk/my_project/run.py
+++ b/Python/testing-sdk/my_project/run.py
@@ -1,12 +1,7 @@
-import dotenv
-
from my_project.test_suites import flashcard_generator
from my_project.test_suites import study_guide_outline
-dotenv.load_dotenv(".env")
-
-
def run():
# Autoblocks handles running these tests asynchronously behind the scenes
# in a dedicated event loop, so no need to attempt to add any concurrency
diff --git a/Python/testing-sdk/poetry.lock b/Python/testing-sdk/poetry.lock
index eb1e5781..94032874 100644
--- a/Python/testing-sdk/poetry.lock
+++ b/Python/testing-sdk/poetry.lock
@@ -354,20 +354,6 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
-[[package]]
-name = "python-dotenv"
-version = "1.0.1"
-description = "Read key-value pairs from a .env file and set them as environment variables"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
- {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
-]
-
-[package.extras]
-cli = ["click (>=5.0)"]
-
[[package]]
name = "pyyaml"
version = "6.0.1"
@@ -462,4 +448,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "0930cd0da837d72aaa58df249b5c914b3855e7ef3fa97d19829c980923bd2595"
+content-hash = "4925a91a5164a9e0abfb898b49ef93fdd270d5868d8002f5255df93dcceab0f4"
diff --git a/Python/testing-sdk/pyproject.toml b/Python/testing-sdk/pyproject.toml
index 024663da..4aa409de 100644
--- a/Python/testing-sdk/pyproject.toml
+++ b/Python/testing-sdk/pyproject.toml
@@ -12,7 +12,6 @@ packages = [{include = "my_project"}]
python = "^3.11"
autoblocksai = ">=0.0.27"
openai = "^1.0.0"
-python-dotenv = "^1.0.1"
[tool.poetry.scripts]
# This will execute the run() function in my_project/run.py