-
Notifications
You must be signed in to change notification settings - Fork 53
Add support for dataset generation with strands agents #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import asyncio | ||
|
|
||
| from strands import Agent | ||
| from strands_evaluation.dataset import Dataset | ||
| from strands_evaluation.evaluators.interactions_evaluator import InteractionsEvaluator | ||
| from strands_evaluation.evaluators.output_evaluator import OutputEvaluator | ||
| from strands_evaluation.evaluators.trajectory_evaluator import TrajectoryEvaluator | ||
| from strands_evaluation.evaluators.utils import helper_funcs | ||
| from strands_evaluation.generators.dataset_generator import DatasetGenerator | ||
| from typing_extensions import TypedDict | ||
|
|
||
|
|
||
| async def test_dataset_generator(): | ||
| class TrajectoryType(TypedDict): | ||
| tool: str | ||
| input: dict | ||
|
|
||
| generator = DatasetGenerator[str, float]( | ||
| str, float, trajectory_type=TrajectoryType, include_expected_trajectory=True | ||
| ) | ||
| dataset = await generator.from_context_async( | ||
| "Create test cases about math given that you have access to these tools: calculator, python.", | ||
| task_description="Getting the response from an AI agent with access to tools.", | ||
| num_cases=10, | ||
| evaluator=TrajectoryEvaluator, | ||
| ) | ||
| print(len(dataset.cases)) | ||
| dataset.to_file("generated_traj_dataset_context") | ||
|
|
||
| generator = DatasetGenerator[str, str](str, str, trajectory_type=TrajectoryType, include_expected_interactions=True) | ||
| dataset = await generator.from_context_async( | ||
| "Create test cases about research for a multi-agent system with the following agents: researcher, analyst, fact_checker, and report_writer.", | ||
| task_description="Getting the response from an AI agent with access to tools.", | ||
| num_cases=5, | ||
| evaluator=InteractionsEvaluator, | ||
| ) | ||
| print(len(dataset.cases)) | ||
| dataset.to_file("generated_interaction_dataset_context") | ||
|
|
||
| generator = DatasetGenerator[str, str](str, str, trajectory_type=TrajectoryType, include_expected_interactions=True) | ||
| dataset = await generator.from_scratch_async( | ||
| ["math", "science"], | ||
| task_description="Getting the response from an AI agent.", | ||
| num_cases=5, | ||
| evaluator=OutputEvaluator, | ||
| ) | ||
| dataset.to_file("generated_output_dataset_scratch") | ||
| print(len(dataset.cases)) | ||
|
|
||
| # try from_dataset | ||
| generator = DatasetGenerator[str, str](str, str, trajectory_type=TrajectoryType, include_expected_interactions=True) | ||
| dataset = Dataset.from_file("dataset_files/generated_interaction_dataset_context.json") | ||
| new_dataset = await generator.update_current_dataset_async( | ||
| dataset, | ||
| "Getting the response from an AI agent with access to tools.", | ||
| context="Create test cases about research for a multi-agent system with the following agents: researcher, analyst, fact_checker, and report_writer.", | ||
| num_cases=5, | ||
| new_evaluator_type=OutputEvaluator, | ||
| ) | ||
| new_dataset.to_file("generated_output_dataset_from_dataset") | ||
| print(len(new_dataset.cases)) | ||
|
|
||
| # new from dataset | ||
| generator = DatasetGenerator[str, str](str, str, trajectory_type=TrajectoryType, include_expected_trajectory=True) | ||
| dataset = Dataset.from_file("dataset_files/generated_traj_dataset_context.json") | ||
| new_dataset = await generator.from_dataset_async( | ||
| dataset, | ||
| "Getting the response from an AI agent with access to tools.", | ||
| extra_information="Create test cases about math given that you have access to these tools: calculator, python.", | ||
| ) | ||
|
|
||
| new_dataset.to_file("generated_traj_dataset_from_dataset") | ||
| print(len(new_dataset.cases)) | ||
|
|
||
| # try evaluating them | ||
| dataset = Dataset.from_file("dataset_files/generated_traj_dataset_context.json") | ||
|
|
||
| def task_func(input: str) -> dict: | ||
| agent = Agent(system_prompt="You are a helpful assistant that can do math.", callback_handler=None) | ||
| output = agent(input) | ||
| return { | ||
| "output": str(output), | ||
| "trajectory": helper_funcs.extract_agent_tools_used_from_messages(agent.messages), | ||
| } | ||
|
|
||
| report = await dataset.run_evaluations_async(task_func) | ||
| report.run_display() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| # python -m examples.try_dataset_generator | ||
| asyncio.run(test_dataset_generator()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ | |
| import os | ||
| from collections.abc import Callable | ||
|
|
||
| from pydantic import BaseModel | ||
| from typing_extensions import Any, Generic, TypeVar | ||
|
|
||
| from .case import Case | ||
|
|
@@ -18,7 +17,7 @@ | |
| OutputT = TypeVar("OutputT") | ||
|
|
||
|
|
||
| class Dataset(BaseModel, Generic[InputT, OutputT]): | ||
| class Dataset(Generic[InputT, OutputT]): | ||
| """ | ||
| A collection of test cases, representing a dataset. | ||
|
|
||
|
|
@@ -47,8 +46,52 @@ class Dataset(BaseModel, Generic[InputT, OutputT]): | |
| ) | ||
| """ | ||
|
|
||
| cases: list[Case[InputT, OutputT]] | ||
| evaluator: Evaluator[InputT, OutputT] | ||
| def __init__(self, cases: list[Case[InputT, OutputT]] = None, evaluator: Evaluator[InputT, OutputT] = Evaluator()): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it make sense to be list[Case[InputT, OutputT]] | None = None
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have strong feelings either way, so I'll fix it to be like this. I'm not sure if there's any benefit in one way.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will do this fix in the refactor PR. |
||
| self._cases = cases if cases else [] | ||
| self._evaluator = evaluator | ||
|
|
||
| @property | ||
| def cases(self) -> list[Case[InputT, OutputT]]: | ||
| """ | ||
| Get a deep copy of all test cases in the dataset. | ||
|
|
||
| Returns deep copies to prevent accidental mutation of the original test cases. | ||
| Users can safely modify the returned cases without affecting the dataset. | ||
|
|
||
| Returns: | ||
| List of Case objects (deep copies) containing all test cases in the dataset | ||
| """ | ||
| return [case.model_copy(deep=True) for case in self._cases] | ||
|
|
||
| @property | ||
| def evaluator(self) -> Evaluator[InputT, OutputT]: | ||
| """ | ||
| Get the evaluator used for assessing test case performance. | ||
|
|
||
| Returns: | ||
| The evaluator instance configured for this dataset | ||
| """ | ||
| return self._evaluator | ||
|
|
||
| @cases.setter | ||
| def cases(self, new_cases: list[Case[InputT, OutputT]]): | ||
| """ | ||
| Set the test cases for this dataset. | ||
|
|
||
| Args: | ||
| new_cases: List of Case objects to use as the dataset's test cases | ||
| """ | ||
| self._cases = new_cases | ||
|
|
||
| @evaluator.setter | ||
| def evaluator(self, new_evaluator: Evaluator[InputT, OutputT]): | ||
| """ | ||
| Set the evaluator for assessing test case performance. | ||
|
|
||
| Args: | ||
| new_evaluator: Evaluator instance to use for evaluating test cases | ||
| """ | ||
| self._evaluator = new_evaluator | ||
|
|
||
| def _run_task( | ||
| self, task: Callable[[InputT], OutputT | dict[str, Any]], case: Case[InputT, OutputT] | ||
|
|
@@ -175,7 +218,7 @@ def run_evaluations(self, task: Callable[[InputT], OutputT | dict[str, Any]]) -> | |
| test_passes = [] | ||
| cases = [] | ||
| reasons = [] | ||
| for case in self.cases: | ||
| for case in self._cases: | ||
| try: | ||
| evaluation_context = self._run_task(task, case) | ||
| evaluation_output = self.evaluator.evaluate(evaluation_context) | ||
|
|
@@ -217,10 +260,10 @@ async def run_evaluations_async(self, task: Callable, max_workers: int = 10) -> | |
| queue = asyncio.Queue() | ||
| results = [] | ||
|
|
||
| for case in self.cases: | ||
| for case in self._cases: | ||
| queue.put_nowait(case) | ||
|
|
||
| num_workers = min(max_workers, len(self.cases)) | ||
| num_workers = min(max_workers, len(self._cases)) | ||
|
|
||
| # Create and start workers | ||
| workers = [asyncio.create_task(self._worker(queue, task, results)) for _ in range(num_workers)] | ||
|
|
@@ -252,9 +295,9 @@ def to_dict(self) -> dict: | |
| Return: | ||
| A dictionary representation of the dataset. | ||
| """ | ||
| return {"cases": [case.model_dump() for case in self.cases], "evaluator": self.evaluator.to_dict()} | ||
| return {"cases": [case.model_dump() for case in self._cases], "evaluator": self.evaluator.to_dict()} | ||
|
|
||
| def to_file(self, file_name: str, format: str, directory: str = "dataset_files"): | ||
| def to_file(self, file_name: str, format: str = "json", directory: str = "dataset_files"): | ||
| """ | ||
| Write the dataset to a file. | ||
|
|
||
|
|
@@ -304,7 +347,7 @@ def from_dict(cls, data: dict, custom_evaluators: list[Evaluator] = []): | |
| return cls(cases=cases, evaluator=evaluator) | ||
|
|
||
| @classmethod | ||
| def from_file(cls, file_path: str, format: str, custom_evaluators: list[Evaluator] = []): | ||
| def from_file(cls, file_path: str, format: str = "json", custom_evaluators: list[Evaluator] = []): | ||
| """ | ||
| Create a dataset from a file. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ def compose_test_prompt( | |
| if evaluation_case.actual_output: | ||
| evaluation_prompt += f"<Output>{evaluation_case.actual_output}</Output>\n" | ||
| else: | ||
| if not evaluation_case.actual_output: | ||
| if evaluation_case.actual_output is None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: what's the intention of changing it to None instead of not?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. None is the default so we never want to include it, but if the user for some reason want an empty list or array, 'not' would not include it. |
||
| raise Exception( | ||
| "Please make sure the task function return the output or a dictionary with the key 'output'." | ||
| ) | ||
|
|
@@ -48,7 +48,7 @@ def compose_test_prompt( | |
| evaluation_prompt += f"<ExpectedOutput>{evaluation_case.expected_output}</ExpectedOutput>\n" | ||
|
|
||
| if uses_trajectory: # trajectory evaluations require actual_trajectory | ||
| if not evaluation_case.actual_trajectory: | ||
| if evaluation_case.actual_trajectory is None: | ||
| raise Exception("Please make sure the task function return a dictionary with the key 'trajectory'.") | ||
| evaluation_prompt += f"<Trajectory>{evaluation_case.actual_trajectory}</Trajectory>\n" | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.