From ac6a4fba147a7043de9d16cfd7ab34f8a278331b Mon Sep 17 00:00:00 2001 From: afourney Date: Thu, 25 Jan 2024 16:46:58 -0800 Subject: [PATCH] Introduces AutoGenBench (#1048) * Initial commit of AutoGenBench * wording * typo * pre-commit reformulation * Updated README to point to contributor's guide earlier. * Simplified the description of the JSON format. * Added print statements to indicate when run.sh and scenario.py are starting. * Added SocietyOfMind scenario to GAIA. * Pointing autogenbench clone command to the latest branch. * Temporarily disable subsample option. * Updated the GAIA readme to specify how to define a BING API key. * Fixed and re-enabled the subsample option. * Added a draft of a blog post. * Updated authors. * Incorporating Gagan's feedback. * Fixed code formatting. * Updated the help string in the docs. * Light editing of the AutoGenBench blogpost. * Support filtering on model tags. * Added websurfer dependencies to Dockerfile. * Renamed testbed -> autogenbench * Attempting to fix formatting. * Added more gracefull handling of task timeouts (the script is allowed to terminate before Docker is stopped). * Updated the blogpost based on Saleema's and Julia's feedback. * Fixed formatting... again. * Added a main MANIFEST to list available scenarios. * Limit main manifest to directories. * Manifests now use relative paths. * All manifests are now relative. * Updated the contributing guide, and address windows path issues. * Updated the version. Fixed formatting. * Fixed formatting. * De-listing Examples, since it has no clear tabulate criteria. * Updated email in pyproject * typo in blogpost * wording --------- Co-authored-by: Qingyun Wu Co-authored-by: Qingyun Wu --- samples/tools/autogenbench/CONTRIBUTING.md | 188 ++++++ samples/tools/autogenbench/MANIFEST.in | 4 + samples/tools/autogenbench/README.md | 172 +++++ .../autogenbench/autogenbench/__init__.py | 1 + .../autogenbench/autogenbench/__main__.py | 4 + .../tools/autogenbench/autogenbench/cli.py | 93 +++ .../autogenbench/autogenbench/clone_cmd.py | 147 +++++ .../autogenbench/autogenbench/load_module.py | 12 + .../autogenbench/res}/Dockerfile | 2 +- .../autogenbench/autogenbench/run_cmd.py | 613 ++++++++++++++++++ .../autogenbench/autogenbench/tabulate_cmd.py | 212 ++++++ .../autogenbench/template}/global_finalize.sh | 0 .../autogenbench/template}/global_init.sh | 0 .../autogenbench/template/requirements.txt | 1 + .../autogenbench/template}/testbed_utils.py | 0 .../autogenbench/autogenbench/version.py | 1 + samples/tools/autogenbench/pyproject.toml | 49 ++ .../custom_python/test_pwd.py | 0 .../10_password_generator/data.json | 0 .../custom_python/test_file_organize.py | 0 .../Challenges}/11_file_organizer/data.json | 0 .../custom_python/test_url_shorten.py | 0 .../Challenges}/12_url_shortener/data.json | 0 .../custom_python/test_tictactoe.py | 0 .../Challenges}/13_tic_tac_toe/data.json | 0 .../1_sort_csv/artifacts_in/input.csv | 0 .../AutoGPT/Challenges}/1_sort_csv/data.json | 0 .../2_combine_csv/artifacts_in/file1.csv | 0 .../2_combine_csv/artifacts_in/file2.csv | 0 .../Challenges}/2_combine_csv/data.json | 0 .../3_qa_small_csv/artifacts_in/file1.csv | 0 .../Challenges}/3_qa_small_csv/data.json | 0 .../4_qa_csv/artifacts_in/file1.csv | 0 .../AutoGPT/Challenges}/4_qa_csv/data.json | 0 .../AutoGPT/Challenges}/5_search/data.json | 0 .../Challenges}/6_book_price/data.json | 0 .../AutoGPT/Challenges}/7_revenue/data.json | 0 .../Challenges}/8_get_information/data.json | 0 .../custom_python/test_three_sum.py | 0 .../AutoGPT/Challenges}/9_three_sum/data.json | 0 .../scenarios/AutoGPT/MANIFEST.json | 35 + .../autogenbench/scenarios/AutoGPT/README.md | 12 + .../AutoGPT/Scripts/custom_tabulate.py | 11 + .../scenarios/AutoGPT/Scripts/init_tasks.py | 108 +++ .../AutoGPT/Templates/TwoAgents/check.py | 32 +- .../AutoGPT/Templates/TwoAgents/scenario.py | 56 ++ .../Templates/TwoAgents/scenario_init.sh | 1 + .../TwoAgents/should_contain.json.txt} | 0 .../TwoAgents/should_not_contain.json.txt} | 0 .../autogenbench/scenarios/Examples/ENV.json | 3 + .../scenarios/Examples/MANIFEST.json | 9 + .../autogenbench/scenarios/Examples/README.md | 11 + .../Examples/Tasks/default_three_agents.jsonl | 1 + .../Examples/Tasks/default_two_agents.jsonl | 3 + .../Templates/ThreeAgents/scenario.py | 5 +- .../Examples/Templates/TwoAgents/scenario.py | 5 +- .../Templates/TwoAgents/scenario_finalize.sh | 0 .../Templates/TwoAgents/scenario_init.sh | 0 .../autogenbench/scenarios/GAIA/MANIFEST.json | 12 + .../autogenbench/scenarios/GAIA/README.md | 39 ++ .../scenarios/GAIA/Scripts/custom_tabulate.py | 49 ++ .../scenarios/GAIA/Scripts/init_tasks.py | 152 +++++ .../BasicTwoAgents/expected_answer.txt | 0 .../GAIA/Templates/BasicTwoAgents/scenario.py | 8 +- .../SocietyOfMind/expected_answer.txt | 1 + .../Templates/SocietyOfMind/requirements.txt | 4 + .../GAIA/Templates/SocietyOfMind/scenario.py | 182 ++++++ .../scenarios/HumanEval/MANIFEST.json | 10 + .../scenarios/HumanEval/README.md | 21 + .../HumanEval/Scripts/custom_tabulate.py | 11 + .../HumanEval/Scripts/init_tasks.py} | 40 +- .../GroupChatFourAgents/coding/my_tests.py | 0 .../Templates/GroupChatFourAgents/prompt.txt | 0 .../Templates/GroupChatFourAgents/scenario.py | 5 +- .../coding/my_tests.py | 0 .../prompt.txt | 0 .../scenario.py | 5 +- .../coding/my_tests.py | 0 .../prompt.txt | 0 .../scenario.py | 5 +- .../Templates/TwoAgents/coding/my_tests.py | 13 + .../HumanEval/Templates/TwoAgents/prompt.txt | 0 .../HumanEval/Templates/TwoAgents/scenario.py | 5 +- .../autogenbench/scenarios/MANIFEST.json | 8 + .../autogenbench/scenarios/MATH/MANIFEST.json | 11 + .../autogenbench/scenarios/MATH/README.md | 19 + .../scenarios/MATH/Scripts/custom_tabulate.py | 26 + .../scenarios/MATH/Scripts/init_tasks.py | 117 ++++ .../Templates/TwoAgents/expected_answer.txt} | 0 .../MATH/Templates/TwoAgents}/prompt.txt | 0 .../MATH/Templates/TwoAgents}/scenario.py | 45 +- .../MATH/Templates/TwoAgents/scenario_init.sh | 1 + samples/tools/autogenbench/setup.py | 3 + samples/tools/testbed/README.md | 239 ------- samples/tools/testbed/includes/ENV.example | 1 - .../testbed/includes/math_requirements.txt | 4 - .../tools/testbed/includes/requirements.txt | 5 - samples/tools/testbed/run_scenarios.py | 474 -------------- .../tools/testbed/scenarios/AutoGPT/README.md | 3 - .../AutoGPT/Templates/TwoAgents/scenario.py | 55 -- .../Examples/default_three_agents_gpt35.jsonl | 1 - .../Examples/default_three_agents_gpt4.jsonl | 1 - .../Examples/default_two_agents_gpt35.jsonl | 3 - .../Examples/default_two_agents_gpt4.jsonl | 3 - .../testbed/scenarios/HumanEval/README.md | 1 - .../Templates/TwoAgents/coding/my_tests.py | 10 - .../tools/testbed/scenarios/MATH/README.md | 27 - .../scenarios/MATH/count_correct_math.py | 56 -- .../scenarios/MATH/problems_to_json.py | 77 --- .../tools/testbed/utils/collate_autogpt.py | 108 --- .../tools/testbed/utils/collate_gaia_csv.py | 128 ---- .../tools/testbed/utils/collate_gaia_jsonl.py | 76 --- .../tools/testbed/utils/collate_human_eval.py | 98 --- samples/tools/testbed/utils/expand_gaia.py | 110 ---- samples/tools/testbed/utils/metrics_gaia.py | 97 --- .../tools/testbed/utils/metrics_human_eval.py | 116 ---- .../tools/testbed/utils/prepare_autogpt.py | 102 --- .../2024-01-25-AutoGenBench/img/teaser.jpg | Bin 0 -> 240855 bytes .../blog/2024-01-25-AutoGenBench/index.mdx | 131 ++++ 119 files changed, 2626 insertions(+), 1883 deletions(-) create mode 100644 samples/tools/autogenbench/CONTRIBUTING.md create mode 100644 samples/tools/autogenbench/MANIFEST.in create mode 100644 samples/tools/autogenbench/README.md create mode 100644 samples/tools/autogenbench/autogenbench/__init__.py create mode 100644 samples/tools/autogenbench/autogenbench/__main__.py create mode 100644 samples/tools/autogenbench/autogenbench/cli.py create mode 100644 samples/tools/autogenbench/autogenbench/clone_cmd.py create mode 100644 samples/tools/autogenbench/autogenbench/load_module.py rename samples/tools/{testbed => autogenbench/autogenbench/res}/Dockerfile (91%) create mode 100644 samples/tools/autogenbench/autogenbench/run_cmd.py create mode 100644 samples/tools/autogenbench/autogenbench/tabulate_cmd.py rename samples/tools/{testbed/includes => autogenbench/autogenbench/template}/global_finalize.sh (100%) rename samples/tools/{testbed/includes => autogenbench/autogenbench/template}/global_init.sh (100%) create mode 100644 samples/tools/autogenbench/autogenbench/template/requirements.txt rename samples/tools/{testbed/includes => autogenbench/autogenbench/template}/testbed_utils.py (100%) create mode 100644 samples/tools/autogenbench/autogenbench/version.py create mode 100644 samples/tools/autogenbench/pyproject.toml rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/10_password_generator/custom_python/test_pwd.py (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/10_password_generator/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/11_file_organizer/custom_python/test_file_organize.py (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/11_file_organizer/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/12_url_shortener/custom_python/test_url_shorten.py (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/12_url_shortener/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/13_tic_tac_toe/custom_python/test_tictactoe.py (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/13_tic_tac_toe/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/1_sort_csv/artifacts_in/input.csv (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/1_sort_csv/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/2_combine_csv/artifacts_in/file1.csv (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/2_combine_csv/artifacts_in/file2.csv (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/2_combine_csv/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/3_qa_small_csv/artifacts_in/file1.csv (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/3_qa_small_csv/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/4_qa_csv/artifacts_in/file1.csv (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/4_qa_csv/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/5_search/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/6_book_price/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/7_revenue/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/8_get_information/data.json (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/9_three_sum/custom_python/test_three_sum.py (100%) rename samples/tools/{testbed/scenarios/AutoGPT/challenges => autogenbench/scenarios/AutoGPT/Challenges}/9_three_sum/data.json (100%) create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/README.md create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/Scripts/custom_tabulate.py create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/Scripts/init_tasks.py rename samples/tools/{testbed => autogenbench}/scenarios/AutoGPT/Templates/TwoAgents/check.py (73%) create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario.py create mode 100644 samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario_init.sh rename samples/tools/{testbed/scenarios/AutoGPT/Templates/TwoAgents/should_contain.txt => autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_contain.json.txt} (100%) rename samples/tools/{testbed/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.txt => autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.json.txt} (100%) create mode 100644 samples/tools/autogenbench/scenarios/Examples/ENV.json create mode 100644 samples/tools/autogenbench/scenarios/Examples/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/Examples/README.md create mode 100644 samples/tools/autogenbench/scenarios/Examples/Tasks/default_three_agents.jsonl create mode 100644 samples/tools/autogenbench/scenarios/Examples/Tasks/default_two_agents.jsonl rename samples/tools/{testbed => autogenbench}/scenarios/Examples/Templates/ThreeAgents/scenario.py (93%) rename samples/tools/{testbed => autogenbench}/scenarios/Examples/Templates/TwoAgents/scenario.py (88%) rename samples/tools/{testbed => autogenbench}/scenarios/Examples/Templates/TwoAgents/scenario_finalize.sh (100%) rename samples/tools/{testbed => autogenbench}/scenarios/Examples/Templates/TwoAgents/scenario_init.sh (100%) create mode 100644 samples/tools/autogenbench/scenarios/GAIA/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/GAIA/README.md create mode 100644 samples/tools/autogenbench/scenarios/GAIA/Scripts/custom_tabulate.py create mode 100644 samples/tools/autogenbench/scenarios/GAIA/Scripts/init_tasks.py rename samples/tools/{testbed => autogenbench}/scenarios/GAIA/Templates/BasicTwoAgents/expected_answer.txt (100%) rename samples/tools/{testbed => autogenbench}/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py (94%) create mode 100644 samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/expected_answer.txt create mode 100644 samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/requirements.txt create mode 100644 samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/scenario.py create mode 100644 samples/tools/autogenbench/scenarios/HumanEval/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/HumanEval/README.md create mode 100644 samples/tools/autogenbench/scenarios/HumanEval/Scripts/custom_tabulate.py rename samples/tools/{testbed/utils/download_humaneval.py => autogenbench/scenarios/HumanEval/Scripts/init_tasks.py} (75%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatFourAgents/coding/my_tests.py (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatFourAgents/prompt.txt (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py (97%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/coding/my_tests.py (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/prompt.txt (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py (96%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/coding/my_tests.py (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/prompt.txt (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py (97%) create mode 100644 samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/TwoAgents/prompt.txt (100%) rename samples/tools/{testbed => autogenbench}/scenarios/HumanEval/Templates/TwoAgents/scenario.py (94%) create mode 100644 samples/tools/autogenbench/scenarios/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/MATH/MANIFEST.json create mode 100644 samples/tools/autogenbench/scenarios/MATH/README.md create mode 100644 samples/tools/autogenbench/scenarios/MATH/Scripts/custom_tabulate.py create mode 100644 samples/tools/autogenbench/scenarios/MATH/Scripts/init_tasks.py rename samples/tools/{testbed/scenarios/MATH/answer.txt => autogenbench/scenarios/MATH/Templates/TwoAgents/expected_answer.txt} (100%) rename samples/tools/{testbed/scenarios/MATH => autogenbench/scenarios/MATH/Templates/TwoAgents}/prompt.txt (100%) rename samples/tools/{testbed/scenarios/MATH => autogenbench/scenarios/MATH/Templates/TwoAgents}/scenario.py (76%) create mode 100644 samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario_init.sh create mode 100644 samples/tools/autogenbench/setup.py delete mode 100644 samples/tools/testbed/README.md delete mode 100644 samples/tools/testbed/includes/ENV.example delete mode 100644 samples/tools/testbed/includes/math_requirements.txt delete mode 100644 samples/tools/testbed/includes/requirements.txt delete mode 100644 samples/tools/testbed/run_scenarios.py delete mode 100644 samples/tools/testbed/scenarios/AutoGPT/README.md delete mode 100644 samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/scenario.py delete mode 100644 samples/tools/testbed/scenarios/Examples/default_three_agents_gpt35.jsonl delete mode 100644 samples/tools/testbed/scenarios/Examples/default_three_agents_gpt4.jsonl delete mode 100644 samples/tools/testbed/scenarios/Examples/default_two_agents_gpt35.jsonl delete mode 100644 samples/tools/testbed/scenarios/Examples/default_two_agents_gpt4.jsonl delete mode 100644 samples/tools/testbed/scenarios/HumanEval/README.md delete mode 100644 samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py delete mode 100644 samples/tools/testbed/scenarios/MATH/README.md delete mode 100644 samples/tools/testbed/scenarios/MATH/count_correct_math.py delete mode 100644 samples/tools/testbed/scenarios/MATH/problems_to_json.py delete mode 100644 samples/tools/testbed/utils/collate_autogpt.py delete mode 100644 samples/tools/testbed/utils/collate_gaia_csv.py delete mode 100644 samples/tools/testbed/utils/collate_gaia_jsonl.py delete mode 100644 samples/tools/testbed/utils/collate_human_eval.py delete mode 100644 samples/tools/testbed/utils/expand_gaia.py delete mode 100644 samples/tools/testbed/utils/metrics_gaia.py delete mode 100644 samples/tools/testbed/utils/metrics_human_eval.py delete mode 100644 samples/tools/testbed/utils/prepare_autogpt.py create mode 100755 website/blog/2024-01-25-AutoGenBench/img/teaser.jpg create mode 100644 website/blog/2024-01-25-AutoGenBench/index.mdx diff --git a/samples/tools/autogenbench/CONTRIBUTING.md b/samples/tools/autogenbench/CONTRIBUTING.md new file mode 100644 index 000000000000..ec32906eea2e --- /dev/null +++ b/samples/tools/autogenbench/CONTRIBUTING.md @@ -0,0 +1,188 @@ +# Contributing to AutoGenBench + +As part of the broader AutoGen project, AutoGenBench welcomes community contributions. Contributions are subject to AutoGen's [contribution guidelines](https://microsoft.github.io/autogen/docs/Contribute), as well as a few additional AutoGenBench-specific requirements outlined here. You may also wish to develop your own private benchmark scenarios and the guidance in this document will help with such efforts as well. Below you will find the general requirements, followed by a detailed technical description. + +## General Contribution Requirements +We ask that all contributions to AutoGenBench adhere to the following: + +- Follow AutoGen's broader [contribution guidelines](https://microsoft.github.io/autogen/docs/Contribute) +- All AutoGenBench benchmarks should live in a subfolder of `/samples/tools/autogenbench/scenarios` alongside `HumanEval`, `GAIA`, etc. +- Benchmark scenarios should include a detailed README.md, in the root of their folder, describing the benchmark and providing citations where warranted. +- Benchmark data (tasks, ground truth, etc.) should be downloaded from their original sources rather than hosted in the AutoGen repository (unless the benchmark is original, and the repository *is* the original source) + - You can use the `Scripts/init_tasks.py` file to automate this download. +- Basic scoring should be compatible with the `autogenbench tabulate` command (e.g., by outputting logs compatible with the default tabulation mechanism, or by providing a `Scripts/custom_tabulate.py` file) +- If you wish your benchmark to be compatible with the `autogenbench clone` command, include a `MANIFEST.json` file in the root of your folder. + +These requirements are further detailed below, but if you simply copy the `HumanEval` folder, you will already be off to a great start. + +## Implementing and Running Benchmark Tasks +At the core of any benchmark is a set of tasks. To implement tasks that are runnable by AutoGenBench, you must adhere to AutoGenBench's templating and scenario expansion algorithms, as outlined below. + +### Task Definitions + +All tasks are stored in JSONL files (in subdirectories under `./Tasks`). Each line of a tasks file is a JSON object with the following schema: + +``` +{ + "id": string, + "template": dirname, + "substitutions" { + "filename1": { + "find_string1_1": replace_string1_1, + "find_string1_2": replace_string1_2, + ... + "find_string1_M": replace_string1_N + } + "filename2": { + "find_string2_1": replace_string2_1, + "find_string2_2": replace_string2_2, + ... + "find_string2_N": replace_string2_N + } + } +} +``` + +For example: + +``` +{ + "id": "two_agent_stocks_gpt4", + "template": "default_two_agents", + "substitutions": { + "scenario.py": { + "__MODEL__": "gpt-4", + }, + "prompt.txt": { + "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." + } + } +} +``` + +In this example, the string `__MODEL__` will be replaced in the file `scenarios.py`, while the string `__PROMPT__` will be replaced in the `prompt.txt` file. + +The `template` field can also take on a list value, but this usage is considered advanced and is not described here. See the `autogenbench/run_cmd.py` code, or the `GAIA` benchmark tasks files for additional information about this option. + + +## Task Instance Expansion Algorithm + +Once the tasks have been defined, as per above, they must be "instantiated" before they can be run. This instantiation happens automatically when the user issues the `autogenbench run` command and involves creating a local folder to share with Docker. Each instance and repetition gets its own folder along the path: `./results/[scenario]/[task_id]/[instance_id]`. For the sake of brevity we will refer to this folder as the `DEST_FOLDER`. + +The algorithm for populating the `DEST_FOLDER` is as follows: + +1. Pre-populate DEST_FOLDER with all the basic starter files for running a scenario (found in `autogenbench/template`). +2. Recursively copy the template folder specified in the JSONL line to DEST_FOLDER (if the JSON `template` attribute points to a folder) If the JSONs `template` attribute instead points to a file, copy the file, but rename it to `scenario.py` +3. Apply any string replacements, as outlined in the prior section. +4. Write a run.sh file to DEST_FOLDER that will be executed by Docker when it is loaded. The `run.sh` is described below. + +## Scenario Execution Algorithm + +Once the task has been instantiated it is run (via run.sh). This script will execute the following steps: + +1. If a file named `global_init.sh` is present, run it. +2. If a file named `scenario_init.sh` is present, run it. +3. Install the requirements.txt file (if running in Docker) +4. Run the task via `python scenario.py` +5. If the scenario.py exited cleanly (exit code 0), then print "SCENARIO.PY COMPLETE !#!#" +6. Clean up (delete cache, etc.) +7. If a file named `scenario_finalize.sh` is present, run it. +8. If a file named `global_finalize.sh` is present, run it. +9. echo "RUN COMPLETE !#!#", signaling that all steps completed. + +Notably, this means that scenarios can add custom init and teardown logic by including `scenario_init.sh` and `scenario_finalize.sh` files. + +At the time of this writing, the run.sh file is as follows: + +```sh +export AUTOGEN_TESTBED_SETTING="Docker" +umask 000 + +# Run the global init script if it exists +if [ -f global_init.sh ] ; then + . ./global_init.sh +fi + +# Run the scenario init script if it exists +if [ -f scenario_init.sh ] ; then + . ./scenario_init.sh +fi + +# Run the scenario +pip install -r requirements.txt +python scenario.py +EXIT_CODE=$? +if [ $EXIT_CODE -ne 0 ]; then + echo SCENARIO.PY EXITED WITH CODE: $EXIT_CODE !#!# +else + echo SCENARIO.PY COMPLETE !#!# +fi + +# Clean up +if [ -d .cache ] ; then + rm -Rf .cache +fi + +# Run the scenario finalize script if it exists +if [ -f scenario_finalize.sh ] ; then + . ./scenario_finalize.sh +fi + +# Run the global finalize script if it exists +if [ -f global_finalize.sh ] ; then + . ./global_finalize.sh +fi + +echo RUN.SH COMPLETE !#!# +``` + +Be warned that this listing is provided here for illustration purposes, and may vary over time. The source of truth are the `run.sh` files found in the ``./results/[taskset]/[task_id]/[instance_id]`` folders. + + +## Integrating with the `tabulate` and `clone` commands. + +The above details are sufficient for defining and running tasks, but if you wish to support the `autogenbench tabulate` and `autogenbench clone` commands, a few additional steps are required. + +### Tabulations + +If you wish to leverage the default tabulation logic, it is as simple as arranging your `scenario.py` file to output the string "ALL TESTS PASSED !#!#" to the console in the event that a task was solved correctly. + +If you wish to implement your own tabulation logic, simply create the file `Scripts/custom_tabulate.py` and include a `main(args)` method. Here, the `args` parameter will be provided by AutoGenBench, and is a drop-in replacement for `sys.argv`. In particular, `args[0]` will be the invocation command (similar to the executable or script name in `sys.argv`), and the remaining values (`args[1:]`) are the command line parameters. + +Should you provide a custom tabulation script, please implement `--help` and `-h` options for documenting your interface. + +The `scenarios/GAIA/Scripts/custom_tabulate.py` is a great example of custom tabulation. It also shows how you can reuse some components of the default tabulator to speed up development. + + +### Cloning + +If you wish your benchmark to be available via the `autogenbench clone` command, you will need to take three additional steps: + +#### Manifest +First, provide a `MANIFEST.json` file in the root of your benchmark. An example is provided below, from which you can see the schema: + +```json +{ + "files": { + "Templates/TwoAgents/prompt.txt": "Templates/TwoAgents/prompt.txt", + "Templates/TwoAgents/coding/my_tests.py": "Templates/TwoAgents/coding/my_tests.py", + "Templates/TwoAgents/scenario.py": "Templates/TwoAgents/scenario.py", + "README.md": "README.md", + "Scripts/init_tasks.py": "Scripts/init_tasks.py", + "Scripts/custom_tabulate.py": "Scripts/custom_tabulate.py" + } +} +``` + +The keys of the `files` dictionary are local paths, relative to your benchmark's root directory. The values are relative paths in the AutoGen GitHub repository (relative to the folder where the MANIFEST.json file is located). In most cases, the keys and values will be identical. + +#### SCENARIOS dictionary +Second, you must add an entry to the `scenarios` dictionary in `autogen/samples/tools/autogenbench/scenarios/MANIFEST.json`. + +#### Scripts/init_tasks.py +Finally, you should provide an `Scripts/init_tasks.py` file, in your benchmark folder, and include a `main()` method therein. This method will be loaded and called automatically by `autogenbench clone` after all manifest files have been downloaded. + +This `init_tasks.py` script is a great place to download benchmarks from their original sources and convert them to the JSONL format required by AutoGenBench: +- See `HumanEval/Scripts/init_tasks.py` for an example of how to expand a benchmark from an original GitHub repository. +- See `GAIA/Scripts/init_tasks.py` for an example of how to expand a benchmark from `Hugging Face Hub`. +- See `MATH/SCripts/init_tasks.py` for an example of how to expand a benchmark from an author-hosted website. diff --git a/samples/tools/autogenbench/MANIFEST.in b/samples/tools/autogenbench/MANIFEST.in new file mode 100644 index 000000000000..84654bcd6e40 --- /dev/null +++ b/samples/tools/autogenbench/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-exclude scenarios * +recursive-exclude results * +recursive-exclude tests * +recursive-exclude utils * diff --git a/samples/tools/autogenbench/README.md b/samples/tools/autogenbench/README.md new file mode 100644 index 000000000000..9c747c9896db --- /dev/null +++ b/samples/tools/autogenbench/README.md @@ -0,0 +1,172 @@ +# AutoGenBench + +AutoGenBench is a tool for repeatedly running a set of pre-defined AutoGen tasks in a setting with tightly-controlled initial conditions. With each run, AutoGenBench will start from a blank slate. The agents being evaluated will need to work out what code needs to be written, and what libraries or dependencies to install, to solve tasks. The results of each run are logged, and can be ingested by analysis or metrics scripts (such as `autogenbench tabulate`). By default, all runs are conducted in freshly-initialized docker containers, providing the recommended level of consistency and safety. + +AutoGenBench works with all AutoGen 0.1.*, and 0.2.* versions. + +## Technical Specifications + +If you are already an AutoGenBench pro, and want the full technical specifications, please review the [contributor's guide](CONTRIBUTING.md). + + +## Docker Requirement +AutoGenBench also requires Docker (Desktop or Engine). **It will not run in GitHub codespaces**, unless you opt for native execution (with is strongly discouraged). To install Docker Desktop see [https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/). + +## Installation and Setup + +**To get the most out of AutoGenBench, the `autogenbench` package should be installed**. At present, the easiest way to do this is to install it via `pip`: + +``` +pip install autogenbench +``` + +If you would prefer working from source code (e.g., for development, or to utilize an alternate branch), simply clone the [AutoGen](https://github.com/microsoft/autogen) repository, then install `autogenbench` via: + +``` +pip install -e autogen/samples/tools/autogenbench +``` + +After installation, you must configure your API keys. As with other AutoGen applications, AutoGenBench will look for the OpenAI keys in the OAI_CONFIG_LIST file in the current working directory, or the OAI_CONFIG_LIST environment variable. This behavior can be overridden using a command-line parameter described later. + +If you will be running multiple benchmarks, it is often most convenient to leverage the environment variable option. You can load your keys into the environment variable by executing: + +``` +export OAI_CONFIG_LIST=$(cat ./OAI_CONFIG_LIST) +``` + +If an OAI_CONFIG_LIST is *not* provided (by means of file or environment variable), AutoGenBench will use the OPENAI_API_KEY environment variable instead. + + +For some benchmark scenarios, additional keys may be required (e.g., keys for the Bing Search API). These can be added to an `ENV.json` file in the current working folder. An example `ENV.json` file is provided below: + +``` +{ + "BING_API_KEY": "xxxyyyzzz" +} +``` + +## A Typical Session +Once AutoGenBench and necessary keys are installed, a typical session will look as follows: + +``` +autogenbench clone HumanEval +cd HumanEval +autogenbench run Tasks/r_human_eval_two_agents.jsonl +autogenbench tabulate results/r_human_eval_two_agents +``` + +Where: +- `autogenbench clone HumanEval` downloads and expands the HumanEval benchmark scenario. +- `autogenbench run Tasks/r_human_eval_two_agents.jsonl` runs the tasks defined in `Tasks/r_human_eval_two_agents.jsonl` +- `autogenbench tablue results/r_human_eval_two_agents` tabulates the results of the run + +Each of these commands has extensive in-line help via: + +- `autogenbench --help` +- `autogenbench clone --help` +- `autogenbench run --help` +- `autogenbench tabulate --help` + +**NOTE:** If you are running `autogenbench` from within the repository, you don’t need to run `autogenbench clone`. Instead, navigate to the appropriate scenario folder (e.g., `scenarios/HumanEval`) and run the `Scripts/init_tasks.py` file. + +More details of each command are provided in the sections that follow. + +## Cloning Benchmarks +To clone an existing benchmark, simply run: +``` +autogenbench clone [BENCHMARK] +``` + +For example, + +``` +autogenbench clone HumanEval +``` + +To see which existing benchmarks are available to clone, run: + +``` +autogenbench clone --list +``` + +## Running AutoGenBench + +To run a benchmark (which executes the tasks, but does not compute metrics), simply execute: +``` +cd [BENCHMARK] +autogenbench run Tasks +``` + +For example, +``` +cd HumanEval +autogenbench run Tasks +``` + +The default is to run each task once. To run each scenario 10 times, use: + +``` +autogenbench run --repeat 10 Tasks +``` + +The `autogenbench` command-line tool allows a number of command-line arguments to control various parameters of execution. Type ``autogenbench -h`` to explore these options: + +``` +'autogenbench run' will run the specified autogen scenarios for a given number of repetitions and record all logs and trace information. When running in a Docker environment (default), each run will begin from a common, tightly controlled, environment. The resultant logs can then be further processed by other scripts to produce metrics. + +positional arguments: + scenario The JSONL scenario file to run. If a directory is specified, + then all JSONL scenarios in the directory are run. (default: + ./scenarios) + +options: + -h, --help show this help message and exit + -c CONFIG, --config CONFIG + The environment variable name or path to the OAI_CONFIG_LIST (default: OAI_CONFIG_LIST). + -r REPEAT, --repeat REPEAT + The number of repetitions to run for each scenario (default: 1). + -s SUBSAMPLE, --subsample SUBSAMPLE + Run on a subsample of the tasks in the JSONL file(s). If a decimal value is specified, then run on + the given proportion of tasks in each file. For example "0.7" would run on 70% of tasks, and "1.0" + would run on 100% of tasks. If an integer value is specified, then randomly select *that* number of + tasks from each specified JSONL file. For example "7" would run tasks, while "1" would run only 1 + task from each specified JSONL file. (default: 1.0; which is 100%) + -m MODEL, --model MODEL + Filters the config_list to include only models matching the provided model name (default: None, which + is all models). + --requirements REQUIREMENTS + The requirements file to pip install before running the scenario. + -d DOCKER_IMAGE, --docker-image DOCKER_IMAGE + The Docker image to use when running scenarios. Can not be used together with --native. (default: + 'autogenbench:default', which will be created if not present) + --native Run the scenarios natively rather than in docker. NOTE: This is not advisable, and should be done + with great caution. +``` + +## Results + +By default, the AutoGenBench stores results in a folder hierarchy with the following template: + +``./results/[scenario]/[task_id]/[instance_id]`` + +For example, consider the following folders: + +``./results/default_two_agents/two_agent_stocks/0`` +``./results/default_two_agents/two_agent_stocks/1`` + +... + +``./results/default_two_agents/two_agent_stocks/9`` + +This folder holds the results for the ``two_agent_stocks`` task of the ``default_two_agents`` tasks file. The ``0`` folder contains the results of the first instance / run. The ``1`` folder contains the results of the second run, and so on. You can think of the _task_id_ as mapping to a prompt, or a unique set of parameters, while the _instance_id_ defines a specific attempt or run. + +Within each folder, you will find the following files: + +- *timestamp.txt*: records the date and time of the run, along with the version of the pyautogen library installed +- *console_log.txt*: all console output produced by Docker when running AutoGen. Read this like you would a regular console. +- *[agent]_messages.json*: for each Agent, a log of their messages dictionaries +- *./coding*: A directory containing all code written by AutoGen, and all artifacts produced by that code. + +## Contributing or Defining New Tasks or Benchmarks + +If you would like to develop -- or even contribute -- your own tasks or benchmarks, please review the [contributor's guide](CONTRIBUTING.md) for complete technical details. diff --git a/samples/tools/autogenbench/autogenbench/__init__.py b/samples/tools/autogenbench/autogenbench/__init__.py new file mode 100644 index 000000000000..58f3ace6c03d --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/__init__.py @@ -0,0 +1 @@ +from .version import __version__ diff --git a/samples/tools/autogenbench/autogenbench/__main__.py b/samples/tools/autogenbench/autogenbench/__main__.py new file mode 100644 index 000000000000..9ae637f13cd5 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() diff --git a/samples/tools/autogenbench/autogenbench/cli.py b/samples/tools/autogenbench/autogenbench/cli.py new file mode 100644 index 000000000000..dd0ebd70ea74 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/cli.py @@ -0,0 +1,93 @@ +import sys +from .run_cmd import run_cli +from .clone_cmd import clone_cli +from .tabulate_cmd import tabulate_cli + + +def main(args=None): + if args is None: + args = sys.argv[:] # Shallow copy + + invocation_cmd = "autogenbench" + + commands = [ + { + "command": "clone", + "description": "download and expand a benchmark", + "function": clone_cli, + }, + { + "command": "run", + "description": "run a given benchmark configuration", + "function": run_cli, + }, + { + "command": "tabulate", + "description": "tabulate the results of a previous run", + "function": tabulate_cli, + }, + {"command": "--help", "description": "print this message", "function": None}, + ] + + # Some help string formatting + commands_list = ", ".join(["'" + c["command"] + "'" for c in commands]) + max_command_len = max([len(c["command"]) for c in commands]) + commands_details = "" + for c in commands: + padded_cmd = c["command"] + while len(padded_cmd) < max_command_len: + padded_cmd = " " + padded_cmd + commands_details += f" {padded_cmd}: {c['description']}\n" + + usage_text = f""" +usage: {invocation_cmd} COMMAND ARGS + +Where, COMMAND is one of: {commands_list} + +and ARGS are specific to the command. +(use '{invocation_cmd} COMMAND --help' for command-specific help) +""".strip() + + help_text = f""" +usage: {invocation_cmd} COMMAND ARGS + +{invocation_cmd} is a tool for running and managing AutoGen benchmark scenarios. A typically session might resemble: + + {invocation_cmd} clone HumanEval + cd HumanEval + {invocation_cmd} run Tasks/human_eval_two_agents_gpt4.jsonl + +which will download the HumanEval benchmark, expand it, and then run the benchmark once with the `human_eval_two_agents_gpt4` configuration. + +Available COMMANDs include: + +{commands_details} + +Additionally, you can use the --help option with any command for further command-specific instructions. E.g., + + {invocation_cmd} run --help + {invocation_cmd} clone --help + +""".strip() + + if len(args) < 2: + sys.stderr.write(usage_text + "\n") + sys.exit(2) + + for command in commands: + if args[1].lower() == command["command"]: + if command["function"] is None: + sys.stderr.write(help_text + "\n") + sys.exit(0) + else: + command["function"]([invocation_cmd + " " + command["command"]] + args[2:]) + sys.exit(0) + + # Command not found + sys.stderr.write(f"Invalid command '{args[1]}'. Available commands include: {commands_list}\n") + sys.exit(2) + + +############################################################################### +if __name__ == "__main__": + main() diff --git a/samples/tools/autogenbench/autogenbench/clone_cmd.py b/samples/tools/autogenbench/autogenbench/clone_cmd.py new file mode 100644 index 000000000000..db9904d68ec8 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/clone_cmd.py @@ -0,0 +1,147 @@ +import os +import json +import argparse +import requests +from .load_module import load_module + +# Figure out where everything is +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +# Where are the manifests located? +DEFAULT_REPO = "https://raw.githubusercontent.com/microsoft/autogen/" +DEFAULT_BRANCH = "main" +DEFAULT_PATH = "/samples/tools/autogenbench/scenarios/" +# Full url is specified by DEFAULT_REPO + DEFAULT_BRANCH + DEFAULT_PATH + + +def _expand_url(url_fragment, base_url): + """ + If the url is a relative path, append the URL_PREFIX, otherwise return it whole. + """ + if url_fragment.startswith("http://") or url_fragment.startswith("https://"): + return url_fragment + else: + return base_url + url_fragment + + +def get_scenarios(base_url): + """ + Return a list of scenarios. + """ + response = requests.get(_expand_url("MANIFEST.json", base_url), stream=False) + response.raise_for_status() + manifest = json.loads(response.text) + return manifest["scenarios"] + + +def clone_scenario(scenario, base_url): + # If the scenario is a url, then we can just look up that folder directly + if scenario.startswith("http://") or scenario.startswith("https://"): + scenario_url = scenario + local_folder = os.path.abspath(".") + # otherwise, read it from the main manifest file + else: + scenarios = get_scenarios(base_url) + if scenario not in scenarios: + raise ValueError(f"No such scenario '{scenario}'.") + scenario_url = _expand_url(scenarios[scenario], base_url) + local_folder = os.path.abspath(scenario) + + # Download the manifest + print("Fetching manifest...") + manifest = None + response = requests.get(_expand_url("MANIFEST.json", scenario_url), stream=False) + response.raise_for_status() + manifest = json.loads(response.text) + + # Download the files + for item in manifest["files"].items(): + path = item[0] + + # Fixes paths on windows + parts = path.split("/") + path = os.path.join(*parts) + + raw_url = _expand_url(item[1], scenario_url) + dir_name = os.path.join(local_folder, os.path.dirname(path)) + file_name = os.path.basename(path) + path = os.path.join(dir_name, file_name) + + print(f"'{raw_url}' -> '{path}'") + + # Make the directory + os.makedirs(dir_name, exist_ok=True) + + # Send a HTTP request to the URL + response = requests.get(raw_url, stream=True) + response.raise_for_status() + + # If the HTTP request returns a status code 200, proceed + with open(path, "wb") as fh: + for chunk in response.iter_content(chunk_size=512): + fh.write(chunk) + + # Run any init_tasks scripts + init_tasks_script = os.path.join(local_folder, "Scripts", "init_tasks.py") + if os.path.isfile(init_tasks_script): + load_module(init_tasks_script).main() + + # Print the success + print(f"\n\nSuccessfully cloned '{scenario}'") + for readme in ["README.md", "README.txt", "README"]: + if os.path.isfile(os.path.join(local_folder, readme)): + print(f"Please read '{os.path.join(local_folder, readme)}' for more information on running this benchmark.") + break + + +def clone_cli(args): + invocation_cmd = args[0] + args = args[1:] + + # Prepare the argument parser + parser = argparse.ArgumentParser( + prog=invocation_cmd, + description=f"{invocation_cmd} will clone the specified scenario to the current working directory.", + ) + + parser.add_argument( + "scenario", + nargs="?", + help="The name of the scenario clone.", + ) + parser.add_argument( + "-l", + "--list", + action="store_true", + help="List the scenarios available for download.", + ) + parser.add_argument( + "-b", + "--branch", + type=str, + help=f"The specific branch in the AutoGen GitHub repository from which scenarios will be cloned (default: {DEFAULT_BRANCH}).", + default=DEFAULT_BRANCH, + ) + + parsed_args = parser.parse_args(args) + + # Generate the base_url + base_url = DEFAULT_REPO + parsed_args.branch + DEFAULT_PATH + + # Check if we are just printing a list + if parsed_args.list: + print("The following scenarios / benchmarks are available:\n") + for s in get_scenarios(base_url): + print(f" {s}") + print() + return 0 + + if not parsed_args.scenario: + parser.error("the following arguments are required: scenario") + + try: + clone_scenario(parsed_args.scenario, base_url) + except ValueError as e: + parser.error(str(e) + "\nUse '--list' to see a list of available scenarios.") diff --git a/samples/tools/autogenbench/autogenbench/load_module.py b/samples/tools/autogenbench/autogenbench/load_module.py new file mode 100644 index 000000000000..bf18242593c7 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/load_module.py @@ -0,0 +1,12 @@ +import os +import sys +import importlib.util + + +def load_module(module_path): + module_name = os.path.basename(module_path).replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module diff --git a/samples/tools/testbed/Dockerfile b/samples/tools/autogenbench/autogenbench/res/Dockerfile similarity index 91% rename from samples/tools/testbed/Dockerfile rename to samples/tools/autogenbench/autogenbench/res/Dockerfile index 6ce06f93a621..5c3f5f40968c 100644 --- a/samples/tools/testbed/Dockerfile +++ b/samples/tools/autogenbench/autogenbench/res/Dockerfile @@ -9,7 +9,7 @@ RUN pip install --upgrade pip RUN ln -snf /usr/share/zoneinfo/US/Pacific /etc/localtime && echo "US/Pacific" > /etc/timezone # Pre-load autogen dependencies, but not autogen itself since we'll often want to install the latest from source -RUN pip install pyautogen[teachable,lmm,graphs] +RUN pip install pyautogen[teachable,lmm,graphs,websurfer] RUN pip uninstall --yes pyautogen # Pre-load popular packages as per https://learnpython.com/blog/most-popular-python-packages/ diff --git a/samples/tools/autogenbench/autogenbench/run_cmd.py b/samples/tools/autogenbench/autogenbench/run_cmd.py new file mode 100644 index 000000000000..c29f064d56e6 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/run_cmd.py @@ -0,0 +1,613 @@ +import os +import errno +import shutil +import subprocess +import json +import sys +import time +import pathlib +import argparse +import docker +import random +from autogen import config_list_from_json +from autogen.oai.openai_utils import filter_config + +# Figure out where everything is +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +TASK_TIMEOUT = 60 * 30 # 30 minutes + +BASE_TEMPLATE_PATH = os.path.join(SCRIPT_DIR, "template") +RESOURCES_PATH = os.path.join(SCRIPT_DIR, "res") + +# What platform are we running? +IS_WIN32 = sys.platform == "win32" + +# This is the tag given to the image that is *built* when no other image is provided. +# Do not use this field to specify the name of an existing image (e.g., on Dockerhub) +DEFAULT_DOCKER_IMAGE_TAG = "autogenbench:default" + +DEFAULT_ENV_FILE = "ENV.json" + + +# Get a random number generator for subsampling +subsample_rng = random.Random(425) + + +def run_scenarios( + scenario, + n_repeats, + is_native, + config_list, + requirements, + docker_image=None, + results_dir="Results", + subsample=None, +): + """ + Run a set autogenbench scenarios a given number of times. + + Args: + scenario (path): The file or folder containing the scenario JSONL instances. If given a folder, then + all JSONL files in the folder will be loaded and run. + n_repeats (int): The number of times each scenario instance will be repeated + is_native (bool): True if the scenario should be run locally rather than in Docker (proceed with caution!) + config_list (list): An Autogen OAI_CONFIG_LIST to be used when running scenarios. + results_dir (path): The folder were results will be saved. + """ + + files = [] + + # Figure out which files or folders we are working with + if scenario == "-" or os.path.isfile(scenario): + files.append(scenario) + elif os.path.isdir(scenario): + for f in os.listdir(scenario): + scenario_file = os.path.join(scenario, f) + + if not os.path.isfile(scenario_file): + continue + + if not scenario_file.lower().endswith(".jsonl"): + continue + + files.append(scenario_file) + else: + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), scenario) + + # Run all the scenario files + for scenario_file in files: + scenario_name = None + scenario_dir = None + file_handle = None + + # stdin + if scenario_file == "-": + scenario_name = "stdin" + scenario_dir = "." + file_handle = sys.stdin + else: + scenario_name = os.path.basename(scenario_file).split(".") + scenario_name.pop() + scenario_name = ".".join(scenario_name) + scenario_dir = os.path.dirname(os.path.realpath(scenario_file)) + file_handle = open(scenario_file, "rt") + + # Read all the lines, then subsample if needed + lines = [line for line in file_handle] + if subsample is not None: + # How many lines are we sampling + n = 0 + # It's a proportion + if 0 <= subsample < 1: + n = int(len(lines) * subsample + 0.5) + # It's a raw count + else: + n = int(subsample) + n = max(0, min(n, len(lines))) + lines = subsample_rng.sample(lines, n) + + for line in lines: + instance = json.loads(line) + + # Create a folder to store the results + # Results base + if not os.path.isdir(results_dir): + os.mkdir(results_dir) + + # Results for the scenario + results_scenario = os.path.join(results_dir, scenario_name) + if not os.path.isdir(results_scenario): + os.mkdir(results_scenario) + + # Results for the instance + results_instance = os.path.join(results_scenario, instance["id"]) + if not os.path.isdir(results_instance): + os.mkdir(results_instance) + + # Results for the repeats + for i in range(0, n_repeats): + results_repetition = os.path.join(results_instance, str(i)) + + # Skip it if it already exists + if os.path.isdir(results_repetition): + print(f"Found folder {results_repetition} ... Skipping.") + continue + print(f"Running scenario {results_repetition}") + + # Expand the scenario + expand_scenario(scenario_dir, instance, results_repetition, requirements) + + # Prepare the environment (keys/values that need to be added) + env = get_scenario_env(config_list) + + # Run the scenario + if is_native: + run_scenario_natively(results_repetition, env) + else: + run_scenario_in_docker( + results_repetition, + env, + docker_image=docker_image, + ) + + # Close regular files + if scenario_file != "-": + file_handle.close() + + +def expand_scenario(scenario_dir, scenario, output_dir, requirements): + """ + Expand a scenario into a folder. + Despite some awkwardness created by backwards compatibility and notational conveniences, expansion is conceptually simple. + It is a series of copy commands (similar to `cp -R`), followed by a series of in-place fine and replace operations. + """ + + template = scenario["template"] + + # Either key works for finding the substiturions list. "values" may be deprecated in the future + substitutions = scenario["substitutions"] if "substitutions" in scenario else scenario["values"] + + # Older versions are only one-level deep. Convert them, + if len(substitutions) > 0 and isinstance(substitutions[next(iter(substitutions))], str): + substitutions = {"scenario.py": substitutions} + + copy_operations = [] + + # Handle file (str), folder (str), or mapping (List) templates + if isinstance(template, str): + template_path = os.path.join(scenario_dir, template) + if os.path.isdir(template_path): + copy_operations.append((template, "")) + else: + copy_operations.append((template, "scenario.py")) + elif isinstance(template, list): + for elm in template: + if isinstance(elm, list): + copy_operations.append((elm[0], elm[1])) + else: + copy_operations.append((elm, "")) + else: + raise ValueError("expand_scenario expects an str or list for 'template'") + + # The global includes folder is always copied + shutil.copytree( + BASE_TEMPLATE_PATH, + output_dir, + ignore=shutil.ignore_patterns("*.example"), + dirs_exist_ok=False, + ) + + # Expand other folders + for items in copy_operations: + src_path = pathlib.Path(os.path.join(scenario_dir, items[0])).absolute() + dest_path = pathlib.Path(os.path.join(output_dir, items[1])).absolute() + + if os.path.isdir(src_path): + shutil.copytree(src_path, dest_path, dirs_exist_ok=True) + else: + if os.path.isdir(dest_path): + # If the destination is a directory, use the same filename + shutil.copyfile(src_path, os.path.join(dest_path, os.path.basename(src_path))) + else: + # Otherwuse use the filename provided + shutil.copyfile(src_path, dest_path) + + # Copy the requirements file if specified + if requirements is not None: + shutil.copyfile(requirements, pathlib.Path(os.path.join(output_dir, "requirements.txt"))) + + # Expand templated files + for templated_file in substitutions.keys(): # Keys are relative file paths + # Read the templated file into memory + template_contents = list() + with open(os.path.join(output_dir, templated_file), "rt") as fh: + for line in fh: + template_contents.append(line) + + # Rewrite the templated file with substitutions + values = substitutions[templated_file] + with open(os.path.join(output_dir, templated_file), "wt") as fh: + for line in template_contents: + for k, v in values.items(): + line = line.replace(k, v) + fh.write(line) + + +def get_scenario_env(config_list, env_file=DEFAULT_ENV_FILE): + """ + Return a dictionary of environment variables needed to run a scenario. + + Args: + config_list (list): An Autogen OAI_CONFIG_LIST to be used when running scenarios. + env_file (str): The path to the env_file to read. (default: DEFAULT_ENV_FILE) + + Returns: A dictionary of keys and values that need to be added to the system environment. + """ + env = dict() + if os.path.isfile(env_file): + with open(env_file, "rt") as fh: + env = json.loads(fh.read()) + + config_list_json = json.dumps(config_list) + env["OAI_CONFIG_LIST"] = config_list_json + + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key is not None and len(openai_api_key.strip()) > 0: + env["OPENAI_API_KEY"] = openai_api_key + + return env + + +def run_scenario_natively(work_dir, env, timeout=TASK_TIMEOUT): + """ + Run a scenario in the native environment. + + Args: + work_dir (path): the path to the working directory previously created to house this sceario instance + """ + + # Get the current working directory + cwd = os.getcwd() + + # Prepare the environment variables + full_env = os.environ.copy() + full_env.update(env) + + # Navigate to the scenario + os.chdir(work_dir) + print("\n\n" + os.getcwd() + "\n===================================================================") + + # Prepare the run script + with open(os.path.join("run.sh"), "wt") as f: + f.write( + f"""# +echo RUN.SH STARTING !#!# +export AUTOGEN_TESTBED_SETTING="Native" + +# Run the global init script if it exists +if [ -f global_init.sh ] ; then + . ./global_init.sh +fi + +# Run the scenario init script if it exists +if [ -f scenario_init.sh ] ; then + . ./scenario_init.sh +fi + +# Run the scenario +echo SCENARIO.PY STARTING !#!# +timeout --preserve-status --kill-after {timeout + 30}s {timeout}s python scenario.py +EXIT_CODE=$? +if [ $EXIT_CODE -ne 0 ]; then + echo SCENARIO.PY EXITED WITH CODE: $EXIT_CODE !#!# +else + echo SCENARIO.PY COMPLETE !#!# +fi + +# Clean up +if [ -d .cache ] ; then + rm -Rf .cache +fi + +# Run the scenario finalize script if it exists +if [ -f scenario_finalize.sh ] ; then + . ./scenario_finalize.sh +fi + +# Run the global finalize script if it exists +if [ -f global_finalize.sh ] ; then + . ./global_finalize.sh +fi + +echo RUN.SH COMPLETE !#!# +""" + ) + + # Run the script and log the output + with open("console_log.txt", "wb") as f: + process = subprocess.Popen( + ["sh", "run.sh"], + env=full_env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + for c in iter(lambda: process.stdout.read(1), b""): + f.write(c) + os.write(sys.stdout.fileno(), c) # Write binary to stdout + + # Return where we started + os.chdir(cwd) + return + + +def run_scenario_in_docker(work_dir, env, timeout=TASK_TIMEOUT, docker_image=None): + """ + Run a scenario in a Docker environment. + + Args: + work_dir (path): the path to the working directory previously created to house this sceario instance + timeout (Optional, int): the number of seconds to allow a Docker container to run before timing out + """ + + client = docker.from_env() + image = None + + # If the docker_image is None, then we will fetch DEFAULT_DOCKER_IMAGE_TAG, if present, + # or build it if missing. + if docker_image is None: + # Pull a suitable image + try: + image = client.images.get(DEFAULT_DOCKER_IMAGE_TAG) + except docker.errors.ImageNotFound: + print(f"Building default Docker image '{DEFAULT_DOCKER_IMAGE_TAG}'. This may take a few minutes...") + try: + build_default_docker_image(client, DEFAULT_DOCKER_IMAGE_TAG) + image = client.images.get(DEFAULT_DOCKER_IMAGE_TAG) + except docker.errors.DockerException: + print(f"Failed to build image '{DEFAULT_DOCKER_IMAGE_TAG}'") + + # Otherwise get the requested image + else: + try: + image = client.images.get(docker_image) + except docker.errors.ImageNotFound: + # pull the image + print(f"Pulling image '{docker_image}'") + try: + image = client.images.pull(docker_image) + except docker.errors.DockerException: + print(f"Failed to pull image '{docker_image}'") + + # Prepare the run script + with open(os.path.join(work_dir, "run.sh"), "wt", newline="\n") as f: + f.write( + f"""# +echo RUN.SH STARTING !#!# +export AUTOGEN_TESTBED_SETTING="Docker" +umask 000 + +# Run the global init script if it exists +if [ -f global_init.sh ] ; then + . ./global_init.sh +fi + +# Run the scenario init script if it exists +if [ -f scenario_init.sh ] ; then + . ./scenario_init.sh +fi + +# Run the scenario +pip install -r requirements.txt +echo SCENARIO.PY STARTING !#!# +timeout --preserve-status --kill-after {timeout + 30}s {timeout}s python scenario.py +EXIT_CODE=$? +if [ $EXIT_CODE -ne 0 ]; then + echo SCENARIO.PY EXITED WITH CODE: $EXIT_CODE !#!# +else + echo SCENARIO.PY COMPLETE !#!# +fi + +# Clean up +if [ -d .cache ] ; then + rm -Rf .cache +fi + +# Run the scenario finalize script if it exists +if [ -f scenario_finalize.sh ] ; then + . ./scenario_finalize.sh +fi + +# Run the global finalize script if it exists +if [ -f global_finalize.sh ] ; then + . ./global_finalize.sh +fi + +echo RUN.SH COMPLETE !#!# +""" + ) + + print("\n\n" + work_dir + "\n===================================================================") + + # Create and run the container + abs_path = str(pathlib.Path(work_dir).absolute()) + container = client.containers.run( + image, + command=["sh", "run.sh"], + working_dir="/workspace", + environment=env, + detach=True, + # get absolute path to the working directory + volumes={abs_path: {"bind": "/workspace", "mode": "rw"}}, + ) + + # Read the logs in a streaming fashion. Keep an eye on the time to make sure we don't need to stop. + docker_timeout = timeout + 60 # One full minute after the bash timeout command should have already triggered + start_time = time.time() + logs = container.logs(stream=True) + log_file = open(os.path.join(work_dir, "console_log.txt"), "wt") + stopping = False + + for chunk in logs: # When streaming it should return a generator + # Stream the data to the log file and the console + chunk = chunk.decode("utf-8") + log_file.write(chunk) + log_file.flush() + sys.stdout.write(chunk) + sys.stdout.flush() + + # Check if we need to terminate + if not stopping and time.time() - start_time >= docker_timeout: + container.stop() + + # Don't exit the loop right away, as there are things we may still want to read from the logs + # but remember how we got here. + stopping = True + + if stopping: # By this line we've exited the loop, and the container has actually stopped. + log_file.write("\nDocker timed out.\n") + log_file.flush() + sys.stdout.write("\nDocker timed out.\n") + sys.stdout.flush() + + +def build_default_docker_image(docker_client, image_tag): + for segment in docker_client.api.build( + path=RESOURCES_PATH, + dockerfile="Dockerfile", + rm=True, + tag=image_tag, + decode=True, + ): + if "stream" in segment: + sys.stdout.write(segment["stream"]) + + +def run_cli(args): + invocation_cmd = args[0] + args = args[1:] + + # Prepare the argument parser + parser = argparse.ArgumentParser( + prog=invocation_cmd, + description=f"{invocation_cmd} will run the specified AutoGen scenarios for a given number of repetitions and record all logs and trace information. When running in a Docker environment (default), each run will begin from a common, tightly controlled, environment. The resultant logs can then be further processed by other scripts to produce metrics.".strip(), + ) + + parser.add_argument( + "scenario", + help="The JSONL scenario file to run. If a directory is specified, then all JSONL scenarios in the directory are run. If set to '-', then read from stdin.", + ) + parser.add_argument( + "-c", + "--config", + type=str, + help="The environment variable name or path to the OAI_CONFIG_LIST (default: OAI_CONFIG_LIST).", + default="OAI_CONFIG_LIST", + ) + parser.add_argument( + "-r", + "--repeat", + type=int, + help="The number of repetitions to run for each scenario (default: 1).", + default=1, + ) + parser.add_argument( + "-s", + "--subsample", + type=str, + help='Run on a subsample of the tasks in the JSONL file(s). If a decimal value is specified, then run on the given proportion of tasks in each file. For example "0.7" would run on 70%% of tasks, and "1.0" would run on 100%% of tasks. If an integer value is specified, then randomly select *that* number of tasks from each specified JSONL file. For example "7" would run tasks, while "1" would run only 1 task from each specified JSONL file. (default: 1.0; which is 100%%)', + default=None, + ) + parser.add_argument( + "-m", + "--model", + type=str, + help="Filters the config_list to include only models matching the provided model name or tag (default: None, which is all models).", + default=None, + ) + parser.add_argument( + "--requirements", + type=str, + help="The requirements file to pip install before running the scenario.", + default=None, + ) + parser.add_argument( + "-d", + "--docker-image", + type=str, + help="The Docker image to use when running scenarios. Can not be used together with --native. (default: '" + + DEFAULT_DOCKER_IMAGE_TAG + + "', which will be created if not present)", + default=None, + ) + parser.add_argument( + "--native", + action="store_true", + help="Run the scenarios natively rather than in docker. NOTE: This is not advisable, and should be done with great caution.", + ) + + parsed_args = parser.parse_args(args) + + # Load the OAI_CONFIG_LIST + config_list = config_list_from_json(env_or_file=parsed_args.config) + + # Add the model name to the tags to simplify filtering + for entry in config_list: + if "tags" not in entry: + entry["tags"] = list() + if entry["model"] not in entry["tags"]: + entry["tags"].append(entry["model"]) + + # Filter if requested + if parsed_args.model is not None: + filter_dict = {"tags": [parsed_args.model]} + config_list = filter_config(config_list, filter_dict) + if len(config_list) == 0: + sys.exit( + f"The model configuration list is empty. This may be because the model filter '{parsed_args.model}' returned 0 results." + ) + + # Don't allow both --docker-image and --native on the same command + if parsed_args.docker_image is not None and parsed_args.native: + sys.exit("The options --native and --docker-image can not be used together. Exiting.") + + # Warn if running natively + if parsed_args.native: + if IS_WIN32: + sys.exit("Running scenarios with --native is not supported in Windows. Exiting.") + + if parsed_args.requirements is not None: + sys.exit("--requirements is not compatible with --native. Exiting.") + + choice = input( + 'WARNING: Running natively, without Docker, not only poses the usual risks of executing arbitrary AI generated code on your machine, it also makes it impossible to ensure that each test starts from a known and consistent set of initial conditions. For example, if the agents spend time debugging and installing Python libraries to solve the task, then those libraries will be available to all other runs. In other words, earlier runs can influence later runs, leading to many confounds in testing.\n\nAre you absolutely sure you want to continue with native execution? Type "Yes" exactly, and in full, to proceed: ' + ) + + if choice.strip().lower() != "yes": + sys.exit("Received '" + choice + "'. Exiting.") + + # Parse the subsample + subsample = None + if parsed_args.subsample is not None: + subsample = float(parsed_args.subsample) + if "." in parsed_args.subsample: # Intention is to run on a proportion + if subsample == 1.0: # Intention is to run 100%, which is the default + subsample = None # None means 100% ... which use None to differentiate from the integer 1 + elif subsample < 0 or subsample > 1.0: + raise ( + ValueError( + "Subsample must either be an integer (specified without a decimal), or a Real number between 0.0 and 1.0" + ) + ) + + run_scenarios( + scenario=parsed_args.scenario, + n_repeats=parsed_args.repeat, + is_native=True if parsed_args.native else False, + config_list=config_list, + requirements=parsed_args.requirements, + docker_image=parsed_args.docker_image, + subsample=subsample, + ) diff --git a/samples/tools/autogenbench/autogenbench/tabulate_cmd.py b/samples/tools/autogenbench/autogenbench/tabulate_cmd.py new file mode 100644 index 000000000000..b4b90da80a8a --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/tabulate_cmd.py @@ -0,0 +1,212 @@ +import os +import sys +import argparse +import tabulate as tb +from .load_module import load_module + +# Figure out where everything is +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +TABULATE_FILE = "custom_tabulate.py" + +SUCCESS_STRINGS = [ + "ALL TESTS PASSED !#!#", +] + +EXCLUDE_DIR_NAMES = ["__pycache__"] + + +def find_tabulate_module(search_dir, stop_dir=None): + """Hunt for the tabulate script.""" + + search_dir = os.path.abspath(search_dir) + if not os.path.isdir(search_dir): + raise ValueError(f"'{search_dir}' is not a directory.") + + stop_dir = None if stop_dir is None else os.path.abspath(stop_dir) + + while True: + path = os.path.join(search_dir, TABULATE_FILE) + if os.path.isfile(path): + return path + + path = os.path.join(search_dir, "Scripts", TABULATE_FILE) + if os.path.isfile(path): + return path + + path = os.path.join(search_dir, "scripts", TABULATE_FILE) + if os.path.isfile(path): + return path + + # Stop if we hit the stop_dir + if search_dir == stop_dir: + break + + # Stop if we hit the root + parent_dir = os.path.abspath(os.path.join(search_dir, os.pardir)) + if parent_dir == search_dir: + break + + search_dir = parent_dir + + +def default_scorer(instance_dir, success_strings=SUCCESS_STRINGS): + console_log = os.path.join(instance_dir, "console_log.txt") + if os.path.isfile(console_log): + with open(console_log, "rt") as fh: + content = fh.read() + for s in success_strings: + if s in content: + return True + return False + else: + return None + + +def default_tabulate(args, scorer=default_scorer, exclude_dir_names=EXCLUDE_DIR_NAMES): + invocation_cmd = args[0] + args = args[1:] + + warning = f"CAUTION: '{invocation_cmd}' is in early preview and is not thoroughly tested.\nPlease do not cite values from these calculations in academic work without first inspecting and verifying the results in the run logs yourself." + + # Prepare the argument parser + parser = argparse.ArgumentParser( + prog=invocation_cmd, + description=f"{invocation_cmd} will tabulate the results of a previous run.", + ) + + parser.add_argument( + "runlogs", + help="The path where the run's logs are stored.", + ) + parser.add_argument( + "-c", + "--csv", + action="store_true", + help="Output the results in CSV format.", + ) + + parsed_args = parser.parse_args(args) + + all_results = list() + max_instances = 0 + + for task_id in sorted( + os.listdir(parsed_args.runlogs), + key=lambda s: os.path.getmtime(os.path.join(parsed_args.runlogs, s)), + ): + if task_id in exclude_dir_names: + continue + + task_path = os.path.join(parsed_args.runlogs, task_id) + + if not os.path.isdir(task_path): + continue + + # Collect the results vector + results = [task_id] + + instance = 0 + instance_dir = os.path.join(task_path, str(instance)) + while os.path.isdir(instance_dir): + results.append(scorer(instance_dir)) + instance += 1 + instance_dir = os.path.join(task_path, str(instance)) + + max_instances = max(max_instances, instance) + + # Buffer the results + all_results.append(results) + + if parsed_args.csv: + # Create a header + header = ["Task Id"] + for i in range(0, max_instances): + header.append("Trial " + str(i) + " Success") + + print(",".join(header)) + for row in all_results: + str_row = [f"{v}" if v is not None else "" for v in row] + while len(str_row) < max_instances + 1: + str_row.append("") + print(",".join(str_row)) + + # Print out alpha-version warning + sys.stderr.write("\n" + warning + "\n\n") + else: + # Create a header + header = ["\nTask Id"] + for i in range(0, max_instances): + header.append("Trial " + str(i) + "\nSuccess") + + # Create the footer + def _count_equals(value, trial): + count = 0 + for row in all_results: + # Count missing + if value is None: + if trial + 1 < len(row): + if row[trial + 1] is None: + count += 1 + else: + count += 1 + # Count match + elif trial + 1 < len(row) and row[trial + 1] == value: + count += 1 + return count + + footer = [] + footer_row = ["Successes"] + for i in range(0, max_instances): + footer_row.append(_count_equals(True, i)) + footer.append(footer_row) + + footer_row = ["Failures"] + for i in range(0, max_instances): + footer_row.append(_count_equals(False, i)) + footer.append(footer_row) + + footer_row = ["Missing"] + for i in range(0, max_instances): + footer_row.append(_count_equals(None, i)) + footer.append(footer_row) + + footer_row = ["Total"] + for i in range(0, max_instances): + footer_row.append(footer[0][i + 1] + footer[1][i + 1] + footer[2][i + 1]) + footer.append(footer_row) + + table = all_results.copy() + table.append(tb.SEPARATING_LINE) + table.extend(footer) + + print(tb.tabulate(table, headers=header)) + + # Print out alpha-version warning + sys.stderr.write("\n" + warning + "\n\n") + + +def tabulate_cli(args): + invocation_cmd = args[0] + args = args[1:] + + # We won't assume much about the arguments, letting the dynamically-loaded + # tabulate modules parse the arguments however they want. But, we will use + # bare arguments (not starting a "-"), to help us find what module to load. + module_path = find_tabulate_module(os.getcwd(), stop_dir=os.getcwd()) + for arg in reversed(args): + if module_path is not None: + break + if arg.startswith("-"): + continue + module_path = find_tabulate_module(arg) + + # Load the module and hand over control + if module_path is None: + sys.stderr.write("Using default tabulation method.\n\n") + default_tabulate([invocation_cmd] + args) + else: + sys.stderr.write(f"Using tabulation method defined in '{module_path}'\n\n") + load_module(module_path).main([invocation_cmd] + args) diff --git a/samples/tools/testbed/includes/global_finalize.sh b/samples/tools/autogenbench/autogenbench/template/global_finalize.sh similarity index 100% rename from samples/tools/testbed/includes/global_finalize.sh rename to samples/tools/autogenbench/autogenbench/template/global_finalize.sh diff --git a/samples/tools/testbed/includes/global_init.sh b/samples/tools/autogenbench/autogenbench/template/global_init.sh similarity index 100% rename from samples/tools/testbed/includes/global_init.sh rename to samples/tools/autogenbench/autogenbench/template/global_init.sh diff --git a/samples/tools/autogenbench/autogenbench/template/requirements.txt b/samples/tools/autogenbench/autogenbench/template/requirements.txt new file mode 100644 index 000000000000..46ad1e009ca1 --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/template/requirements.txt @@ -0,0 +1 @@ +pyautogen diff --git a/samples/tools/testbed/includes/testbed_utils.py b/samples/tools/autogenbench/autogenbench/template/testbed_utils.py similarity index 100% rename from samples/tools/testbed/includes/testbed_utils.py rename to samples/tools/autogenbench/autogenbench/template/testbed_utils.py diff --git a/samples/tools/autogenbench/autogenbench/version.py b/samples/tools/autogenbench/autogenbench/version.py new file mode 100644 index 000000000000..ecbf4901d90d --- /dev/null +++ b/samples/tools/autogenbench/autogenbench/version.py @@ -0,0 +1 @@ +__version__ = "0.0.1a12" diff --git a/samples/tools/autogenbench/pyproject.toml b/samples/tools/autogenbench/pyproject.toml new file mode 100644 index 000000000000..339217691d97 --- /dev/null +++ b/samples/tools/autogenbench/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "autogenbench" +authors = [ + { name="Autogen Team", email="auto-gen@outlook.com" }, +] +description = "AutoGen Testbed Tools" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.8, <3.13" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +dependencies = [ + "pyautogen", + "docker", + "huggingface_hub", + "tabulate", +] + +dynamic = ["version"] + +[tool.setuptools] +include-package-data = true + + +[tool.setuptools.dynamic] +version = {attr = "autogenbench.version.__version__"} +readme = {file = ["README.md"]} + +[tool.setuptools.packages.find] +include = ["autogenbench*"] +exclude = ["*.tests*"] + +[tool.setuptools.package-data] +"autogenbench" = ["*.*"] + +[project.urls] +"Homepage" = "https://github.com/microsoft/autogen" +"Bug Tracker" = "https://github.com/microsoft/autogen/issues" + +[project.scripts] +autogenbench = "autogenbench.cli:main" diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/10_password_generator/custom_python/test_pwd.py b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/10_password_generator/custom_python/test_pwd.py similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/10_password_generator/custom_python/test_pwd.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/10_password_generator/custom_python/test_pwd.py diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/10_password_generator/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/10_password_generator/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/10_password_generator/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/10_password_generator/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/11_file_organizer/custom_python/test_file_organize.py b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/11_file_organizer/custom_python/test_file_organize.py similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/11_file_organizer/custom_python/test_file_organize.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/11_file_organizer/custom_python/test_file_organize.py diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/11_file_organizer/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/11_file_organizer/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/11_file_organizer/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/11_file_organizer/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/12_url_shortener/custom_python/test_url_shorten.py b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/12_url_shortener/custom_python/test_url_shorten.py similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/12_url_shortener/custom_python/test_url_shorten.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/12_url_shortener/custom_python/test_url_shorten.py diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/12_url_shortener/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/12_url_shortener/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/12_url_shortener/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/12_url_shortener/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/13_tic_tac_toe/custom_python/test_tictactoe.py b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/13_tic_tac_toe/custom_python/test_tictactoe.py similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/13_tic_tac_toe/custom_python/test_tictactoe.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/13_tic_tac_toe/custom_python/test_tictactoe.py diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/13_tic_tac_toe/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/13_tic_tac_toe/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/13_tic_tac_toe/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/13_tic_tac_toe/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/1_sort_csv/artifacts_in/input.csv b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/1_sort_csv/artifacts_in/input.csv similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/1_sort_csv/artifacts_in/input.csv rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/1_sort_csv/artifacts_in/input.csv diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/1_sort_csv/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/1_sort_csv/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/1_sort_csv/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/1_sort_csv/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/artifacts_in/file1.csv b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/artifacts_in/file1.csv similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/artifacts_in/file1.csv rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/artifacts_in/file1.csv diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/artifacts_in/file2.csv b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/artifacts_in/file2.csv similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/artifacts_in/file2.csv rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/artifacts_in/file2.csv diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/2_combine_csv/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/2_combine_csv/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/3_qa_small_csv/artifacts_in/file1.csv b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/3_qa_small_csv/artifacts_in/file1.csv similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/3_qa_small_csv/artifacts_in/file1.csv rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/3_qa_small_csv/artifacts_in/file1.csv diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/3_qa_small_csv/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/3_qa_small_csv/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/3_qa_small_csv/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/3_qa_small_csv/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/4_qa_csv/artifacts_in/file1.csv b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/4_qa_csv/artifacts_in/file1.csv similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/4_qa_csv/artifacts_in/file1.csv rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/4_qa_csv/artifacts_in/file1.csv diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/4_qa_csv/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/4_qa_csv/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/4_qa_csv/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/4_qa_csv/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/5_search/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/5_search/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/5_search/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/5_search/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/6_book_price/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/6_book_price/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/6_book_price/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/6_book_price/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/7_revenue/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/7_revenue/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/7_revenue/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/7_revenue/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/8_get_information/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/8_get_information/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/8_get_information/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/8_get_information/data.json diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/9_three_sum/custom_python/test_three_sum.py b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/9_three_sum/custom_python/test_three_sum.py similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/9_three_sum/custom_python/test_three_sum.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/9_three_sum/custom_python/test_three_sum.py diff --git a/samples/tools/testbed/scenarios/AutoGPT/challenges/9_three_sum/data.json b/samples/tools/autogenbench/scenarios/AutoGPT/Challenges/9_three_sum/data.json similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/challenges/9_three_sum/data.json rename to samples/tools/autogenbench/scenarios/AutoGPT/Challenges/9_three_sum/data.json diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/MANIFEST.json b/samples/tools/autogenbench/scenarios/AutoGPT/MANIFEST.json new file mode 100644 index 000000000000..d91e5e200d2e --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/MANIFEST.json @@ -0,0 +1,35 @@ +{ + "files": { + "README.md": "README.md", + "Scripts/init_tasks.py": "Scripts/init_tasks.py", + "Scripts/custom_tabulate.py": "Scripts/custom_tabulate.py", + "Templates/TwoAgents/check.py": "Templates/TwoAgents/check.py", + "Templates/TwoAgents/should_contain.json.txt": "Templates/TwoAgents/should_contain.json.txt", + "Templates/TwoAgents/should_not_contain.json.txt": "Templates/TwoAgents/should_not_contain.json.txt", + "Templates/TwoAgents/scenario.py": "Templates/TwoAgents/scenario.py", + "Templates/TwoAgents/scenario_init.sh": "Templates/TwoAgents/scenario_init.sh", + "Challenges/1_sort_csv/data.json": "Challenges/1_sort_csv/data.json", + "Challenges/1_sort_csv/artifacts_in/input.csv": "Challenges/1_sort_csv/artifacts_in/input.csv", + "Challenges/2_combine_csv/data.json": "Challenges/2_combine_csv/data.json", + "Challenges/2_combine_csv/artifacts_in/file1.csv": "Challenges/2_combine_csv/artifacts_in/file1.csv", + "Challenges/2_combine_csv/artifacts_in/file2.csv": "Challenges/2_combine_csv/artifacts_in/file2.csv", + "Challenges/3_qa_small_csv/data.json": "Challenges/3_qa_small_csv/data.json", + "Challenges/3_qa_small_csv/artifacts_in/file1.csv": "Challenges/3_qa_small_csv/artifacts_in/file1.csv", + "Challenges/4_qa_csv/data.json": "Challenges/4_qa_csv/data.json", + "Challenges/4_qa_csv/artifacts_in/file1.csv": "Challenges/4_qa_csv/artifacts_in/file1.csv", + "Challenges/5_search/data.json": "Challenges/5_search/data.json", + "Challenges/6_book_price/data.json": "Challenges/6_book_price/data.json", + "Challenges/7_revenue/data.json": "Challenges/7_revenue/data.json", + "Challenges/8_get_information/data.json": "Challenges/8_get_information/data.json", + "Challenges/9_three_sum/custom_python/test_three_sum.py": "Challenges/9_three_sum/custom_python/test_three_sum.py", + "Challenges/9_three_sum/data.json": "Challenges/9_three_sum/data.json", + "Challenges/10_password_generator/custom_python/test_pwd.py": "Challenges/10_password_generator/custom_python/test_pwd.py", + "Challenges/10_password_generator/data.json": "Challenges/10_password_generator/data.json", + "Challenges/11_file_organizer/custom_python/test_file_organize.py": "Challenges/11_file_organizer/custom_python/test_file_organize.py", + "Challenges/11_file_organizer/data.json": "Challenges/11_file_organizer/data.json", + "Challenges/12_url_shortener/custom_python/test_url_shorten.py": "Challenges/12_url_shortener/custom_python/test_url_shorten.py", + "Challenges/12_url_shortener/data.json": "Challenges/12_url_shortener/data.json", + "Challenges/13_tic_tac_toe/custom_python/test_tictactoe.py": "Challenges/13_tic_tac_toe/custom_python/test_tictactoe.py", + "Challenges/13_tic_tac_toe/data.json": "Challenges/13_tic_tac_toe/data.json" + } +} diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/README.md b/samples/tools/autogenbench/scenarios/AutoGPT/README.md new file mode 100644 index 000000000000..a42c0db0d7a3 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/README.md @@ -0,0 +1,12 @@ +# AutoGPT Benchmark + +This scenario implements an older subset of the [AutoGPT](https://github.com/Significant-Gravitas/Auto-GPT-Benchmarks/tree/master/agbenchmark#readme) benchmark. + +Tasks were selected in November 2023, and may have since been deprecated. They are nonetheless useful for comparison and development. + +## Running the tasks + +``` +autogenbench run Tasks/autogpt__two_agents.jsonl +autogenbench tabulate Results/autogpt__two_agents +``` diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/custom_tabulate.py b/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/custom_tabulate.py new file mode 100644 index 000000000000..ba8700d1f472 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/custom_tabulate.py @@ -0,0 +1,11 @@ +import os +import sys +from autogenbench.tabulate_cmd import default_tabulate + + +def main(args): + default_tabulate(args) + + +if __name__ == "__main__" and __package__ is None: + main(sys.argv) diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/init_tasks.py b/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/init_tasks.py new file mode 100644 index 000000000000..00a6d15ef77f --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/Scripts/init_tasks.py @@ -0,0 +1,108 @@ +# +# Run this file to download the human_eval dataset, and create a corresponding testbed scenario: +# (default: ../scenarios/human_eval_two_agents_gpt4.jsonl and ./scenarios/human_eval_two_agents_gpt35.jsonl) +# + +import json +import os +import sys +import glob +import base64 +from huggingface_hub import snapshot_download + +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +SCENARIO_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir)) +TEMPLATES_DIR = os.path.join(SCENARIO_DIR, "Templates") +TASKS_DIR = os.path.join(SCENARIO_DIR, "Tasks") +CHALLENGES_DIR = os.path.join(SCENARIO_DIR, "Challenges") + + +def create_jsonl(name, template): + """Creates a JSONL scenario file with a given name, and template path.""" + + if not os.path.isdir(TASKS_DIR): + os.mkdir(TASKS_DIR) + + with open(os.path.join(TASKS_DIR, name + ".jsonl"), "wt") as fh: + data_paths = glob.glob(str(CHALLENGES_DIR + "/*/data.json")) + for data_path in data_paths: + print("Converting data path: ", data_path) + workspace = os.path.dirname(data_path) + artifacts = os.path.join(workspace, "artifacts_in") + custom_python = os.path.join(workspace, "custom_python") + + with open(data_path, "r") as f: + data = json.load(f) + + should_contain = data["ground"].get("should_contain", []) + should_not_contain = data["ground"].get("should_not_contain", []) + case_sensitive = data["ground"].get("case_sensitive", False) + + # Figure out what files we need to copy + template_cp_list = [template] + + # Artifacts in + if os.path.exists(artifacts): + template_cp_list.append( + [ + artifacts, + "coding", + ] + ) + + # Custom python + if os.path.exists(custom_python): + template_cp_list.append( + [ + custom_python, + "custom_python", + ] + ) + + record = { + "id": data["name"], + "template": template_cp_list, + "substitutions": { + "scenario.py": { + "__TASK__": data["task"], + }, + "check.py": { + "__FILE_PATTERN__": data["ground"]["files"][0], + "__EVAL_TYPE__": data["ground"]["eval"]["type"], + "__CASE_SENSITIVE__": str(case_sensitive), + }, + "should_contain.json.txt": { + "__CONTAIN__": json.dumps(should_contain), # Double-encoded + }, + "should_not_contain.json.txt": { + "__NO_CONTAIN__": json.dumps(should_not_contain), # Double-encoded + }, + }, + } + + fh.write(json.dumps(record).strip() + "\n") + + +############################################################################### +def main(): + templates = {"two_agents": os.path.join(TEMPLATES_DIR, "TwoAgents")} + + # Add coding directories if needed (these are usually empty and left out of the repo) + for template in templates.values(): + code_dir_path = os.path.join(template, "coding") + if not os.path.isdir(code_dir_path): + os.mkdir(code_dir_path) + + # Create the various combinations of [models] x [templates] + for t in templates.items(): + create_jsonl( + f"autogpt__{t[0]}", + t[1], + ) + + +if __name__ == "__main__" and __package__ is None: + main() diff --git a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/check.py b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/check.py similarity index 73% rename from samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/check.py rename to samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/check.py index 57043d5695a8..da7ea832a80e 100644 --- a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/check.py +++ b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/check.py @@ -1,16 +1,21 @@ -import base64 +# Disable ruff linter for incomplete template files +# ruff: noqa: F821 + import glob import os import subprocess import sys import shutil +import json def scoring(content: str, should_contain: list, should_not_contain: list): - print("\033[1;34mScoring content:\033[0m", content) + is_case_sensitive = __CASE_SENSITIVE__ + + print("\033[1;34mScoring content:\033[0m\n", content) if should_contain: for should_contain_word in should_contain: - if not "__CASE_SENSITIVE__" == "True": + if not is_case_sensitive: should_contain_word = should_contain_word.lower() content = content.lower() if should_contain_word not in content: @@ -19,10 +24,9 @@ def scoring(content: str, should_contain: list, should_not_contain: list): if should_not_contain: for should_not_contain_word in should_not_contain: - if not "__CASE_SENSITIVE__" == "True": + if not is_case_sensitive: should_not_contain_word = should_not_contain_word.lower() content = content.lower() - # print_content = f"\033[1;34mWord that should not exist\033[0m - {should_not_contain_word}:" if should_not_contain_word in content: return 0.0 return 1.0 @@ -35,15 +39,13 @@ def check(): file_pattern = "__FILE_PATTERN__" eval_type = "__EVAL_TYPE__" - with open("../should_contain.txt", "r") as f: - should_contain = eval(f.read()) + with open("../should_contain.json.txt", "r") as f: + should_contain = json.loads(f.read()) assert type(should_contain) == list, "TERMINATE\n" - should_contain = [base64.b64decode(encoded).decode("utf-8") for encoded in should_contain] - with open("../should_not_contain.txt", "r") as f: - should_not_contain = eval(f.read()) + with open("../should_not_contain.json.txt", "r") as f: + should_not_contain = json.loads(f.read()) assert type(should_not_contain) == list, "TERMINATE\n" - should_not_contain = [base64.b64decode(encoded).decode("utf-8") for encoded in should_not_contain] # Check if file pattern is a file extension if file_pattern.startswith("."): @@ -75,14 +77,14 @@ def check(): ) for content in files_contents: - # print("\033[1;34mScoring content:\033[0m", content) score = scoring(content, should_contain, should_not_contain) scores.append(score) if 1.0 in scores: - print("ALL TESTS PASSED!\n\nTERMINATE.") + print("ALL TESTS PASSED !#!#\n\nTERMINATE") else: - print("Test failed.") + print("TEST FAILED !#!#") -check() +if __name__ == "__main__": + check() diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario.py b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario.py new file mode 100644 index 000000000000..eba850334730 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario.py @@ -0,0 +1,56 @@ +# We would like to gracefully handle any exception +# ruff: noqa: E722 + +import traceback +from autogen import AssistantAgent, UserProxyAgent, config_list_from_json +import testbed_utils + +# Assistant agent can call check.py to check if all the unit tests have passed +testbed_utils.init() + +work_dir = "coding" + +config_list = config_list_from_json("OAI_CONFIG_LIST") + +assistant = AssistantAgent( + "assistant", + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + llm_config={ + "config_list": config_list, + }, +) +user_proxy = UserProxyAgent( + "user_proxy", + human_input_mode="NEVER", + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + code_execution_config={ + "work_dir": work_dir, + "use_docker": False, + }, + max_consecutive_auto_reply=5, +) + +message = """ +__TASK__ +""".strip() + +# Solve the task +try: + user_proxy.initiate_chat( + assistant, + message=message, + ) +except: + traceback.print_exc() + +# Check the results +assistant.send( + "```bash\npython ../check.py\n```", + user_proxy, + request_reply=False, + silent=True, +) +reply = user_proxy.generate_reply(sender=assistant) +print(reply) + +testbed_utils.finalize(agents=[assistant, user_proxy]) diff --git a/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario_init.sh b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario_init.sh new file mode 100644 index 000000000000..a5135e2a6c22 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/scenario_init.sh @@ -0,0 +1 @@ +pip install pandas beautifulsoup4 requests pytest diff --git a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/should_contain.txt b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_contain.json.txt similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/should_contain.txt rename to samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_contain.json.txt diff --git a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.txt b/samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.json.txt similarity index 100% rename from samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.txt rename to samples/tools/autogenbench/scenarios/AutoGPT/Templates/TwoAgents/should_not_contain.json.txt diff --git a/samples/tools/autogenbench/scenarios/Examples/ENV.json b/samples/tools/autogenbench/scenarios/Examples/ENV.json new file mode 100644 index 000000000000..a8631378f353 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/Examples/ENV.json @@ -0,0 +1,3 @@ +{ + "BING_API_KEY": "" +} diff --git a/samples/tools/autogenbench/scenarios/Examples/MANIFEST.json b/samples/tools/autogenbench/scenarios/Examples/MANIFEST.json new file mode 100644 index 000000000000..03cfa6a05809 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/Examples/MANIFEST.json @@ -0,0 +1,9 @@ +{ + "files": { + "Templates/TwoAgents/scenario_finalize.sh": "Templates/TwoAgents/scenario_finalize.sh", + "Templates/TwoAgents/scenario.py": "Templates/TwoAgents/scenario.py", + "Templates/TwoAgents/scenario_init.sh": "Templates/TwoAgents/scenario_init.sh", + "Tasks/default_two_agents.jsonl": "Tasks/default_two_agents.jsonl", + "README.md": "README.md" + } +} diff --git a/samples/tools/autogenbench/scenarios/Examples/README.md b/samples/tools/autogenbench/scenarios/Examples/README.md new file mode 100644 index 000000000000..7572f7a15a9b --- /dev/null +++ b/samples/tools/autogenbench/scenarios/Examples/README.md @@ -0,0 +1,11 @@ +# Example Tasks + +Various AutoGen example tasks. Unlike other benchmark tasks, these tasks have no automated evaluation. + +## Running the tasks + +``` +autogenbench run Tasks/default_two_agents +``` + +Some tasks require a Bing API key. Edit the ENV.json file to provide a valid BING_API_KEY, or simply allow that task to fail (it is only required by one task). diff --git a/samples/tools/autogenbench/scenarios/Examples/Tasks/default_three_agents.jsonl b/samples/tools/autogenbench/scenarios/Examples/Tasks/default_three_agents.jsonl new file mode 100644 index 000000000000..a9a2537b1bf1 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/Examples/Tasks/default_three_agents.jsonl @@ -0,0 +1 @@ +{ "id": "nvda_tsla_stocks", "template": "../Templates/ThreeAgents", "substitutions": { "scenario.py": { "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD.", "__SELECTION_METHOD__": "auto", "__3RD_AGENT_NAME__": "visualization_critic", "__3RD_AGENT_PROMPT__": "A student of Edward Tufte, you are an expert in information design, and will provide helpful critiques of visualizations. As you prepare your critiques, please consider the following dimensions:\n- Are there bugs, logic errors, syntax error or typos in the visualization code? Are there any reasons why the code may fail to run? How should it be fixed?\n- Is the data transformed appropriately for the visualization type? E.g., is the dataset appropriated filtered, aggregated, or grouped if needed? If a date field is used, is the date field first converted to a date object etc?\n- How well does the code meet the specified visualization goals?\n- CONSIDERING BEST PRACTICES, is the visualization type appropriate for the data and intent? Is there a visualization type that would be more effective in conveying insights? \n- Are the aesthetics of the visualization appropriate for the visualization type and the data?" } } } diff --git a/samples/tools/autogenbench/scenarios/Examples/Tasks/default_two_agents.jsonl b/samples/tools/autogenbench/scenarios/Examples/Tasks/default_two_agents.jsonl new file mode 100644 index 000000000000..78c3a12f7bd3 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/Examples/Tasks/default_two_agents.jsonl @@ -0,0 +1,3 @@ +{ "id": "nvda_tsla_stocks", "template": "../Templates/TwoAgents", "substitutions": { "scenario.py": { "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." } } } +{ "id": "arxiv_search", "template": "../Templates/TwoAgents", "substitutions": { "scenario.py": { "__PROMPT__": "Find 10 papers on explainable or interpretable AI that were submitted to arXiv within the last year. When printing results, include paper titles, authors, dates, and URLs, but not their abstracts." } } } +{ "id": "old_mslogo_search", "template": "../Templates/TwoAgents", "substitutions": { "scenario.py": { "__PROMPT__": "Find Microsoft's logo from 1983, and save it to disk. If searching the web, use Bing with API key stored in os.environ['BING_API_KEY']" } } } diff --git a/samples/tools/testbed/scenarios/Examples/Templates/ThreeAgents/scenario.py b/samples/tools/autogenbench/scenarios/Examples/Templates/ThreeAgents/scenario.py similarity index 93% rename from samples/tools/testbed/scenarios/Examples/Templates/ThreeAgents/scenario.py rename to samples/tools/autogenbench/scenarios/Examples/Templates/ThreeAgents/scenario.py index cbf383ebfe27..6aa2456deae8 100644 --- a/samples/tools/testbed/scenarios/Examples/Templates/ThreeAgents/scenario.py +++ b/samples/tools/autogenbench/scenarios/Examples/Templates/ThreeAgents/scenario.py @@ -6,10 +6,7 @@ testbed_utils.init() ############################## -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "assistant", diff --git a/samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario.py b/samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario.py similarity index 88% rename from samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario.py rename to samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario.py index 6f736a5aaba5..ae2682562b83 100644 --- a/samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario.py +++ b/samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario.py @@ -6,10 +6,7 @@ testbed_utils.init() ############################## -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "assistant", diff --git a/samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario_finalize.sh b/samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario_finalize.sh similarity index 100% rename from samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario_finalize.sh rename to samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario_finalize.sh diff --git a/samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario_init.sh b/samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario_init.sh similarity index 100% rename from samples/tools/testbed/scenarios/Examples/Templates/TwoAgents/scenario_init.sh rename to samples/tools/autogenbench/scenarios/Examples/Templates/TwoAgents/scenario_init.sh diff --git a/samples/tools/autogenbench/scenarios/GAIA/MANIFEST.json b/samples/tools/autogenbench/scenarios/GAIA/MANIFEST.json new file mode 100644 index 000000000000..807ec57bdc32 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/MANIFEST.json @@ -0,0 +1,12 @@ +{ + "files": { + "README.md": "README.md", + "Scripts/init_tasks.py": "Scripts/init_tasks.py", + "Scripts/custom_tabulate.py": "Scripts/custom_tabulate.py", + "Templates/BasicTwoAgents/expected_answer.txt": "Templates/BasicTwoAgents/expected_answer.txt", + "Templates/BasicTwoAgents/scenario.py": "Templates/BasicTwoAgents/scenario.py", + "Templates/SocietyOfMind/scenario.py": "Templates/SocietyOfMind/scenario.py", + "Templates/SocietyOfMind/expected_answer.txt": "Templates/SocietyOfMind/expected_answer.txt", + "Templates/SocietyOfMind/requirements.txt": "Templates/SocietyOfMind/requirements.txt" + } +} diff --git a/samples/tools/autogenbench/scenarios/GAIA/README.md b/samples/tools/autogenbench/scenarios/GAIA/README.md new file mode 100644 index 000000000000..eb8e6b3339d1 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/README.md @@ -0,0 +1,39 @@ +# GAIA Benchmark + +This scenario implements the [GAIA](https://arxiv.org/abs/2311.12983) agent benchmark. + +## Running the TwoAgents tasks + +Level 1 tasks: +```sh +autogenbench run Tasks/gaia_test_level_1__two_agents.jsonl +autogenbench tabulate Results/gaia_test_level_1__two_agents +``` + +Level 2 and 3 tasks are executed similarly. + +## Running the SocietyOfMind tasks + +Running the SocietyOfMind tasks is similar to the TwoAgentTasks, but requires an `ENV.json` file +with a working BING API key. This file should be located in the root current working directory +from where you are running autogenbench, and should have at least the following contents: + +```json +{ + "BING_API_KEY": "Your_API_key" +} +``` + +Once created, simply run: + +```sh +autogenbench run Tasks/gaia_test_level_1__soc.jsonl +autogenbench tabulate Results/gaia_test_level_1__soc +``` + +And similarly for level 2 and 3. + +## References +**GAIA: a benchmark for General AI Assistants**
+Grégoire Mialon, Clémentine Fourrier, Craig Swift, Thomas Wolf, Yann LeCun, Thomas Scialom
+[https://arxiv.org/abs/2311.12983](https://arxiv.org/abs/2311.12983) diff --git a/samples/tools/autogenbench/scenarios/GAIA/Scripts/custom_tabulate.py b/samples/tools/autogenbench/scenarios/GAIA/Scripts/custom_tabulate.py new file mode 100644 index 000000000000..144f882af24b --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/Scripts/custom_tabulate.py @@ -0,0 +1,49 @@ +import os +import sys +import json +import re +from autogenbench.tabulate_cmd import default_tabulate + + +def normalize_answer(a): + # Lower case + # Trim (left and right) + # Replace multiple spaces with one space + # Remove trailing punctuation + return re.sub(r"[\.\!\?]+$", "", re.sub(r"\s+", " ", a.strip().lower())) + + +def scorer(instance_dir): + # Read the expected answer + expected_answer_file = os.path.join(instance_dir, "expected_answer.txt") + if not os.path.isfile(expected_answer_file): + return None + + expected_answer = None + with open(expected_answer_file, "rt") as fh: + expected_answer = fh.read().strip() + + # Read the console + console_log_file = os.path.join(instance_dir, "console_log.txt") + if not os.path.isfile(console_log_file): + return None + + console_log = "" + with open(console_log_file, "rt") as fh: + console_log = fh.read() + + final_answer = "" + m = re.search(r"FINAL ANSWER:(.*?)\n", console_log, re.DOTALL) + if m: + final_answer = m.group(1).strip() + + # Return true if they are equal after normalization + return normalize_answer(expected_answer) == normalize_answer(final_answer) + + +def main(args): + default_tabulate(args, scorer=scorer) + + +if __name__ == "__main__" and __package__ is None: + main(sys.argv) diff --git a/samples/tools/autogenbench/scenarios/GAIA/Scripts/init_tasks.py b/samples/tools/autogenbench/scenarios/GAIA/Scripts/init_tasks.py new file mode 100644 index 000000000000..3ff483af1817 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/Scripts/init_tasks.py @@ -0,0 +1,152 @@ +# +# Run this file to download the human_eval dataset, and create a corresponding testbed scenario: +# (default: ../scenarios/human_eval_two_agents_gpt4.jsonl and ./scenarios/human_eval_two_agents_gpt35.jsonl) +# + +import json +import os +import sys +from huggingface_hub import snapshot_download + +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +SCENARIO_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir)) +TEMPLATES_DIR = os.path.join(SCENARIO_DIR, "Templates") +TASKS_DIR = os.path.join(SCENARIO_DIR, "Tasks") +DOWNLOADS_DIR = os.path.join(SCENARIO_DIR, "Downloads") +REPO_DIR = os.path.join(DOWNLOADS_DIR, "GAIA") + + +def download_gaia(): + """Download the GAIA benchmark from Hugging Face.""" + + if not os.path.isdir(DOWNLOADS_DIR): + os.mkdir(DOWNLOADS_DIR) + + """Download the GAIA dataset from Hugging Face Hub""" + snapshot_download( + repo_id="gaia-benchmark/GAIA", + repo_type="dataset", + local_dir=REPO_DIR, + local_dir_use_symlinks=True, + ) + + +def create_jsonl(name, tasks, files_dir, template): + """Creates a JSONL scenario file with a given name, and template path.""" + + if not os.path.isdir(TASKS_DIR): + os.mkdir(TASKS_DIR) + + with open(os.path.join(TASKS_DIR, name + ".jsonl"), "wt") as fh: + for task in tasks: + print(f"Converting: [{name}] {task['task_id']}") + + # Figure out what files we need to copy + template_cp_list = [template] + if len(task["file_name"].strip()) > 0: + template_cp_list.append( + [ + os.path.join(files_dir, task["file_name"].strip()), + os.path.join("coding", task["file_name"].strip()), + ] + ) + + record = { + "id": task["task_id"], + "template": template_cp_list, + "substitutions": { + "scenario.py": { + "__FILE_NAME__": task["file_name"], + "__PROMPT__": task["Question"], + }, + "expected_answer.txt": {"__EXPECTED_ANSWER__": task["Final answer"]}, + }, + } + + fh.write(json.dumps(record).strip() + "\n") + + +############################################################################### +def main(): + download_gaia() + + gaia_validation_files = os.path.join(REPO_DIR, "2023", "validation") + gaia_test_files = os.path.join(REPO_DIR, "2023", "test") + + if not os.path.isdir(gaia_validation_files) or not os.path.isdir(gaia_test_files): + sys.exit(f"Error: '{REPO_DIR}' does not appear to be a copy of the GAIA repository.") + + # Load the GAIA data + gaia_validation_tasks = [[], [], []] + with open(os.path.join(gaia_validation_files, "metadata.jsonl")) as fh: + for line in fh: + data = json.loads(line) + gaia_validation_tasks[data["Level"] - 1].append(data) + + gaia_test_tasks = [[], [], []] + with open(os.path.join(gaia_test_files, "metadata.jsonl")) as fh: + for line in fh: + data = json.loads(line) + + # A welcome message -- not a real task + if data["task_id"] == "0-0-0-0-0": + continue + + gaia_test_tasks[data["Level"] - 1].append(data) + + templates = { + "two_agents": os.path.join(TEMPLATES_DIR, "BasicTwoAgents"), + "soc": os.path.join(TEMPLATES_DIR, "SocietyOfMind"), + } + + # Add coding directories if needed (these are usually empty and left out of the repo) + for template in templates.values(): + code_dir_path = os.path.join(template, "coding") + if not os.path.isdir(code_dir_path): + os.mkdir(code_dir_path) + + # Create the various combinations of [models] x [templates] + for t in templates.items(): + create_jsonl( + f"gaia_validation_level_1__{t[0]}", + gaia_validation_tasks[0], + gaia_validation_files, + t[1], + ) + create_jsonl( + f"gaia_validation_level_2__{t[0]}", + gaia_validation_tasks[1], + gaia_validation_files, + t[1], + ) + create_jsonl( + f"gaia_validation_level_3__{t[0]}", + gaia_validation_tasks[2], + gaia_validation_files, + t[1], + ) + create_jsonl( + f"gaia_test_level_1__{t[0]}", + gaia_test_tasks[0], + gaia_test_files, + t[1], + ) + create_jsonl( + f"gaia_test_level_2__{t[0]}", + gaia_test_tasks[1], + gaia_test_files, + t[1], + ) + create_jsonl( + f"gaia_test_level_3__{t[0]}", + gaia_test_tasks[2], + gaia_test_files, + t[1], + ) + + +if __name__ == "__main__" and __package__ is None: + main() diff --git a/samples/tools/testbed/scenarios/GAIA/Templates/BasicTwoAgents/expected_answer.txt b/samples/tools/autogenbench/scenarios/GAIA/Templates/BasicTwoAgents/expected_answer.txt similarity index 100% rename from samples/tools/testbed/scenarios/GAIA/Templates/BasicTwoAgents/expected_answer.txt rename to samples/tools/autogenbench/scenarios/GAIA/Templates/BasicTwoAgents/expected_answer.txt diff --git a/samples/tools/testbed/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py b/samples/tools/autogenbench/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py similarity index 94% rename from samples/tools/testbed/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py rename to samples/tools/autogenbench/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py index f96f88364c4e..5ca7b0a28146 100644 --- a/samples/tools/testbed/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py +++ b/samples/tools/autogenbench/scenarios/GAIA/Templates/BasicTwoAgents/scenario.py @@ -27,11 +27,7 @@ """.strip() ) - -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "assistant", @@ -57,7 +53,7 @@ """.strip() if len(filename) > 0: - question = f"Consider the file '{filename}', which can be read from the current working directory. {question}" + question = f"Consider the file '{filename}', which can be read from the current working directory. If you need to read or write it, output python code in a code block (```python) to do so. {question}" user_proxy.initiate_chat(assistant, message=question) diff --git a/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/expected_answer.txt b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/expected_answer.txt new file mode 100644 index 000000000000..8153c2bf8242 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/expected_answer.txt @@ -0,0 +1 @@ +__EXPECTED_ANSWER__ diff --git a/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/requirements.txt b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/requirements.txt new file mode 100644 index 000000000000..ec2725c530d9 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/requirements.txt @@ -0,0 +1,4 @@ +git+https://github.com/microsoft/autogen.git@society_of_mind_gaia +pdfminer.six +markdownify +pathvalidate diff --git a/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/scenario.py b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/scenario.py new file mode 100644 index 000000000000..129c898e47f7 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/GAIA/Templates/SocietyOfMind/scenario.py @@ -0,0 +1,182 @@ +# ruff: noqa: E722 +import os +import sys +import json +import autogen +import copy +import traceback +from datetime import datetime +import testbed_utils +from autogen.agentchat.contrib.web_surfer import WebSurferAgent +from autogen.agentchat.contrib.society_of_mind_agent import SocietyOfMindAgent +from autogen.agentchat.contrib.group_chat_moderator import GroupChatModerator +from autogen.token_count_utils import count_token, get_max_token_limit + +testbed_utils.init() +############################## + +config_list = autogen.config_list_from_json( + "OAI_CONFIG_LIST", + filter_dict={"model": ["gpt-4"]}, +) +llm_config = testbed_utils.default_llm_config(config_list, timeout=180) +llm_config["temperature"] = 0.1 + +summarizer_config_list = autogen.config_list_from_json( + "OAI_CONFIG_LIST", + filter_dict={"model": ["gpt-3.5-turbo-16k"]}, +) +summarizer_llm_config = testbed_utils.default_llm_config(summarizer_config_list, timeout=180) +summarizer_llm_config["temperature"] = 0.1 + +final_config_list = autogen.config_list_from_json( + "OAI_CONFIG_LIST", + filter_dict={"model": ["gpt-4-1106-preview"]}, +) +final_llm_config = testbed_utils.default_llm_config(final_config_list, timeout=180) +final_llm_config["temperature"] = 0.1 + + +client = autogen.OpenAIWrapper(**final_llm_config) + + +def response_preparer(inner_messages): + tokens = 0 + + messages = [ + { + "role": "user", + "content": """Earlier you were asked the following: + +__PROMPT__ + +Your team then worked diligently to address that request. Here is a transcript of that conversation:""", + } + ] + tokens += count_token(messages[-1]) + + # The first message just repeats the question, so remove it + if len(inner_messages) > 1: + del inner_messages[0] + + # copy them to this context + for message in inner_messages: + message = copy.deepcopy(message) + message["role"] = "user" + messages.append(message) + tokens += count_token(messages[-1]) + + messages.append( + { + "role": "user", + "content": """ +Read the above conversation and output a FINAL ANSWER to the question. The question is repeated here for convenience: + +__PROMPT__ + +To output the final answer, use the following template: FINAL ANSWER: [YOUR FINAL ANSWER] +YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. +If you are asked for a number, don’t use comma to write your number neither use units such as $ or percent sign unless specified otherwise, and don't output any final sentence punctuation such as '.', '!', or '?'. +If you are asked for a string, don’t use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. +If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.""", + } + ) + tokens += count_token(messages[-1]) + + # # Hardcoded + # while tokens > 3200: + # mid = int(len(messages) / 2) # Remove from the middle + # tokens -= count_token(messages[mid]) + # del messages[mid] + + response = client.create(context=None, messages=messages) + extracted_response = client.extract_text_or_completion_object(response)[0] + if not isinstance(extracted_response, str): + return str(extracted_response.model_dump(mode="dict")) # Not sure what to do here + else: + return extracted_response + + +assistant = autogen.AssistantAgent( + "assistant", + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + llm_config=llm_config, +) +user_proxy = autogen.UserProxyAgent( + "computer_terminal", + human_input_mode="NEVER", + description="A computer terminal that performs no other action than running Python scripts (provided to it quoted in ```python code blocks), or sh shell scripts (provided to it quoted in ```sh code blocks)", + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + code_execution_config={ + "work_dir": "coding", + "use_docker": False, + }, + default_auto_reply="", + max_consecutive_auto_reply=15, +) + +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0" + +web_surfer = WebSurferAgent( + "web_surfer", + llm_config=llm_config, + summarizer_llm_config=summarizer_llm_config, + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + browser_config={ + "bing_api_key": os.environ["BING_API_KEY"], + "viewport_size": 1024 * 5, + "downloads_folder": "coding", + "request_kwargs": { + "headers": {"User-Agent": user_agent}, + }, + }, +) + +filename_prompt = "__FILE_NAME__".strip() +if len(filename_prompt) > 0: + filename_prompt = f"Consider the file '{filename_prompt}' which can be read from the current working directory. If you need to read or write it, output python code in a code block (```python) to do so. " + + +question = f""" +Below I will pose a question to you that I would like you to answer. You should begin by listing all the relevant facts necessary to derive an answer, then fill in those facts from memory where possible, including specific names, numbers and statistics. You are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from. After listing the facts, begin to solve the question in earnest. Here is the question: + +{filename_prompt}__PROMPT__ +""".strip() + +groupchat = GroupChatModerator( + agents=[user_proxy, assistant, web_surfer], + first_speaker=assistant, + max_round=30, + messages=[], + speaker_selection_method="auto", + allow_repeat_speaker=[web_surfer, assistant], +) + +manager = autogen.GroupChatManager( + groupchat=groupchat, + is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, + send_introductions=True, + llm_config=llm_config, +) + +soc = SocietyOfMindAgent( + "gaia_agent", + chat_manager=manager, + response_preparer=response_preparer, + llm_config=llm_config, +) + +try: + # Initiate one turn of the conversation + user_proxy.send( + question, + soc, + request_reply=True, + silent=False, + ) +except: + traceback.print_exc() + + +############################## +testbed_utils.finalize(agents=[soc, assistant, user_proxy, web_surfer, manager]) diff --git a/samples/tools/autogenbench/scenarios/HumanEval/MANIFEST.json b/samples/tools/autogenbench/scenarios/HumanEval/MANIFEST.json new file mode 100644 index 000000000000..c6946de003a7 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/HumanEval/MANIFEST.json @@ -0,0 +1,10 @@ +{ + "files": { + "Templates/TwoAgents/prompt.txt": "Templates/TwoAgents/prompt.txt", + "Templates/TwoAgents/coding/my_tests.py": "Templates/TwoAgents/coding/my_tests.py", + "Templates/TwoAgents/scenario.py": "Templates/TwoAgents/scenario.py", + "README.md": "README.md", + "Scripts/init_tasks.py": "Scripts/init_tasks.py", + "Scripts/custom_tabulate.py": "Scripts/custom_tabulate.py" + } +} diff --git a/samples/tools/autogenbench/scenarios/HumanEval/README.md b/samples/tools/autogenbench/scenarios/HumanEval/README.md new file mode 100644 index 000000000000..61a33fb05620 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/HumanEval/README.md @@ -0,0 +1,21 @@ +# HumanEval Benchmark + +This scenario implements a modified version of the [HumanEval](https://arxiv.org/abs/2107.03374) benchmark. +Compared to the original benchmark, there are **two key differences** here: + +- A chat model rather than a completion model is used. +- The agents get pass/fail feedback about their implementations, and can keep trying until they succeed or run out of tokens or turns. + +## Running the tasks + +``` +autogenbench run Tasks/human_eval_two_agents.jsonl +autogenbench tabulate Results/human_eval_two_agents +``` + +For faster development and iteration, a reduced HumanEval set is available via `Tasks/r_human_eval_two_agents.jsonl`, and contains only 26 problems of varying difficulty. + +## References +**Evaluating Large Language Models Trained on Code**
+Mark Chen, Jerry Tworek, Heewoo Jun, Qiming Yuan, Henrique Ponde de Oliveira Pinto, Jared Kaplan, Harri Edwards, Yuri Burda, Nicholas Joseph, Greg Brockman, Alex Ray, Raul Puri, Gretchen Krueger, Michael Petrov, Heidy Khlaaf, Girish Sastry, Pamela Mishkin, Brooke Chan, Scott Gray, Nick Ryder, Mikhail Pavlov, Alethea Power, Lukasz Kaiser, Mohammad Bavarian, Clemens Winter, Philippe Tillet, Felipe Petroski Such, Dave Cummings, Matthias Plappert, Fotios Chantzis, Elizabeth Barnes, Ariel Herbert-Voss, William Hebgen Guss, Alex Nichol, Alex Paino, Nikolas Tezak, Jie Tang, Igor Babuschkin, Suchir Balaji, Shantanu Jain, William Saunders, Christopher Hesse, Andrew N. Carr, Jan Leike, Josh Achiam, Vedant Misra, Evan Morikawa, Alec Radford, Matthew Knight, Miles Brundage, Mira Murati, Katie Mayer, Peter Welinder, Bob McGrew, Dario Amodei, Sam McCandlish, Ilya Sutskever, Wojciech Zaremba
+[https://arxiv.org/abs/2107.03374](https://arxiv.org/abs/2107.03374) diff --git a/samples/tools/autogenbench/scenarios/HumanEval/Scripts/custom_tabulate.py b/samples/tools/autogenbench/scenarios/HumanEval/Scripts/custom_tabulate.py new file mode 100644 index 000000000000..ba8700d1f472 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/HumanEval/Scripts/custom_tabulate.py @@ -0,0 +1,11 @@ +import os +import sys +from autogenbench.tabulate_cmd import default_tabulate + + +def main(args): + default_tabulate(args) + + +if __name__ == "__main__" and __package__ is None: + main(sys.argv) diff --git a/samples/tools/testbed/utils/download_humaneval.py b/samples/tools/autogenbench/scenarios/HumanEval/Scripts/init_tasks.py similarity index 75% rename from samples/tools/testbed/utils/download_humaneval.py rename to samples/tools/autogenbench/scenarios/HumanEval/Scripts/init_tasks.py index c967b5342a3e..799ac7b170ce 100644 --- a/samples/tools/testbed/utils/download_humaneval.py +++ b/samples/tools/autogenbench/scenarios/HumanEval/Scripts/init_tasks.py @@ -69,21 +69,25 @@ def download_human_eval(): return results -def create_jsonl(name, tasks, template, model): - """Creates a JSONL scenario file with a given name, list of HumanEval tasks, template path, and model.""" +def create_jsonl(name, tasks, template): + """Creates a JSONL scenario file with a given name, list of HumanEval tasks, and template path.""" - scenarios_dir = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir, "scenarios", "HumanEval")) + # Create a task directory if it doesn't exist + scenario_dir = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir)) + task_dir = os.path.join(scenario_dir, "Tasks") + if not os.path.isdir(task_dir): + os.mkdir(task_dir) - with open(os.path.join(scenarios_dir, name + ".jsonl"), "wt") as fh: + # Create the jsonl file + with open(os.path.join(task_dir, name + ".jsonl"), "wt") as fh: for task in tasks: print(f"Converting: [{name}] {task['task_id']}") record = { "id": task["task_id"].replace("/", "_"), - "template": template, + "template": os.path.join(os.path.pardir, template), "substitutions": { "scenario.py": { - "__MODEL__": model, "__ENTRY_POINT__": task["entry_point"], "__SELECTION_METHOD__": "auto", }, @@ -96,24 +100,22 @@ def create_jsonl(name, tasks, template, model): ############################################################################### -if __name__ == "__main__": +def main(): human_eval = download_human_eval() reduced_human_eval = [t for t in human_eval if t["task_id"] in REDUCED_SET] - models = { - "gpt4": "gpt-4", - "gpt35": "gpt-3.5-turbo-16k", - } - templates = { "two_agents": "Templates/TwoAgents", - "gc3_distractor": "Templates/GroupChatThreeAgents_Distractor", - "gc3_guardrails": "Templates/GroupChatThreeAgents_Guardrails", - "gc4": "Templates/GroupChatFourAgents", + # "gc3_distractor": "Templates/GroupChatThreeAgents_Distractor", + # "gc3_guardrails": "Templates/GroupChatThreeAgents_Guardrails", + # "gc4": "Templates/GroupChatFourAgents", } # Create the various combinations of [models] x [templates] - for m in models.items(): - for t in templates.items(): - create_jsonl(f"human_eval_{t[0]}_{m[0]}", human_eval, t[1], m[1]) - create_jsonl(f"r_human_eval_{t[0]}_{m[0]}", reduced_human_eval, t[1], m[1]) + for t in templates.items(): + create_jsonl(f"human_eval_{t[0]}", human_eval, t[1]) + create_jsonl(f"r_human_eval_{t[0]}", reduced_human_eval, t[1]) + + +if __name__ == "__main__" and __package__ is None: + main() diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/coding/my_tests.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/coding/my_tests.py similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/coding/my_tests.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/coding/my_tests.py diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/prompt.txt b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/prompt.txt similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/prompt.txt rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/prompt.txt diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py similarity index 97% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py index c49166ce7eb1..dacf7b83906e 100644 --- a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py +++ b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatFourAgents/scenario.py @@ -20,10 +20,7 @@ PROMPT = fh.read() # Ok, now get autogen to solve it. -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "coder", diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/coding/my_tests.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/coding/my_tests.py similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/coding/my_tests.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/coding/my_tests.py diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/prompt.txt b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/prompt.txt similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/prompt.txt rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/prompt.txt diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py similarity index 96% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py index ef8d339429a0..06b88638d471 100644 --- a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py +++ b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Distractor/scenario.py @@ -20,10 +20,7 @@ PROMPT = fh.read() # Ok, now get autogen to solve it. -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "coder", diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/coding/my_tests.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/coding/my_tests.py similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/coding/my_tests.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/coding/my_tests.py diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/prompt.txt b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/prompt.txt similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/prompt.txt rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/prompt.txt diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py similarity index 97% rename from samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py index 258fdcecc2a7..a4708a55853d 100644 --- a/samples/tools/testbed/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py +++ b/samples/tools/autogenbench/scenarios/HumanEval/Templates/GroupChatThreeAgents_Guardrails/scenario.py @@ -20,10 +20,7 @@ PROMPT = fh.read() # Ok, now get autogen to solve it. -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "coder", diff --git a/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py new file mode 100644 index 000000000000..d93c24296e22 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py @@ -0,0 +1,13 @@ +# Disable ruff linter for template files +# ruff: noqa: F821 E722 + +__TEST__ + + +def run_tests(candidate): + try: + check(candidate) + # We can search for this string in the output + print("ALL TESTS PASSED !#!#\nTERMINATE") + except: + print("SOME TESTS FAILED - TRY AGAIN !#!#") diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/prompt.txt b/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/prompt.txt similarity index 100% rename from samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/prompt.txt rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/prompt.txt diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/scenario.py b/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/scenario.py similarity index 94% rename from samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/scenario.py rename to samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/scenario.py index d47a09458888..2d0815f6aacc 100644 --- a/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/scenario.py +++ b/samples/tools/autogenbench/scenarios/HumanEval/Templates/TwoAgents/scenario.py @@ -20,10 +20,7 @@ PROMPT = fh.read() # Ok, now get autogen to solve it. -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["__MODEL__"]}, -) +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") assistant = autogen.AssistantAgent( "assistant", diff --git a/samples/tools/autogenbench/scenarios/MANIFEST.json b/samples/tools/autogenbench/scenarios/MANIFEST.json new file mode 100644 index 000000000000..d70993ecfe60 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MANIFEST.json @@ -0,0 +1,8 @@ +{ + "scenarios": { + "HumanEval": "HumanEval/", + "GAIA": "GAIA/", + "AutoGPT": "AutoGPT/", + "MATH": "MATH/" + } +} diff --git a/samples/tools/autogenbench/scenarios/MATH/MANIFEST.json b/samples/tools/autogenbench/scenarios/MATH/MANIFEST.json new file mode 100644 index 000000000000..d1985a5dd6f1 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MATH/MANIFEST.json @@ -0,0 +1,11 @@ +{ + "files": { + "README.md": "README.md", + "Scripts/init_tasks.py": "Scripts/init_tasks.py", + "Scripts/custom_tabulate.py": "Scripts/custom_tabulate.py", + "Templates/TwoAgents/prompt.txt": "Templates/TwoAgents/prompt.txt", + "Templates/TwoAgents/expected_answer.txt": "Templates/TwoAgents/expected_answer.txt", + "Templates/TwoAgents/scenario.py": "Templates/TwoAgents/scenario.py", + "Templates/TwoAgents/scenario_init.sh": "Templates/TwoAgents/scenario_init.sh" + } +} diff --git a/samples/tools/autogenbench/scenarios/MATH/README.md b/samples/tools/autogenbench/scenarios/MATH/README.md new file mode 100644 index 000000000000..ac0680351e1d --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MATH/README.md @@ -0,0 +1,19 @@ +# MATH Benchmark + +This scenario implements the [MATH](https://arxiv.org/abs/2103.03874) benchmark. + +## Running the tasks + +``` +autogenbench run Tasks/math_two_agents.jsonl +autogenbench tabulate Results/math_two_agents +``` + +By default, only a small subset (17 of 5000) MATH problems are exposed. Edit `Scripts/init_tasks.py` to expose more tasks. + +*Note*: Scoring is done by prompting the LLM (ideally GPT-4) with both the proposed answer and the ground truth answer, and asking the LLM to grade itself. + +## References +**Measuring Mathematical Problem Solving With the MATH Dataset**
+Dan Hendrycks, Collin Burns, Saurav Kadavath, Akul Arora, Steven Basart, Eric Tang, Dawn Song, Jacob Steinhardt
+[https://arxiv.org/abs/2103.03874](https://arxiv.org/abs/2103.03874) diff --git a/samples/tools/autogenbench/scenarios/MATH/Scripts/custom_tabulate.py b/samples/tools/autogenbench/scenarios/MATH/Scripts/custom_tabulate.py new file mode 100644 index 000000000000..2571145dbffa --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MATH/Scripts/custom_tabulate.py @@ -0,0 +1,26 @@ +import os +import sys +import json +from autogenbench.tabulate_cmd import default_tabulate + + +def scorer(instance_dir): + checker_messages = os.path.join(instance_dir, "checker_messages.json") + if os.path.isfile(checker_messages): + with open(checker_messages, "rt") as fh: + messages = json.loads(fh.read())["checker_proxy"] + results = messages[-1]["content"].lower() + if "the answer is correct" in results or "the answer is approximated but should be correct" in results: + return True + else: + return False + else: + return None + + +def main(args): + default_tabulate(args, scorer=scorer) + + +if __name__ == "__main__" and __package__ is None: + main(sys.argv) diff --git a/samples/tools/autogenbench/scenarios/MATH/Scripts/init_tasks.py b/samples/tools/autogenbench/scenarios/MATH/Scripts/init_tasks.py new file mode 100644 index 000000000000..16545c8e5d04 --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MATH/Scripts/init_tasks.py @@ -0,0 +1,117 @@ +# +# Run this file to download the human_eval dataset, and create a corresponding testbed scenario: +# (default: ../scenarios/human_eval_two_agents_gpt4.jsonl and ./scenarios/human_eval_two_agents_gpt35.jsonl) +# + +import requests +import tarfile +import io +import json +import os +import sys + +URL = "https://people.eecs.berkeley.edu/~hendrycks/MATH.tar" + +SCRIPT_PATH = os.path.realpath(__file__) +SCRIPT_NAME = os.path.basename(SCRIPT_PATH) +SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) + +SCENARIO_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir)) +TEMPLATES_DIR = os.path.join(SCENARIO_DIR, "Templates") +TASKS_DIR = os.path.join(SCENARIO_DIR, "Tasks") +DOWNLOADS_DIR = os.path.join(SCENARIO_DIR, "Downloads") + +SELECTED_PROBLEMS = [ + "MATH/test/algebra/2144.json", + "MATH/test/algebra/1997.json", + "MATH/test/algebra/2072.json", + "MATH/test/algebra/2137.json", + "MATH/test/algebra/2557.json", + "MATH/test/algebra/2045.json", + "MATH/test/algebra/2499.json", + "MATH/test/counting_and_probability/483.json", + "MATH/test/intermediate_algebra/590.json", + "MATH/test/prealgebra/1511.json", + "MATH/test/intermediate_algebra/935.json", + "MATH/test/prealgebra/808.json", + "MATH/test/number_theory/233.json", + "MATH/test/number_theory/960.json", + "MATH/test/precalculus/551.json", + "MATH/test/counting_and_probability/909.json", + "MATH/test/algebra/2417.json", +] + + +def download_math(): + """Download the MATH dataset (if not already downloaded). + Return a JSON dictionary of selected problems.""" + + selected_problems = dict() + + if not os.path.isdir(DOWNLOADS_DIR): + os.mkdir(DOWNLOADS_DIR) + + tar_file = os.path.join(DOWNLOADS_DIR, "MATH.tar") + + if not os.path.isfile(tar_file): + # Send a HTTP request to the URL + response = requests.get(URL, stream=True) + response.raise_for_status() + + # If the HTTP request returns a status code 200, proceed + with open(tar_file, "wb") as fh: + for chunk in response.iter_content(chunk_size=512): + fh.write(chunk) + + # Extract selected problems + tar = tarfile.open(tar_file) + for member in tar.getmembers(): + if member.name in SELECTED_PROBLEMS: + print(f"Extracting: {member.name}") + content = tar.extractfile(member).read() + selected_problems[member.name] = json.loads(content) + + return selected_problems + + +def create_jsonl(name, problems, template): + """Creates a JSONL scenario file with a given name, dictionary of MATH problems, and template path.""" + + # Create a task directory if it doesn't exist + if not os.path.isdir(TASKS_DIR): + os.mkdir(TASKS_DIR) + + # Create the jsonl file + with open(os.path.join(TASKS_DIR, name + ".jsonl"), "wt") as fh: + for item in problems.items(): + data = item[1] + + task_id = item[0].replace("MATH/", "").replace(".json", "").replace("/", "_") + print(f"Converting: [{item[0]}] {task_id}") + + record = { + "id": task_id, + "template": os.path.join(os.path.pardir, template), + "substitutions": { + "prompt.txt": {"__PROMPT__": data["problem"]}, + "expected_answer.txt": {"__ANSWER__": data["solution"]}, + }, + } + + fh.write(json.dumps(record).strip() + "\n") + + +############################################################################### +def main(): + problems = download_math() + + templates = { + "two_agents": "Templates/TwoAgents", + } + + for t in templates.items(): + create_jsonl(f"math_{t[0]}", problems, t[1]) + + +if __name__ == "__main__" and __package__ is None: + main() diff --git a/samples/tools/testbed/scenarios/MATH/answer.txt b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/expected_answer.txt similarity index 100% rename from samples/tools/testbed/scenarios/MATH/answer.txt rename to samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/expected_answer.txt diff --git a/samples/tools/testbed/scenarios/MATH/prompt.txt b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/prompt.txt similarity index 100% rename from samples/tools/testbed/scenarios/MATH/prompt.txt rename to samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/prompt.txt diff --git a/samples/tools/testbed/scenarios/MATH/scenario.py b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario.py similarity index 76% rename from samples/tools/testbed/scenarios/MATH/scenario.py rename to samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario.py index 89cdfad1aee0..b9d92a33528a 100644 --- a/samples/tools/testbed/scenarios/MATH/scenario.py +++ b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario.py @@ -1,44 +1,39 @@ import os import json import autogen - import testbed_utils testbed_utils.init() - PROMPT = "" with open("prompt.txt", "rt") as fh: PROMPT = fh.read() ANSWER = "" -with open("answer.txt", "rt") as fh: +with open("expected_answer.txt", "rt") as fh: ANSWER = fh.read() #################### -config_list = autogen.config_list_from_json( - "OAI_CONFIG_LIST", - filter_dict={"model": ["gpt40613"]}, +config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") +llm_config = testbed_utils.default_llm_config(config_list, timeout=180) + +assistant = autogen.AssistantAgent( + "assistant", + llm_config=llm_config, + is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0, ) -llm_config = { - "cache_seed": 42, - "config_list": config_list, - "timeout": 600, -} -code_execution_config = { - "work_dir": "coding", - "use_docker": False, # set to True or image name like "python:3" to use docker -} -# ---------between "user" and "assistant"--------- -assistant = autogen.AssistantAgent(name="assistant", llm_config=llm_config) + user_proxy = autogen.UserProxyAgent( - name="user", + "user_proxy", human_input_mode="NEVER", - code_execution_config=code_execution_config, + is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0, + code_execution_config={ + "work_dir": "coding", + "use_docker": False, + }, max_consecutive_auto_reply=10, - is_termination_msg=lambda x: x.get("content", "") - and (x.get("content", "").rstrip().endswith("TERMINATE") or x.get("content", "").rstrip().endswith("TERMINATE.")), + default_auto_reply="TERMINATE", ) user_proxy.initiate_chat(assistant, message=PROMPT) @@ -76,10 +71,14 @@ answer_checker = autogen.AssistantAgent(name="checker", llm_config=llm_config, system_message=check_sys_msg) checker_proxy = autogen.UserProxyAgent( - name="checker_proxy", + "checker_proxy", human_input_mode="NEVER", - code_execution_config=code_execution_config, + code_execution_config={ + "work_dir": "coding", + "use_docker": False, + }, max_consecutive_auto_reply=5, + default_auto_reply="TERMINATE", is_termination_msg=lambda x: x.get("content", "").lower() and ( "the answer is correct" in x.get("content", "").lower() diff --git a/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario_init.sh b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario_init.sh new file mode 100644 index 000000000000..d85f27cf0ecd --- /dev/null +++ b/samples/tools/autogenbench/scenarios/MATH/Templates/TwoAgents/scenario_init.sh @@ -0,0 +1 @@ +pip install sympy matplotlib numpy diff --git a/samples/tools/autogenbench/setup.py b/samples/tools/autogenbench/setup.py new file mode 100644 index 000000000000..606849326a40 --- /dev/null +++ b/samples/tools/autogenbench/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/samples/tools/testbed/README.md b/samples/tools/testbed/README.md deleted file mode 100644 index fa4f87404ea3..000000000000 --- a/samples/tools/testbed/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Autogen Testbed Environment - -The Autogen Testbed environment is a tool for repeatedly running a set of pre-defined Autogen scenarios in a setting with tightly-controlled initial conditions. With each run, Autogen will start from a blank slate, working out what code needs to be written, and what libraries or dependencies to install. The results of each run are logged, and can be ingested by analysis or metrics scripts (see the HumanEval example later in this README). By default, all runs are conducted in freshly-initialized docker containers, providing the recommended level of consistency and safety. - -This Testbed sample has been tested in, and is known to work with, Autogen versions 0.1.14 and 0.2.0 - -## Setup - -Before you begin, you must configure your API keys for use with the Testbed. As with other Autogen applications, the Testbed will look for the OpenAI keys in a file in the current working directory, or environment variable named, OAI_CONFIG_LIST. This can be overridden using a command-line parameter described later. - -For some scenarios, additional keys may be required (e.g., keys for the Bing Search API). These can be added to an `ENV` file in the `includes` folder. A sample has been provided in ``includes/ENV.example``. Edit ``includes/ENV`` as needed. - -The Testbed also requires Docker (Desktop or Engine) AND the __python docker__ library. **It will not run in codespaces**, unless you opt for native execution (with is strongly discouraged). To install Docker Desktop see [https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/). To install the Python library: - -``pip install docker`` - -## Running the Testbed - -To run the Testbed, simply execute -``python run_scenarios.py scenarios/Examples`` - -The default is to run each scenario once. To run each scenario 10 times, use: - -``python run_scenarios.py --repeat 10 scenarios/Examples `` - -The run_scenarios.py script also allows a number of command-line arguments to control various parameters of execution. Type ``python run_scenarios.py -h`` to explore these options: - -``` -run_scenarios.py will run the specified autogen scenarios for a given number of repetitions and record all logs and trace information. When running in a Docker environment (default), each run will begin from a common, tightly controlled, environment. The resultant logs can then be further processed by other scripts to produce metrics. - -positional arguments: - scenario The JSONL scenario file to run. If a directory is specified, - then all JSONL scenarios in the directory are run. (default: - ./scenarios) - -options: - -h, --help show this help message and exit - - -r REPEAT, --repeat REPEAT - The number of repetitions to run for each scenario (default: 1). - - -c CONFIG, --config CONFIG - The environment variable name or path to the OAI_CONFIG_LIST (default: OAI_CONFIG_LIST). - - --requirements REQUIREMENTS - The requirements file to pip install before running the scenario. This file must be found in - the 'includes' directory. (default: requirements.txt) - - -d DOCKER_IMAGE, --docker-image DOCKER_IMAGE - The Docker image to use when running scenarios. Can not be used together with --native. - (default: 'autogen/testbed:default', which will be created if not present) - - --native Run the scenarios natively rather than in docker. - NOTE: This is not advisable, and should be done with great caution. -``` - -## Results - -By default, the Testbed stores results in a folder hierarchy with the following template: - -``./results/[scenario]/[instance_id]/[repetition]`` - -For example, consider the following folders: - -``./results/default_two_agents_gpt35/two_agent_stocks/0`` -``./results/default_two_agents_gpt35/two_agent_stocks/1`` - -... - -``./results/default_two_agents_gpt35/two_agent_stocks/9`` - -This folder holds the results for the ``two_agent_stocks`` instance of the ``default_two_agents_gpt35`` scenario. The ``0`` folder contains the results of the first run. The ``1`` folder contains the results of the second run, and so on. You can think of the _instance_ as mapping to a prompt, or a unique set of parameters, while the _scenario_ defines the template in which those parameters are input. - -Within each folder, you will find the following files: - -- *timestamp.txt*: records the date and time of the run, along with the version of the pyautogen library installed -- *console_log.txt*: all console output produced by Docker when running autogen. Read this like you would a regular console. -- *chat_completions.json*: a log of all OpenAI ChatCompletions, as logged by `autogen.ChatCompletion.start_logging(compact=False)` -- *[agent]_messages.json*: for each Agent, a log of their messages dictionaries -- *./coding*: A directory containing all code written by Autogen, and all artifacts produced by that code. - -## Scenario Templating - -All scenarios are stored in JSONL files (in subdirectories under `./scenarios`). Each line of a scenario file is a JSON object. The schema varies slightly based on if "template" specifies a _file_ or a _directory_. - -If "template" points to a _file_, the format is: -``` -{ - "id": string, - "template": filename, - "substitutions" { - "find_string1": replace_string1, - "find_string2": replace_string2, - ... - "find_stringN": replace_stringN - } -} -``` - -For example: - -``` -{ - "id": "two_agent_stocks_gpt4", - "template": "default_two_agents.py", - "substitutions": { - "\__MODEL\__": "gpt-4", - "\__PROMPT\__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." - } -} -``` - - -If "template" points to a _directory_, the format is: - -``` -{ - "id": string, - "template": dirname, - "substitutions" { - "filename1": { - "find_string1_1": replace_string1_1, - "find_string1_2": replace_string1_2, - ... - "find_string1_M": replace_string1_N - } - "filename2": { - "find_string2_1": replace_string2_1, - "find_string2_2": replace_string2_2, - ... - "find_string2_N": replace_string2_N - } - } -} -``` - -For example: - -``` -{ - "id": "two_agent_stocks_gpt4", - "template": "default_two_agents", - "substitutions": { - "scenario.py": { - "\__MODEL\__": "gpt-4", - }, - "prompt.txt": { - "\__PROMPT\__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." - } - } -} -``` - -In this example, the string `__MODEL__` will be replaced in the file `scenarios.py`, while the string `__PROMPT__` will be replaced in the `prompt.txt` file. - - -## Scenario Expansion Algorithm - -When the Testbed runs a scenario, it creates a local folder to share with Docker. As noted above, each instance and repetition gets its own folder along the path: ``./results/[scenario]/[instance_id]/[repetition]`` - -For the sake of brevity we will refer to this folder as the `DEST_FOLDER`. - -The algorithm for populating the `DEST_FOLDER` is as follows: - -1. Recursively copy the contents of `./includes` to DEST_FOLDER. This folder contains all the basic starter files for running a scenario, including an ENV file which will set the Docker environment variables. -2. Append the OAI_CONFIG_LIST to the ENV file so that autogen may access these secrets. -3. Recursively copy the scenario folder (if `template` in the json scenario definition points to a folder) to DEST_FOLDER. If the `template` instead points to a file, copy the file, but rename it to `scenario.py` -4. Apply any templating, as outlined in the prior section. -5. Write a run.sh file to DEST_FOLDER that will be executed by Docker when it is loaded. - - -## Scenario Execution Algorithm - -Once the scenario has been expanded it is run (via run.sh). This script will execute the following steps: - -1. Read and set the ENV environment variables -2. If a file named `global_init.sh` is present, run it. -3. If a file named `scenario_init.sh` is present, run it. -4. Install the requirements file (if running in Docker) -5. Run the Autogen scenario via `python scenario.py` -6. Clean up (delete cache, etc.) -7. If a file named `scenario_finalize.sh` is present, run it. -8. If a file named `global_finalize.sh` is present, run it. -9. echo "SCENARIO COMPLETE !#!#", signaling that all steps completed. - -Notably, this means that scenarios can add custom init and teardown logic by including `scenario_init.sh` and `scenario_finalize.sh` files. - - -## (Example) Running HumanEval - -One sample Testbed scenario type is a variation of the classic [HumanEval](https://github.com/openai/human-eval) benchmark. In this scenario, agents are given access to the unit test results, and are able to continue to debug their code until the problem is solved or they run out of tokens or turns. We can then count how many turns it took to solve the problem (returning -1 if the problem remains unsolved by the end of the conversation, and "" if the run is missing). - -Accessing this scenario-type requires downloading and converting the HumanEval dataset, running the Testbed, collating the results, and finally computing the metrics. The following commands will accomplish this, running each test instance 3 times with GPT-3.5-Turbo-16k: - -``` -python utils/download_humaneval.py -python ./run_scenarios.py scenarios/HumanEval/human_eval_two_agents_gpt35.jsonl -python utils/collate_human_eval.py ./results/human_eval_two_agents_gpt35 | python utils/metrics_human_eval.py > human_eval_results_gpt35.csv -cat human_eval_results_gpt35.csv -``` - -## (Example) Running GAIA - -The Testbed can also be used to run the recently released [GAIA benchmark](https://huggingface.co/gaia-benchmark). This integration is presently experimental, and needs further validation. In this scenario, agents are presented with a series of questions that may include file references, or multi-modal input. Agents then must provide a `FINAL ANSWER`, which is considered correct if it (nearly) exactly matches an unambiguously accepted answer. - -Accessing this scenario-type requires downloading and converting the GAIA dataset, running the Testbed, collating the results, and finally computing the metrics. The following commands will accomplish this, running each test instance once with GPT-4: - -``` -# Clone the GAIA dataset repo (assuming a 'repos' folder in your home directory) -cd ~/repos -git clone https://huggingface.co/datasets/gaia-benchmark/GAIA - -# Expand GAIA -cd ~/repos/autogen/samples/tools/testbed -python ./utils/expand_gaia.py ~/repos/GAIA - -# Run GAIA -python ./run_scenarios.py ./scenarios/GAIA/gaia_validation_level_1__two_agents_gpt4.jsonl - -# Compute Metrics -python utils/collate_gaia_csv.py ./results/gaia_validation_level_1__two_agents_gpt4 | python utils/metrics_gaia.py -``` - -## (Example) Running tasks from AutoGPT - -The Testbed supports running tasks proposed in [AutoGPT benchmark](https://github.com/Significant-Gravitas/AutoGPT/tree/master/benchmark/agbenchmark/challenges). In this scenario, the agents are prompted to handle a diverse range of tasks, including coding, question answering according to given tasks, web scraping. Similar to scenarios in HumanEval, the agents can call the unit test script to check if the task is successfully done. - -Accessing this scenario-type requires converting tasks, running the Testbed, collating the results, and finally computing the metrics. The following commands will run each test instance with GPT-4: - -``` -# Convert tasks -python utils/prepare_autogpt.py - -# Run all the scenarios with GPT-4 -python run_scenarios.py scenarios/AutoGPT/autogpt_twoagent_gpt4.jsonl - -# Compute metrics, the metric script shares the same one with HumanEval -python utils/collate_autogpt.py ./results/autogpt_twoagent_gpt4 | python metrics_human_eval.py -``` diff --git a/samples/tools/testbed/includes/ENV.example b/samples/tools/testbed/includes/ENV.example deleted file mode 100644 index b1f190647d05..000000000000 --- a/samples/tools/testbed/includes/ENV.example +++ /dev/null @@ -1 +0,0 @@ -export BING_API_KEY= diff --git a/samples/tools/testbed/includes/math_requirements.txt b/samples/tools/testbed/includes/math_requirements.txt deleted file mode 100644 index 0600c8ce047a..000000000000 --- a/samples/tools/testbed/includes/math_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -git+https://github.com/microsoft/autogen.git -sympy -matplotlib -numpy diff --git a/samples/tools/testbed/includes/requirements.txt b/samples/tools/testbed/includes/requirements.txt deleted file mode 100644 index 33070268d1f7..000000000000 --- a/samples/tools/testbed/includes/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -git+https://github.com/microsoft/autogen.git -pandas -beautifulsoup4 -requests -pytest diff --git a/samples/tools/testbed/run_scenarios.py b/samples/tools/testbed/run_scenarios.py deleted file mode 100644 index 88547bab8b57..000000000000 --- a/samples/tools/testbed/run_scenarios.py +++ /dev/null @@ -1,474 +0,0 @@ -import os -import errno -import shutil -import subprocess -import json -import sys -import time -import pathlib -import argparse -from autogen import config_list_from_json - -# What platform are we running? -IS_WIN32 = sys.platform == "win32" - -# Location of the global includes dir. The contents of this directory will be copied to the Docker environment. -GLOBAL_INCLUDES_DIR = "includes" - -# This is the tag given to the image that is *built* when no other image is provided. -# Do not use this field to specify the name of an existing image (e.g., on Dockerhub) -DEFAULT_DOCKER_IMAGE_TAG = "autogen/testbed:default" - - -def run_scenarios(scenario, n_repeats, is_native, config_list, requirements, docker_image=None, results_dir="results"): - """ - Run a set testbed scenarios a given number of times. - - Args: - scenario (path): The file or folder containing the scenario JSONL instances. If given a folder, then - all JSONL files in the folder will be loaded and run. - n_repeats (int): The number of times each scenario instance will be repeated - is_native (bool): True if the scenario should be run locally rather than in Docker (proceed with caution!) - config_list (list): An Autogen OAI_CONFIG_LIST to be used when running scenarios. - results_dir (path): The folder were results will be saved. - """ - - files = [] - - # Figure out which files or folders we are working with - if os.path.isfile(scenario): - files.append(scenario) - elif os.path.isdir(scenario): - for f in os.listdir(scenario): - scenario_file = os.path.join(scenario, f) - - if not os.path.isfile(scenario_file): - continue - - if not scenario_file.lower().endswith(".jsonl"): - continue - - files.append(scenario_file) - else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), scenario) - - # Run all the scenario files - for scenario_file in files: - scenario_name = os.path.basename(scenario_file).split(".") - scenario_name.pop() - scenario_name = ".".join(scenario_name) - - scenario_dir = os.path.dirname(os.path.realpath(scenario_file)) - - # Each line in the scenario file is an instance. Run it. - with open(scenario_file) as fh: - for line in fh: - instance = json.loads(line) - - # Create a folder to store the results - # Results base - if not os.path.isdir(results_dir): - os.mkdir(results_dir) - - # Results for the scenario - results_scenario = os.path.join(results_dir, scenario_name) - if not os.path.isdir(results_scenario): - os.mkdir(results_scenario) - - # Results for the instance - results_instance = os.path.join(results_scenario, instance["id"]) - if not os.path.isdir(results_instance): - os.mkdir(results_instance) - - # Results for the repeats - for i in range(0, n_repeats): - results_repetition = os.path.join(results_instance, str(i)) - - # Skip it if it already exists - if os.path.isdir(results_repetition): - print(f"Found folder {results_repetition} ... Skipping.") - continue - print(f"Running scenario {results_repetition}") - - # Expand the scenario - expand_scenario(scenario_dir, instance, results_repetition) - - # Append the config list to the ENV file - with open(os.path.join(results_repetition, "ENV"), "at") as fh: - config_list_json = json.dumps(config_list) - fh.write(f"export OAI_CONFIG_LIST='{config_list_json}'\n") - - # If set, append the OpenAI API Key - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key is not None and len(openai_api_key.strip()) > 0: - fh.write(f"export OPENAI_API_KEY='{openai_api_key}'\n") - - # Run the scenario - if is_native: - run_scenario_natively(results_repetition) - else: - run_scenario_in_docker(results_repetition, requirements, docker_image=docker_image) - - -def expand_scenario(scenario_dir, scenario, output_dir): - """ - Expand a scenario into a folder. - Despite some awkwardness created by backwards compatibility and notational conveniences, expansion is conceptually simple. - It is a series of copy commands (similar to `cp -R`), followed by a series of in-place fine and replace operations. - """ - - template = scenario["template"] - - # Either key works for finding the substitutions list. "values" may be deprecated in the future - substitutions = scenario["substitutions"] if "substitutions" in scenario else scenario["values"] - - # Older versions are only one-level deep. Convert them, - if len(substitutions) > 0 and isinstance(substitutions[next(iter(substitutions))], str): - substitutions = {"scenario.py": substitutions} - - copy_operations = [] - - # Handle file (str), folder (str), or mapping (List) templates - if isinstance(template, str): - template_path = os.path.join(scenario_dir, template) - if os.path.isdir(template_path): - copy_operations.append((template, "")) - else: - copy_operations.append((template, "scenario.py")) - elif isinstance(template, list): - for elm in template: - if isinstance(elm, list): - copy_operations.append((elm[0], elm[1])) - else: - copy_operations.append((elm, "")) - else: - raise ValueError("expand_scenario expects an str or list for 'template'") - - # The global includes folder is always copied - shutil.copytree(GLOBAL_INCLUDES_DIR, output_dir, ignore=shutil.ignore_patterns("*.example"), dirs_exist_ok=False) - - # Expand other folders - for items in copy_operations: - src_path = pathlib.Path(os.path.join(scenario_dir, items[0])).absolute() - dest_path = pathlib.Path(os.path.join(output_dir, items[1])).absolute() - - if os.path.isdir(src_path): - shutil.copytree(src_path, dest_path, dirs_exist_ok=True) - else: - if os.path.isdir(dest_path): - # If the destination is a directory, use the same filename - shutil.copyfile(src_path, os.path.join(dest_path, os.path.basename(src_path))) - else: - # Otherwise use the filename provided - shutil.copyfile(src_path, dest_path) - - # Expand templated files - for templated_file in substitutions.keys(): # Keys are relative file paths - # Read the templated file into memory - template_contents = list() - with open(os.path.join(output_dir, templated_file), "rt") as fh: - for line in fh: - template_contents.append(line) - - # Rewrite the templated file with substitutions - values = substitutions[templated_file] - with open(os.path.join(output_dir, templated_file), "wt") as fh: - for line in template_contents: - for k, v in values.items(): - line = line.replace(k, v) - fh.write(line) - - -def run_scenario_natively(work_dir): - """ - Run a scenario in the native environment. - - Args: - work_dir (path): the path to the working directory previously created to house this scenario instance - """ - - # Get the current working directory - cwd = os.getcwd() - - # Navigate to the scenario - os.chdir(work_dir) - print("\n\n" + os.getcwd() + "\n===================================================================") - - # Prepare the run script - with open(os.path.join("run.sh"), "wt") as f: - f.write( - """# -export AUTOGEN_TESTBED_SETTING="Native" - -# Read the environment variables -. ./ENV - -# Run the global init script if it exists -if [ -f global_init.sh ] ; then - . ./global_init.sh -fi - -# Run the scenario init script if it exists -if [ -f scenario_init.sh ] ; then - . ./scenario_init.sh -fi - -# Run the scenario -python scenario.py - -# Clean up -rm ENV -if [ -d .cache ] ; then - rm -Rf .cache -fi - -# Run the scenario finalize script if it exists -if [ -f scenario_finalize.sh ] ; then - . ./scenario_finalize.sh -fi - -# Run the global finalize script if it exists -if [ -f global_finalize.sh ] ; then - . ./global_finalize.sh -fi - -echo SCENARIO COMPLETE !#!# -""" - ) - - # Run the script and log the output - with open("console_log.txt", "wb") as f: - process = subprocess.Popen(["sh", "run.sh"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - for c in iter(lambda: process.stdout.read(1), b""): - f.write(c) - os.write(sys.stdout.fileno(), c) # Write binary to stdout - - # Return where we started - os.chdir(cwd) - return - - -def run_scenario_in_docker(work_dir, requirements, timeout=600, docker_image=None): - """ - Run a scenario in a Docker environment. - - Args: - work_dir (path): the path to the working directory previously created to house this scenario instance - timeout (Optional, int): the number of seconds to allow a Docker container to run before timing out - """ - - client = docker.from_env() - image = None - - # If the docker_image is None, then we will fetch DEFAULT_DOCKER_IMAGE_TAG, if present, - # or build it if missing. - if docker_image is None: - # Pull a suitable image - try: - image = client.images.get(DEFAULT_DOCKER_IMAGE_TAG) - except docker.errors.ImageNotFound: - print(f"Building default Docker image '{DEFAULT_DOCKER_IMAGE_TAG}'. This may take a few minutes...") - try: - build_default_docker_image(client, DEFAULT_DOCKER_IMAGE_TAG) - image = client.images.get(DEFAULT_DOCKER_IMAGE_TAG) - except docker.errors.DockerException: - print(f"Failed to build image '{DEFAULT_DOCKER_IMAGE_TAG}'") - - # Otherwise get the requested image - else: - try: - image = client.images.get(docker_image) - except docker.errors.ImageNotFound: - # pull the image - print(f"Pulling image '{docker_image}'") - try: - image = client.images.pull(docker_image) - except docker.errors.DockerException: - print(f"Failed to pull image '{docker_image}'") - - # Prepare the run script - with open(os.path.join(work_dir, "run.sh"), "wt", newline="\n") as f: - f.write( - f"""# -export AUTOGEN_TESTBED_SETTING="Docker" -umask 000 - -# Read the environment variables -. ./ENV - -# Run the global init script if it exists -if [ -f global_init.sh ] ; then - . ./global_init.sh -fi - -# Run the scenario init script if it exists -if [ -f scenario_init.sh ] ; then - . ./scenario_init.sh -fi - -# Run the scenario -pip install -r {requirements} -python scenario.py - -# Clean up -rm ENV -if [ -d .cache ] ; then - rm -Rf .cache -fi - -# Run the scenario finalize script if it exists -if [ -f scenario_finalize.sh ] ; then - . ./scenario_finalize.sh -fi - -# Run the global finalize script if it exists -if [ -f global_finalize.sh ] ; then - . ./global_finalize.sh -fi - -echo SCENARIO COMPLETE !#!# -""" - ) - - print("\n\n" + work_dir + "\n===================================================================") - - # Create and run the container - abs_path = str(pathlib.Path(work_dir).absolute()) - container = client.containers.run( - image, - command=["sh", "run.sh"], - working_dir="/workspace", - detach=True, - # get absolute path to the working directory - volumes={abs_path: {"bind": "/workspace", "mode": "rw"}}, - ) - - # Poll until the container is done, or we've timed out - start_time = time.time() - while container.status != "exited" and time.time() - start_time < timeout: - # Reload the container object - container.reload() - - if container.status != "exited": - container.stop() - - logs = container.logs().decode("utf-8").rstrip() + "\nDocker timed out.\n" - print(logs) - with open(os.path.join(work_dir, "console_log.txt"), "wt") as f: - f.write(logs) - - container.remove() - return - - # get the container logs - logs = container.logs().decode("utf-8").rstrip() + "\n" - container.remove() - - print(logs) - with open(os.path.join(work_dir, "console_log.txt"), "wt") as f: - f.write(logs) - - -def build_default_docker_image(docker_client, image_tag): - for segment in docker_client.api.build(path=".", dockerfile="Dockerfile", rm=True, tag=image_tag, decode=True): - if "stream" in segment: - sys.stdout.write(segment["stream"]) - - -############################################################################### -if __name__ == "__main__": - script_name = os.path.basename(__file__) - parser = argparse.ArgumentParser( - description=f"{script_name} will run the specified autogen scenarios for a given number of repetitions and record all logs and trace information. When running in a Docker environment (default), each run will begin from a common, tightly controlled, environment. The resultant logs can then be further processed by other scripts to produce metrics.".strip() - ) - - parser.add_argument( - "scenario", - nargs="?", - help="The JSONL scenario file to run. If a directory is specified, then all JSONL scenarios in the directory are run. (default: ./scenarios)", - default="scenarios", - ) - parser.add_argument( - "-c", - "--config", - type=str, - help="The environment variable name or path to the OAI_CONFIG_LIST (default: OAI_CONFIG_LIST).", - default="OAI_CONFIG_LIST", - ) - parser.add_argument( - "-r", "--repeat", type=int, help="The number of repetitions to run for each scenario (default: 1).", default=1 - ) - parser.add_argument( - "--requirements", - type=str, - help="The requirements file to pip install before running the scenario. This file must be found in the '" - + GLOBAL_INCLUDES_DIR - + "' directory. (default: requirements.txt)", - default=None, - ) - parser.add_argument( - "-d", - "--docker-image", - type=str, - help="The Docker image to use when running scenarios. Can not be used together with --native. (default: '" - + DEFAULT_DOCKER_IMAGE_TAG - + "', which will be created if not present)", - default=None, - ) - parser.add_argument( - "--native", - action="store_true", - help="Run the scenarios natively rather than in docker. NOTE: This is not advisable, and should be done with great caution.", - ) - - args = parser.parse_args() - - # Load the OAI_CONFIG_LIST - config_list = config_list_from_json(env_or_file=args.config) - if len(config_list) == 0: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), args.config) - - # Don't allow both --docker-image and --native on the same command - if args.docker_image is not None and args.native: - sys.exit("The options --native and --docker-image can not be used together. Exiting.") - - # Warn if running natively - if args.native: - if IS_WIN32: - sys.exit("Running scenarios with --native is not supported in Windows. Exiting.") - - if args.requirements is not None: - sys.exit("--requirements is not compatible with --native. Exiting.") - - choice = input( - 'WARNING: Running natively, without Docker, not only poses the usual risks of executing arbitrary AI generated code on your machine, it also makes it impossible to ensure that each test starts from a known and consistent set of initial conditions. For example, if the agents spend time debugging and installing Python libraries to solve the task, then those libraries will be available to all other runs. In other words, earlier runs can influence later runs, leading to many confounds in testing.\n\nAre you absolutely sure you want to continue with native execution? Type "Yes" exactly, and in full, to proceed: ' - ) - - if choice.strip().lower() != "yes": - sys.exit("Received '" + choice + "'. Exiting.") - - # What requirements file are we working with? - requirements = "requirements.txt" - if args.requirements is not None: - requirements = args.requirements - - is_native = True if args.native else False - if not is_native: - # Import docker - import docker - - # Make sure the requirements file exists - req_file = os.path.join(GLOBAL_INCLUDES_DIR, requirements) - if not os.path.isfile(req_file): - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), req_file) - - # Warn about a common error - env_file = os.path.join(GLOBAL_INCLUDES_DIR, "ENV") - example_file = os.path.join(GLOBAL_INCLUDES_DIR, "ENV.example") - if not os.path.isfile(env_file): - shutil.copyfile(example_file, env_file) - sys.stderr.write( - f"The environment file '{env_file}' does not exist (perhaps this is your first time setting up the testbed). A default environment file has been provided, but you may want to edit it to include your API keys and configurations.\n" - ) - - run_scenarios(args.scenario, args.repeat, is_native, config_list, requirements, docker_image=args.docker_image) diff --git a/samples/tools/testbed/scenarios/AutoGPT/README.md b/samples/tools/testbed/scenarios/AutoGPT/README.md deleted file mode 100644 index db08a0af4844..000000000000 --- a/samples/tools/testbed/scenarios/AutoGPT/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The AutoGPT style tasks are contained in folder `challenges`. - -Run `python ../../utils/prepare_autogpt.py` to convert the tasks to jsonl format compatible for evaluation. diff --git a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/scenario.py b/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/scenario.py deleted file mode 100644 index 1b71eb783914..000000000000 --- a/samples/tools/testbed/scenarios/AutoGPT/Templates/TwoAgents/scenario.py +++ /dev/null @@ -1,55 +0,0 @@ -from autogen import AssistantAgent, UserProxyAgent, config_list_from_json -import testbed_utils - -# Assistant agent can call check.py to check if all the unit tests have passed -testbed_utils.init() - -work_dir = "coding" -target_folder = "__TARGET_FOLDER__" # path to the artifact folder - -config_list = config_list_from_json("OAI_CONFIG_LIST", filter_dict={"model": ["__MODEL__"]}) - -assistant = AssistantAgent( - "assistant", - is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, - llm_config={ - "config_list": config_list, - }, -) -user_proxy = UserProxyAgent( - "user_proxy", - human_input_mode="NEVER", - is_termination_msg=lambda x: x.get("content", "").rstrip().find("TERMINATE") >= 0, - code_execution_config={ - "work_dir": work_dir, - "use_docker": False, - }, - max_consecutive_auto_reply=5, - # default_auto_reply="TERMINATE", -) - -if target_folder: - # The tasks involves reading from a file then do sth to it. - message = """ - Here is the task description: __TASK__ The file you needed is located in this directory: '__TARGET_FOLDER__'. You should save the output files in the current directory: './' - Run the following command to check if all the unit tests have passed: - ```bash - python ../check.py - ``` - You should refine the code and results until all the tests have passed. - """ -else: - message = """ - Here is the task description: __TASK__ - Run the following command to check if all the unit tests have passed: - ```bash - python ../check.py - ``` - You should refine the code and results until all the tests have passed. - """ -user_proxy.initiate_chat( - assistant, - message=message, -) - -testbed_utils.finalize(agents=[assistant, user_proxy]) diff --git a/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt35.jsonl b/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt35.jsonl deleted file mode 100644 index 9dc14578f0dc..000000000000 --- a/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt35.jsonl +++ /dev/null @@ -1 +0,0 @@ -{ "id": "nvda_tsla_stocks", "template": "Templates/ThreeAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-3.5-turbo-16k", "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD.", "__SELECTION_METHOD__": "auto", "__3RD_AGENT_NAME__": "visualization_critic", "__3RD_AGENT_PROMPT__": "A student of Edward Tufte, you are an expert in information design, and will provide helpful critiques of visualizations. As you prepare your critiques, please consider the following dimensions:\n- Are there bugs, logic errors, syntax error or typos in the visualization code? Are there any reasons why the code may fail to run? How should it be fixed?\n- Is the data transformed appropriately for the visualization type? E.g., is the dataset appropriated filtered, aggregated, or grouped if needed? If a date field is used, is the date field first converted to a date object etc?\n- How well does the code meet the specified visualization goals?\n- CONSIDERING BEST PRACTICES, is the visualization type appropriate for the data and intent? Is there a visualization type that would be more effective in conveying insights? \n- Are the aesthetics of the visualization appropriate for the visualization type and the data?" } } } diff --git a/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt4.jsonl b/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt4.jsonl deleted file mode 100644 index 8b1f5b717e62..000000000000 --- a/samples/tools/testbed/scenarios/Examples/default_three_agents_gpt4.jsonl +++ /dev/null @@ -1 +0,0 @@ -{ "id": "nvda_tsla_stocks", "template": "Templates/ThreeAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-4", "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD.", "__SELECTION_METHOD__": "auto", "__3RD_AGENT_NAME__": "visualization_critic", "__3RD_AGENT_PROMPT__": "A student of Edward Tufte, you are an expert in information design, and will provide helpful critiques of visualizations. As you prepare your critiques, please consider the following dimensions:\n- Are there bugs, logic errors, syntax error or typos in the visualization code? Are there any reasons why the code may fail to run? How should it be fixed?\n- Is the data transformed appropriately for the visualization type? E.g., is the dataset appropriated filtered, aggregated, or grouped if needed? If a date field is used, is the date field first converted to a date object etc?\n- How well does the code meet the specified visualization goals?\n- CONSIDERING BEST PRACTICES, is the visualization type appropriate for the data and intent? Is there a visualization type that would be more effective in conveying insights? \n- Are the aesthetics of the visualization appropriate for the visualization type and the data?" } } } diff --git a/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt35.jsonl b/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt35.jsonl deleted file mode 100644 index e67f6b40121a..000000000000 --- a/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt35.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{ "id": "nvda_tsla_stocks", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-3.5-turbo-16k", "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." } } } -{ "id": "arxiv_search", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-3.5-turbo-16k", "__PROMPT__": "Find 10 papers on explainable or interpretable AI that were submitted to arXiv within the last year. When printing results, include paper titles, authors, dates, and URLs, but not their abstracts." } } } -{ "id": "old_mslogo_search", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-3.5-turbo-16k", "__PROMPT__": "Find Microsoft's logo from 1983, and save it to disk. If searching the web, use Bing with API key stored in os.environ['BING_API_KEY']" } } } diff --git a/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt4.jsonl b/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt4.jsonl deleted file mode 100644 index 3bb941d92ab9..000000000000 --- a/samples/tools/testbed/scenarios/Examples/default_two_agents_gpt4.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{ "id": "nvda_tsla_stocks", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-4", "__PROMPT__": "Plot and save to disk a chart of NVDA and TESLA stock price YTD." } } } -{ "id": "arxiv_search", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-4", "__PROMPT__": "Find 10 papers on explainable or interpretable AI that were submitted to arXiv within the last year. When printing results, include paper titles, authors, dates, and URLs, but not their abstracts." } } } -{ "id": "old_mslogo_search", "template": "Templates/TwoAgents", "substitutions": { "scenario.py": { "__MODEL__": "gpt-4", "__PROMPT__": "Find Microsoft's logo from 1983, and save it to disk. If searching the web, use Bing with API key stored in os.environ['BING_API_KEY']" } } } diff --git a/samples/tools/testbed/scenarios/HumanEval/README.md b/samples/tools/testbed/scenarios/HumanEval/README.md deleted file mode 100644 index b0748865807a..000000000000 --- a/samples/tools/testbed/scenarios/HumanEval/README.md +++ /dev/null @@ -1 +0,0 @@ -Run `python ../../utils/download_humaneval.py` to populate this folder. diff --git a/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py b/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py deleted file mode 100644 index 951a40831111..000000000000 --- a/samples/tools/testbed/scenarios/HumanEval/Templates/TwoAgents/coding/my_tests.py +++ /dev/null @@ -1,10 +0,0 @@ -# Disable ruff linter for template files -# ruff: noqa: F821 - -__TEST__ - - -def run_tests(candidate): - check(candidate) - # We can search for this string in the output - print("ALL TESTS PASSED !#!#\nTERMINATE") diff --git a/samples/tools/testbed/scenarios/MATH/README.md b/samples/tools/testbed/scenarios/MATH/README.md deleted file mode 100644 index 7fea2cd0f4bd..000000000000 --- a/samples/tools/testbed/scenarios/MATH/README.md +++ /dev/null @@ -1,27 +0,0 @@ -## Get json file to run - -This will convert the math problems to json format and put it in the `scenarios/MATH` folder. -```sh -cd samples/tools/testbed/ -python scenarios/MATH/problems_to_json.py -``` - -## Run the testbed - -Note: this will first run autogen on the math problems, and then use a LLM as answer checker to check the answers. -This means the results is not 100% accurate. - -```sh -python run_scenarios.py scenarios/MATH/problems.jsonl -c --requirements math_requirements.txt -``` - -## Get the correct count -Use `--path` or `-p` to specify the path to the problem directory, the default is `./results/problems/`, which is the default save path of this testbed. -```sh -python scenarios/MATH/count_correct_math.py --path -``` - -Example output: -``` -Trial 0 | Total Correct: 10 | Total Problems: 17 -``` diff --git a/samples/tools/testbed/scenarios/MATH/count_correct_math.py b/samples/tools/testbed/scenarios/MATH/count_correct_math.py deleted file mode 100644 index 69766dfb0c5d..000000000000 --- a/samples/tools/testbed/scenarios/MATH/count_correct_math.py +++ /dev/null @@ -1,56 +0,0 @@ -import argparse -import json -import os - - -def main(args): - stars = "*" * 100 - - # initiate the correct count for each trial - correct_count = [0 for i in range(args.num_trials)] - - for i in range(args.num_trials): - for problem_name in os.listdir(args.path): - problem_path = os.path.join(args.path, problem_name, str(i)) - if os.path.isdir(problem_path): - checker_file_path = os.path.join(problem_path, "checker_messages.json") - - with open(checker_file_path, "r") as file: - checker_messages = json.load(file) - - check_result = checker_messages["checker_proxy"][-1]["content"].lower() - - if ( - "the answer is correct" in check_result - or "the answer is approximated but should be correct" in check_result - ): - correct_count[i] += 1 - # print(f"{problem_name} | Correct") - # else: - # print(f"{problem_name} | Wrong") - - print(f"{stars}\nTrial {i} | Total Correct: {correct_count[i]} | Total Problems: {len(os.listdir(args.path))}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="""Print Math Problems results.""".strip(), - ) - parser.add_argument( - "--path", - "-p", - type=str, - default="./results/problems/", - help="Path to the problems directory", - ) - # num trials - parser.add_argument( - "--num_trials", - "-n", - type=int, - default=1, - help="Number of trials to check", - ) - - args = parser.parse_args() - main(args) diff --git a/samples/tools/testbed/scenarios/MATH/problems_to_json.py b/samples/tools/testbed/scenarios/MATH/problems_to_json.py deleted file mode 100644 index 4dd9dba0d12f..000000000000 --- a/samples/tools/testbed/scenarios/MATH/problems_to_json.py +++ /dev/null @@ -1,77 +0,0 @@ -import json - -problems = [ - "Find all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.", - "Find the value of $a_2+a_4+a_6+a_8+\\dots+a_{98}$ if $a_1, a_2, a_3, \\ldots$ is an arithmetic progression with common difference $1$ and \\[a_1+a_2+a_3+\\dots+a_{98}=137.\\]", - "Tina the tourist goes on a trip. She starts at the origin and drives north (in the positive $y$ direction) for $10$ units. Then she turns east (the positive $x$ direction) and as she's turning her camera flies out the window and lands exactly at $(0,10)$. She then drives $9$ units east, turns and drives $8$ units north. She continues this pattern of turning and driving one unit less than after the previous turn, until stopping after driving $1$ unit east. She reaches for her camera only to find it missing! She activates the GPS homing device on her camera and drives back to it in a straight line. What is the equation of this line? Express your answer as $ax+by=c$, where $a$, $b$, and $c$ are integers, $a>0$, and $a$ is as small as possible.", - "For what negative value of $k$ is there exactly one solution to the system of equations \\begin{align*}\ny &= 2x^2 + kx + 6 \\\\\ny &= -x + 4?\n\\end{align*}", - "If $\\frac{3x^2-4x+1}{x-1}=m$, and $x$ can be any real number except $1$, what real values can $m$ NOT have?", - "Find all numbers $a$ for which the graph of $y=x^2+a$ and the graph of $y=ax$ intersect. Express your answer in interval notation.", - "If $\\displaystyle{f(x)=x^{(x+1)}(x+2)^{(x+3)}}$, then find the value of $f(0)+f(-1)+f(-2)+f(-3)$.", - "An envelope contains eight bills: 2 ones, 2 fives, 2 tens, and 2 twenties. Two bills are drawn at random without replacement. What is the probability that their sum is $\\$20$ or more?", - "Find the coefficient of $x^2$ in the expansion of the product $$(1-x)(1+2x)(1-3x)\\dotsm(1+14x)(1-15x).$$", - "All 50 states as well as the District of Columbia and Puerto Rico, have distinct two-letter postal abbreviations. If a two-letter sequence of letters (such as CO or EE) is chosen at random, what is the probability that it is a postal abbreviation for one of the 50 states, the District of Columbia, or Puerto Rico? Express your answer as a common fraction.", - "Let $x$ and $y$ be real numbers. Find the set of possible values of\n\\[\\frac{(x + y)(1 - xy)}{(1 + x^2)(1 + y^2)}.\\]", - "On a number line, the coordinates of $P$ and $Q$ are 8 and 48, respectively. The midpoint of $\\overline{PQ}$ is $B$, the midpoint of $\\overline{BQ}$ is $C$, and the midpoint of $\\overline{PC}$ is $D$. What is the coordinate of $D$?", - "Find $24^{-1} \\pmod{11^2}$. That is, find the residue $b$ for which $24b \\equiv 1\\pmod{11^2}$.\n\nExpress your answer as an integer from $0$ to $11^2-1$, inclusive.", - "There are two cameras that take pictures of a traffic intersection. Camera A starts taking pictures at $6$ AM and takes a picture every $11$ minutes. Camera B starts taking pictures at $7$ AM and takes pictures every $7$ minutes. Camera A and Camera B take a picture at the same time at four different times before noon. When Camera A and Camera B take their last picture together, how many minutes before noon is it?", - "Let $z$ be a complex number such that $z^{13} = 1.$ Let $w_1,$ $w_2,$ $\\dots,$ $w_k$ be all the possible values of\n\\[z + z^3 + z^4 + z^9 + z^{10} + z^{12}.\\]Find $w_1^2 + w_2^2 + \\dots + w_k^2.$", - "There are 190 people on the beach. 110 are wearing sunglasses, 70 are wearing bathing suits, and 95 are wearing a hat. Everyone is wearing at least one of these items. 30 are wearing both bathing suits and sunglasses. 25 are wearing both bathing suits and a hat. 40 are wearing both sunglasses and a hat. How many people are wearing all three items?", - "Completely simplify and rationalize the denominator: $$\\frac{\\sqrt{160}}{\\sqrt{252}}\\times\\frac{\\sqrt{245}}{\\sqrt{108}}$$", -] -answers = [ - # 6 algebra - "(-\\infty, -14)\\cup(-3,\\infty)", - "93", - "4x-5y=-50", - "-5", - "2", - "(-\\infty,0]\\cup[4,\\infty)", - # 11 problems, 2 from each category, (1 algebra is deleted) - "\\frac{10}{9}", - "\\frac{1}{2}", - "-588", - " \\frac{1}{13}", - "\\left[ -\\frac{1}{2}, \\frac{1}{2} \\right]", - "23", - "116", - "41", - "43", - "10", - "\\frac{5\\sqrt{42}}{27}", -] - - -def problem_to_json(): - with open("problems.jsonl", "w") as f: - for i, problem in enumerate(problems): - # a = { - # 'id': problem{i}', - # 'template': 'scenario.py', - # 'substitutions': { - # '__PROMPT__': problem, - # '__ANSWER__': answers[i], - # }, - # } - a = { - "id": f"problem{i}", - "template": "./", - "substitutions": {"prompt.txt": {"__PROMPT__": problem}, "answer.txt": {"__ANSWER__": answers[i]}}, - } - # Convert the dictionary to a JSON string and write it to the file - json_string = json.dumps(a) - f.write(json_string + "\n") # Add a newline character after each JSON object - - -problem_to_json() - -problems = [] -with open("problems.jsonl", "r") as file: - for line in file: - # Parse each line as a JSON object - problem = json.loads(line) - problems.append(problem) - print(problem["substitutions"]) - print() - -# Now 'problems' is a list of dictionaries, each representing a problem diff --git a/samples/tools/testbed/utils/collate_autogpt.py b/samples/tools/testbed/utils/collate_autogpt.py deleted file mode 100644 index 3dc8bcdba59d..000000000000 --- a/samples/tools/testbed/utils/collate_autogpt.py +++ /dev/null @@ -1,108 +0,0 @@ -import argparse -import os -import re -import subprocess -import sys - - -def collate(results_dir="results"): - """ - Collate the results of running AutoGPT test. - - Args: - results_dir (str, optional): The folder where results are saved. Defaults to "results". - """ - - all_results = list() - max_instances = 0 - - for test_name in os.listdir(results_dir): - test_path = os.path.join(results_dir, test_name) - - # Collect the results vector - results = [test_name] - - instance = 0 - instance_dir = os.path.join(test_path, str(instance)) - while os.path.isdir(instance_dir): - console_log = os.path.join(instance_dir, "console_log.txt") - if os.path.isfile(console_log): - with open(console_log, "rt") as fh: - content = fh.read() - if "ALL TESTS PASSED!" in content: - # Ideally we would have a more distinctive pattern. - results.append(str(len(re.findall(r"\n(.*?) \(to (.*?)\)\:\n", content)))) - else: - # Sometimes the task actually succeeds, but the check.py isn't properly called - result = subprocess.run( - [sys.executable, "../check.py"], - cwd=os.path.join(instance_dir, "coding"), - capture_output=True, - text=True, - ) - if "error" in result.stderr or result.returncode != 0: - results.append("-1") - else: - # The task actually succeeds. - if "ALL TESTS PASSED!" in result.stdout: - results.append(str(len(re.findall(r"\n(.*?) \(to (.*?)\)\:\n", content)))) - else: - results.append("-1") - else: - # Missing results will appear as blanks - results.append("") - - instance += 1 - instance_dir = os.path.join(test_path, str(instance)) - - max_instances = max(max_instances, instance) - - # Buffer the results - all_results.append(results) - - # Create a header - header = "TestName" - for i in range(0, max_instances): - header += ",Trial" + str(i) - print(header) - - # Print a fully-populated table of results - for r in all_results: - while len(r) < max_instances + 1: - r.append("") - print(",".join(r)) - - -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - # Path to the default results directory - # (relative to this script, up on directory, then into the results folder) - default_results_dir = os.path.realpath(os.path.join(script_dir, os.path.pardir, "results")) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will collate the results of the AutoGPT scenarios and output them to a CSV. The CSV format is as follows: - -TestName, Trial0, Trial1, ..., TrialN -Test_1, x_10, x_11, ..., X_1N -Test_2, x_20, x_21, ..., X_2N -... -Test_M, x_M0, x_M1, ..., X_MN - - -Where x_ij is the number of AssistantAgent conversation turns needed to pass all the tests for problem i, in Trial/repetition j. If the agent was not able to pass the tests by the end of the conversation, the value will be -1. If data for the trial is missing, the value will be an empty string "". -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to the scenario results. (default: " + default_results_dir + ")", - default=default_results_dir, - ) - args = parser.parse_args() - collate(args.scenario) diff --git a/samples/tools/testbed/utils/collate_gaia_csv.py b/samples/tools/testbed/utils/collate_gaia_csv.py deleted file mode 100644 index 88f1ec819ed5..000000000000 --- a/samples/tools/testbed/utils/collate_gaia_csv.py +++ /dev/null @@ -1,128 +0,0 @@ -import os -import json -import re -import sys -import argparse - - -def normalize_answer(a): - # Lower case - # Trim (left and right) - # Replace multiple spaces with one space - # Remove trailing punctuation - return re.sub(r"[\.\!\?]+$", "", re.sub(r"\s+", " ", a.strip().lower())) - - -def collate(results_dir): - """ - Collate the results of running GAIA - - Args: - results_dir (path): The folder were results were be saved. - """ - - all_results = list() - max_instances = 0 - - for test_id in os.listdir(results_dir): - test_path = os.path.join(results_dir, test_id) - - # Collect the results vector - results = [test_id] - - instance = 0 - instance_dir = os.path.join(test_path, str(instance)) - while os.path.isdir(instance_dir): - expected_answer_file = os.path.join(instance_dir, "expected_answer.txt") - if not os.path.isfile(expected_answer_file): - # Expected answer is missing - results.append("") - - instance += 1 - instance_dir = os.path.join(test_path, str(instance)) - continue - - expected_answer = "!!!NULL ANSWER!!!" - with open(expected_answer_file, "rt") as fh: - expected_answer = fh.read().strip() - - console_log_file = os.path.join(instance_dir, "console_log.txt") - if not os.path.isfile(console_log_file): - # Console log file missing - results.append("") - - instance += 1 - instance_dir = os.path.join(test_path, str(instance)) - continue - - with open(console_log_file, "rt") as fh: - console_log = fh.read() - - final_answer = "" - m = re.search(r"FINAL ANSWER:(.*?)\n", console_log, re.DOTALL) - if m: - final_answer = m.group(1).strip() - - # print(f"Expected Answer: {expected_answer}\nAutogen Answer: {final_answer}\n") - - if normalize_answer(expected_answer) == normalize_answer(final_answer): - results.append("1") - else: - results.append("-1") - - instance += 1 - instance_dir = os.path.join(test_path, str(instance)) - - max_instances = max(max_instances, instance) - - # Buffer the results - all_results.append(results) - - # Create a header - header = "TestId" - for i in range(0, max_instances): - header += ",Trial" + str(i) - print(header) - - # Print a fully-populated table of results - for r in all_results: - while len(r) < max_instances + 1: - r.append("") - print(",".join(r)) - - -############################################################################### -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - # Path to the default results directory - # (relative to this script, up on directory, then into the results folder) - default_results_dir = os.path.realpath( - os.path.join(script_dir, os.path.pardir, "results", "gaia_validation_level_1__two_agents_gpt4") - ) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will collate the results of the GAIA scenarios and output them to a CSV. The CSV format is as follows: - -TestId, Trial0, Trial1, ..., TrialN -uuid_1, x_10, x_11, ..., X_1N -uuid_2, x_20, x_21, ..., X_2N -... -uuid_M, x_M0, x_M1, ..., X_MN - -Where uuid_i is the identifier of the ith test question, and x_ij is 1 or -1 depending on if the test passed or failed, respectively. If data for the trial is missing (e.g., due to a runtime error, the value will be an empty string "". -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to the scenario results. (default: " + default_results_dir + ")", - default=default_results_dir, - ) - args = parser.parse_args() - collate(args.scenario) diff --git a/samples/tools/testbed/utils/collate_gaia_jsonl.py b/samples/tools/testbed/utils/collate_gaia_jsonl.py deleted file mode 100644 index 6a4ac07cad30..000000000000 --- a/samples/tools/testbed/utils/collate_gaia_jsonl.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import json -import re -import sys -import argparse - - -def normalize_answer(a): - # Trim (left and right) - # Replace multiple spaces with one space - # Remove trailing punctuation - # Trim again - return re.sub(r"[\.\!\?]+$", "", re.sub(r"\s+", " ", a.strip())).strip() - - -def collate(results_dir, instance=0): - """ - Collate the results of running GAIA. Print the results in the format accepted by the leaderboard. - - Args: - results_dir (path): The folder where results were be saved. - """ - - for test_id in os.listdir(results_dir): - test_path = os.path.join(results_dir, test_id) - - instance_dir = os.path.join(test_path, str(instance)) - console_log_file = os.path.join(instance_dir, "console_log.txt") - - final_answer = "" - if os.path.isfile(console_log_file): - with open(console_log_file, "rt") as fh: - console_log = fh.read() - - final_answer = "" - m = re.search(r"FINAL ANSWER:(.*?)\n", console_log, re.DOTALL) - if m: - final_answer = normalize_answer(m.group(1)) - - # Clean up the GAIA logs so they don't have the Docker setup preamble - m = re.search(r"^.*?\r?\n(user_proxy \(to assistant\).*$)", console_log, re.DOTALL) - if m: - console_log = m.group(1) - - print(json.dumps({"task_id": test_id, "model_answer": final_answer, "reasoning_trace": console_log})) - - -############################################################################### -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - # Path to the default results directory - # (relative to this script, up on directory, then into the results folder) - default_results_dir = os.path.realpath( - os.path.join(script_dir, os.path.pardir, "results", "gaia_validation_level_1__two_agents_gpt4") - ) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will collate the results of the GAIA scenarios into the jsonl format that can be submit to the GAIA leaderboard. - -NOTE: You will likely need to concatenate results for level 1, level 2 and level 3 to form a complete submission. -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to the scenario results. (default: " + default_results_dir + ")", - default=default_results_dir, - ) - args = parser.parse_args() - collate(args.scenario) diff --git a/samples/tools/testbed/utils/collate_human_eval.py b/samples/tools/testbed/utils/collate_human_eval.py deleted file mode 100644 index e46c83f84fc9..000000000000 --- a/samples/tools/testbed/utils/collate_human_eval.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import json -import re -import sys -import argparse - - -def collate(results_dir): - """ - Collate the results of running human eval. - - Args: - results_dir (path): The folder where results are saved. - """ - - all_results = list() - max_instances = 0 - - for test_id in os.listdir(results_dir): - test_path = os.path.join(results_dir, test_id) - - # Collect the results vector - results = [test_id] - - instance = 0 - instance_dir = os.path.join(test_path, str(instance)) - while os.path.isdir(instance_dir): - console_log = os.path.join(instance_dir, "console_log.txt") - if os.path.isfile(console_log): - with open(console_log, "rt") as fh: - content = fh.read() - if "ALL TESTS PASSED !#!#" in content: - # Ideally we would have a more distinctive pattern. - results.append(str(len(re.findall(r"\n(.*?) \(to (.*?)\)\:\n", content)))) - else: - results.append("-1") - - else: - # Missing results will appear as blanks - results.append("") - - instance += 1 - instance_dir = os.path.join(test_path, str(instance)) - - max_instances = max(max_instances, instance) - - # Buffer the results - all_results.append(results) - - # Create a header - header = "TestId" - for i in range(0, max_instances): - header += ",Trial" + str(i) - print(header) - - # Print a fully-populated table of results - for r in all_results: - while len(r) < max_instances + 1: - r.append("") - print(",".join(r)) - - -############################################################################### -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - # Path to the default results directory - # (relative to this script, up on directory, then into the results folder) - default_results_dir = os.path.realpath( - os.path.join(script_dir, os.path.pardir, "results", "human_eval_two_agents_gpt4") - ) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will collate the results of the HumanEval scenarios and output them to a CSV. The CSV format is as follows: - -TestId, Trial0, Trial1, ..., TrialN -HumanEval_1, x_10, x_11, ..., X_1N -HumanEval_2, x_20, x_21, ..., X_2N -... -HumanEval_M, x_M0, x_M1, ..., X_MN - - -Where x_ij is the number of AssistantAgent conversation turns needed to pass all the tests for problem i, in Trial/repetition j. If the agent was not able to pass the tests by the end of the conversation, the value will be -1. If data for the trial is missing, the value will be an empty string "". -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to the scenario results. (default: " + default_results_dir + ")", - default=default_results_dir, - ) - args = parser.parse_args() - collate(args.scenario) diff --git a/samples/tools/testbed/utils/expand_gaia.py b/samples/tools/testbed/utils/expand_gaia.py deleted file mode 100644 index ed751b081320..000000000000 --- a/samples/tools/testbed/utils/expand_gaia.py +++ /dev/null @@ -1,110 +0,0 @@ -# -# Run this file to download the human_eval dataset, and create a corresponding testbed scenario: -# (default: ../scenarios/human_eval_two_agents_gpt4.jsonl and ./scenarios/human_eval_two_agents_gpt35.jsonl) -# - -import json -import os -import sys -import shutil - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_NAME = os.path.basename(SCRIPT_PATH) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -SCENARIOS_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, os.path.pardir, "scenarios", "GAIA")) - - -def create_jsonl(name, tasks, template, model): - """Creates a JSONL scenario file with a given name, list of HumanEval tasks, template path, and model.""" - - with open(os.path.join(SCENARIOS_DIR, name + ".jsonl"), "wt") as fh: - for task in tasks: - print(f"Converting: [{name}] {task['task_id']}") - - # Figure out what files we need to copy - template_cp_list = [template] - if len(task["file_name"].strip()) > 0: - template_cp_list.append( - [ - os.path.join("GAIA_Files", task["file_name"].strip()), - os.path.join("coding", task["file_name"].strip()), - ] - ) - - record = { - "id": task["task_id"], - "template": template_cp_list, - "substitutions": { - "scenario.py": { - "__MODEL__": model, - "__FILE_NAME__": task["file_name"], - "__PROMPT__": task["Question"], - }, - "expected_answer.txt": {"__EXPECTED_ANSWER__": task["Final answer"]}, - }, - } - - fh.write(json.dumps(record).strip() + "\n") - - -############################################################################### -if __name__ == "__main__": - if len(sys.argv) != 2: - sys.exit( - f"SYNTAX: python {SCRIPT_NAME} [path to GIA repository]\n\nNote: to clone the GAIA repository, do 'git clone https://huggingface.co/datasets/gaia-benchmark/GAIA'" - ) - - # Copy the relevant GAIA files - gaia_path = os.path.realpath(sys.argv[1]) - - gaia_validation_files = os.path.join(gaia_path, "2023", "validation") - gaia_test_files = os.path.join(gaia_path, "2023", "test") - - if not os.path.isdir(gaia_validation_files) or not os.path.isdir(gaia_test_files): - sys.exit(f"Error: '{gaia_path}' does not appear to be a copy of the GAIA repository.") - - gaia_merged_files = os.path.realpath(os.path.join(SCENARIOS_DIR, "GAIA_Files")) - - shutil.copytree( - gaia_validation_files, gaia_merged_files, ignore=shutil.ignore_patterns("metadata.jsonl"), dirs_exist_ok=True - ) - shutil.copytree( - gaia_test_files, gaia_merged_files, ignore=shutil.ignore_patterns("metadata.jsonl"), dirs_exist_ok=True - ) - - # Load the GAIA data - gaia_validation_tasks = [[], [], []] - with open(os.path.join(gaia_validation_files, "metadata.jsonl")) as fh: - for line in fh: - data = json.loads(line) - gaia_validation_tasks[data["Level"] - 1].append(data) - - gaia_test_tasks = [[], [], []] - with open(os.path.join(gaia_test_files, "metadata.jsonl")) as fh: - for line in fh: - data = json.loads(line) - gaia_test_tasks[data["Level"] - 1].append(data) - - models = { - "gpt4": "gpt-4", - } - - templates = { - "two_agents": "Templates/BasicTwoAgents", - } - - # Add coding directories if needed (these are usually empty and left out of the repo) - for template in templates.values(): - code_dir_path = os.path.join(SCENARIOS_DIR, template, "coding") - if not os.path.isdir(code_dir_path): - os.mkdir(code_dir_path) - - # Create the various combinations of [models] x [templates] - for m in models.items(): - for t in templates.items(): - create_jsonl(f"gaia_validation_level_1__{t[0]}_{m[0]}", gaia_validation_tasks[0], t[1], m[1]) - create_jsonl(f"gaia_validation_level_2__{t[0]}_{m[0]}", gaia_validation_tasks[1], t[1], m[1]) - create_jsonl(f"gaia_validation_level_3__{t[0]}_{m[0]}", gaia_validation_tasks[2], t[1], m[1]) - create_jsonl(f"gaia_test_level_1__{t[0]}_{m[0]}", gaia_test_tasks[0], t[1], m[1]) - create_jsonl(f"gaia_test_level_2__{t[0]}_{m[0]}", gaia_test_tasks[1], t[1], m[1]) - create_jsonl(f"gaia_test_level_3__{t[0]}_{m[0]}", gaia_test_tasks[2], t[1], m[1]) diff --git a/samples/tools/testbed/utils/metrics_gaia.py b/samples/tools/testbed/utils/metrics_gaia.py deleted file mode 100644 index 6119f4f38f49..000000000000 --- a/samples/tools/testbed/utils/metrics_gaia.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import sys -import argparse -import csv - - -def metrics(results_fh): - """ - Compute metrics from collated GAIA results. - - Args: - results_fh (File Stream): A file stream containing the collated results in CSV. - """ - - reader = csv.reader(results_fh) - first_row = next(reader) # Read the first line - - num_trials = len(first_row) - 1 # Don't count the first column (TestId) - - # Set up the counters - counters = [] - for i in range(0, num_trials): - counters.append({"successes": 0, "failures": 0, "missing": 0}) - - # Load the results. We'll need to iterate over them a few times. - results = list() - for row in reader: - name = row[0] - trials = [(None if v.strip() == "" else int(v)) for v in row[1:]] - for i in range(0, len(trials)): - v = trials[i] - if v is None: - counters[i]["missing"] += 1 - elif v > 0: - counters[i]["successes"] += 1 - else: - counters[i]["failures"] += 1 - - results.append([name, trials]) - - def _safe_div(num, denom): - if denom == 0: - return "" - else: - return num / denom - - # Print the header - for i in range(0, len(counters)): - counter = counters[i] - n = counter["successes"] + counter["failures"] + counter["missing"] - score = _safe_div(counter["successes"], n) - print(f"{i},{n},{counter['successes']},{counter['failures']},{counter['missing']},{score}") - - -############################################################################### -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will compute metrics on the collated results of the GAIA scenarios. Use collate_gaia.py to prepare input to this script. - -The output will be formatted as a CSV with the following schema: - -Trial, n, successes, failures, missing, score -0 N_0, s_0 f_0 m_0, p_0 -0 N_1, s_1 f_1 m_1, p_1 -... -M N_M, s_M f_M m_M, p_M - -Where: - - N_i is the number of questions in trial i - s_i is the number of successes in trial i - f_i is the number of failures in trial i - m_i is the number of missing values in trial i - p_i is the proportion of successes in trail i (i.e, s_i / N_i) - -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to collated results. If '-' or omitted, read from stdin. (default: '-')", - default="-", - ) - args = parser.parse_args() - - if args.scenario == "" or args.scenario == "-": - metrics(sys.stdin) - else: - with open(args.scenario, "rt") as fh: - metrics(fh) diff --git a/samples/tools/testbed/utils/metrics_human_eval.py b/samples/tools/testbed/utils/metrics_human_eval.py deleted file mode 100644 index 25d9aa90fda2..000000000000 --- a/samples/tools/testbed/utils/metrics_human_eval.py +++ /dev/null @@ -1,116 +0,0 @@ -import os -import sys -import argparse -import csv - - -def metrics(results_fh): - """ - Compute metrics from collated HumanEval results. - - Args: - results_fh (File Stream): A file stream containing the collated results in CSV. - """ - - reader = csv.reader(results_fh) - first_row = next(reader) # Read the first line - - num_trials = len(first_row) - 1 # Don't count the first column (TestId) - max_turns = 0 - num_rows = 0 - - # Load the results. We'll need to iterate over them a few times. - results = list() - for row in reader: - num_rows += 1 - - name = row[0] - trials = [(None if v.strip() == "" else int(v)) for v in row[1:]] - for v in trials: - if v is not None: - max_turns = max(max_turns, v) - results.append([name, trials]) - - # Print the header - header = ["Trial"] - for i in range(1, max_turns + 1): - header.append("cumulative_passes_by_turn_" + str(i)) - header.append("fails") - header.append("missing") - print(",".join(header)) - - # Compute the metrics - def _metrics_for_trial(t): - counts = [None] - fails = 0 - missing = 0 - - # Compute cumulative passes for each conversation turn - for i in range(1, max_turns + 1): - counts.append(0) - assert len(counts) == i + 1 - - for r in results: - v = r[1][t] - if v is not None: - v = int(v) - if 0 <= v and v <= i: - counts[i] += 1 - - # Count missing and failed - for r in results: - v = r[1][t] - if v is None: - missing += 1 - elif int(v) < 0: - fails += 1 - - # Prepare the row in the format specified by the header - return str(t) + "," + ",".join([str(v) for v in counts[1:]]) + "," + str(fails) + "," + str(missing) - - # Print each row - for t in range(0, num_trials): - print(_metrics_for_trial(t)) - - -############################################################################### -if __name__ == "__main__": - script_path = os.path.realpath(__file__) - script_name = os.path.basename(script_path) - script_dir = os.path.dirname(script_path) - - parser = argparse.ArgumentParser( - description=f""" -{script_name} will compute metrics on the collated results of the HumanEval scenarios. Use collate_human_eval.py to prepare input to this script. - -The output will be formatted as a CSV with the following schema: - -Trial, cumulative_passes_by_turn_1, ..., cumulative_passes_by_turn_N, fails, missing -0 x_01, x_0N, y_0, z_0 -1 x_11, x_1N, y_1, z_1 -... -M x_M1, x_MN, y_M, z_M - -Where: - - x_ij is the number of HumanEval problems in Trial i that achieved a passing result by conversation turn j. - y_i is the number of HumanEval problems in Trial i that never achieved a passing result (they failed). - z_i is the number of HumanEval problems in Trial i that have missing data. - -""".strip(), - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "scenario", - nargs="?", - help="Path to collated results. If '-' or omitted, read from stdin. (default: '-')", - default="-", - ) - args = parser.parse_args() - - if args.scenario == "" or args.scenario == "-": - metrics(sys.stdin) - else: - with open(args.scenario, "rt") as fh: - metrics(fh) diff --git a/samples/tools/testbed/utils/prepare_autogpt.py b/samples/tools/testbed/utils/prepare_autogpt.py deleted file mode 100644 index 9da279735452..000000000000 --- a/samples/tools/testbed/utils/prepare_autogpt.py +++ /dev/null @@ -1,102 +0,0 @@ -import base64 -import glob -import json -import os -import shutil - -current_file_dir = os.path.dirname(os.path.abspath(__file__)) -challenge_path = os.path.join(os.path.dirname(current_file_dir), "scenarios/AutoGPT/challenges") -data_paths = glob.glob(str(challenge_path) + "/*/data.json") - -for data_path in data_paths: - print("Converting data path: ", data_path) - workspace = os.path.dirname(data_path) - - with open(data_path, "r") as f: - data = json.load(f) - - should_contain = data["ground"].get("should_contain", []) - should_not_contain = data["ground"].get("should_not_contain", []) - case_sensitive = data["ground"].get("case_sensitive", False) - - # since 'should_contain' field may contain escape characters, this can cause problems when using str() method and eval(), I used base64 encode to avoid such problems - should_contain_base64 = [] - for word in should_contain: - encoded_word = base64.b64encode(word.encode("utf-8")).decode("utf-8") - should_contain_base64.append(encoded_word) - - should_not_contain_base64 = [] - for word in should_not_contain: - encoded_word = base64.b64encode(word.encode("utf-8")).decode("utf-8") - should_not_contain_base64.append(encoded_word) - - # copy all the files needed to 'coding' directory - # 1. 'artifacts_in' directory: all the files needed for QA - save_path = os.path.join(os.path.dirname(current_file_dir), "scenarios/AutoGPT") - artifacts_in = False - if os.path.exists(os.path.join(workspace, "artifacts_in")): - artifacts_in = True - target_folder = os.path.join(save_path, "Templates/TwoAgents/coding/file", data["name"]) - if os.path.exists(target_folder): - shutil.rmtree(target_folder) - shutil.copytree(os.path.join(workspace, "artifacts_in"), target_folder) - # print(f"All the artifacts are copied from {os.path.join(workspace, 'artifacts_in')} to {target_folder}") - - # 2. 'custom_python' directory: all the files needed for testing python code - if os.path.exists(os.path.join(workspace, "custom_python")): - target_folder = os.path.join(save_path, "Templates/TwoAgents/custom_python") - if not os.path.exists(target_folder): - os.makedirs(target_folder) - for filename in os.listdir(os.path.join(workspace, "custom_python")): - shutil.copy(os.path.join(workspace, "custom_python", filename), os.path.join(target_folder, filename)) - # print(f"File copied from {os.path.join(workspace, 'custom_python', filename)} to {target_folder}") - - record = { - "id": data["name"], - "template": "Templates/TwoAgents", - "substitutions": { - "scenario.py": { - "__MODEL__": "gpt-35-turbo-16k", - "__TASK__": data["task"], - "__TARGET_FOLDER__": f"file/{data['name']}" if artifacts_in else "", - }, - "check.py": { - "__FILE_PATTERN__": data["ground"]["files"][0], - "__EVAL_TYPE__": data["ground"]["eval"]["type"], - "__CASE_SENSITIVE__": str(case_sensitive), - }, - "should_contain.txt": { - "__CONTAIN__": str(should_contain_base64), - }, - "should_not_contain.txt": { - "__NO_CONTAIN__": str(should_not_contain_base64), - }, - }, - } - with open(os.path.join(save_path, "autogpt_twoagent_gpt35.jsonl"), "a") as f: - f.write(json.dumps(record).strip() + "\n") - - record = { - "id": data["name"], - "template": "Templates/TwoAgents", - "substitutions": { - "scenario.py": { - "__MODEL__": "gpt-4-1106-preview", - "__TASK__": data["task"], - "__TARGET_FOLDER__": f"file/{data['name']}" if artifacts_in else "", - }, - "check.py": { - "__FILE_PATTERN__": data["ground"]["files"][0], - "__EVAL_TYPE__": data["ground"]["eval"]["type"], - "__CASE_SENSITIVE__": str(case_sensitive), - }, - "should_contain.txt": { - "__CONTAIN__": str(should_contain_base64), - }, - "should_not_contain.txt": { - "__NO_CONTAIN__": str(should_not_contain_base64), - }, - }, - } - with open(os.path.join(save_path, "autogpt_twoagent_gpt4.jsonl"), "a") as f: - f.write(json.dumps(record).strip() + "\n") diff --git a/website/blog/2024-01-25-AutoGenBench/img/teaser.jpg b/website/blog/2024-01-25-AutoGenBench/img/teaser.jpg new file mode 100755 index 0000000000000000000000000000000000000000..00571529badc80944d0328cf0d5dd33c50b8be06 GIT binary patch literal 240855 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

AutoGenBench is a standalone tool for evaluating AutoGen agents and workflows on common benchmarks.

+ + +## TLDR +Today we are releasing AutoGenBench – a tool for evaluating AutoGen agents and workflows on established LLM and agentic benchmarks. + +AutoGenBench is a standalone command line tool, installable from PyPI, which handles downloading, configuring, running, and reporting supported benchmarks. AutoGenBench works best when run alongside Docker, since it uses Docker to isolate tests from one another. + +* See the [AutoGenBench README](https://github.com/microsoft/autogen/blob/main/samples/tools/testbed/README.md) for information on installation and running benchmarks. +* See the [AutoGenBench CONTRIBUTING guide](https://github.com/microsoft/autogen/blob/main/samples/tools/testbed/CONTRIBUTING.md) for information on developing or contributing benchmark datasets. + + +### Quick Start +Get started quickly by running the following commands in a bash terminal. + +*Note:* You may need to adjust the path to the `OAI_CONFIG_LIST`, as appropriate. +```sh +export OAI_CONFIG_LIST=$(cat ./OAI_CONFIG_LIST) +pip install autogenbench +autogenbench clone HumanEval +cd HumanEval +cat README.md +autogenbench run --subsample 0.1 --repeat 3 Tasks/human_eval_two_agents.jsonl +autogenbench tabulate Results/human_eval_two_agents +``` + +## Introduction +Measurement and evaluation are core components of every major AI or ML research project. The same is true for AutoGen. To this end, today we are releasing AutoGenBench, a standalone command line tool that we have been using to guide development of AutoGen. Conveniently, AutoGenBench handles: downloading, configuring, running, and reporting results of agents on various public benchmark datasets. In addition to reporting top-line numbers, each AutoGenBench run produces a comprehensive set of logs and telemetry that can be used for debugging, profiling, computing custom metrics, and as input to [AgentEval](https://microsoft.github.io/autogen/blog/2023/11/20/AgentEval). In the remainder of this blog post, we outline core design principles for AutoGenBench (key to understanding its operation); present a guide to installing and running AutoGenBench; outline a roadmap for evaluation; and conclude with an open call for contributions. + +## Design Principles +AutoGenBench is designed around three core design principles. Knowing these principles will help you understand the tool, its operation and its output. These three principles are: +- **Repetition:** LLMs are stochastic, and in many cases, so too is the code they write to solve problems. For example, a Python script might call an external search engine, and the results may vary run-to-run. This can lead to variance in agent performance. Repetition is key to measuring and understanding this variance. To this end, AutoGenBench is built from the ground up with an understanding that tasks may be run multiple times, and that variance is a metric we often want to measure. + + - **Isolation:** Agents interact with their worlds in both subtle and overt ways. For example an agent may install a python library or write a file to disk. This can lead to ordering effects that can impact future measurements. Consider, for example, comparing two agents on a common benchmark. One agent may appear more efficient than the other simply because it ran second, and benefitted from the hard work the first agent did in installing and debugging necessary Python libraries. To address this, AutoGenBench isolates each task in its own Docker container. This ensures that all runs start with the same initial conditions. (Docker is also a *much safer way to run agent-produced code*, in general.) + +- **Instrumentation:** While top-line metrics are great for comparing agents or models, we often want much more information about how the agents are performing, where they are getting stuck, and how they can be improved. We may also later think of new research questions that require computing a different set of metrics. To this end, AutoGenBench is designed to log everything, and to compute metrics from those logs. This ensures that one can always go back to the logs to answer questions about what happened, run profiling software, or feed the logs into tools like [AgentEval](https://microsoft.github.io/autogen/blog/2023/11/20/AgentEval). + +## Installing and Running AutoGenBench +As noted above, isolation is a key design principle, and so AutoGenBench must be run in an environment where Docker is available (desktop or Engine). **It will not run in GitHub codespaces**, unless you opt for native execution (which is strongly discouraged). To install Docker Desktop see [https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/). +Once Docker is installed, AutoGenBench can then be installed as a standalone tool from PyPI. With `pip`, installation can be achieved as follows: + +```sh +pip install autogenbench +``` +After installation, you must configure your API keys. As with other AutoGen applications, AutoGenBench will look for the OpenAI keys in the OAI_CONFIG_LIST file in the current working directory, or the OAI_CONFIG_LIST environment variable. This behavior can be overridden using a command-line parameter. + +If you will be running multiple benchmarks, it is often most convenient to leverage the environment variable option. You can load your keys into the environment variable by executing: + +```sh +export OAI_CONFIG_LIST=$(cat ./OAI_CONFIG_LIST) +``` +## A Typical Session +Once AutoGenBench and necessary keys are installed, a typical session will look as follows: + +``` +autogenbench clone HumanEval +cd HumanEval +cat README.md +autogenbench run --subsample 0.1 --repeat 3 Tasks/human_eval_two_agents.jsonl +autogenbench tabulate results/human_eval_two_agents +``` + +Where: +- `autogenbench clone HumanEval` downloads and expands the HumanEval benchmark scenario. +- `cd HumanEval; cat README.md` navigates to the benchmark directory, and prints the README (which you should always read!) +- `autogenbench run --subsample 0.1 --repeat 3 Tasks/human_eval_two_agents.jsonl` + runs a 10% subsample of the tasks defined in `Tasks/human_eval_two_agents.jsonl`. Each task is run 3 times. +- `autogenbench tabulate results/human_eval_two_agents` tabulates the results of the run. + +After running the above `tabulate` command, you should see output similar to the following: + +``` + Trial 0 Trial 1 Trial 2 +Task Id Success Success Success +------------- --------- --------- --------- +HumanEval_107 False True True +HumanEval_22 True True True +HumanEval_43 True True True +HumanEval_88 True True True +HumanEval_14 True True True +HumanEval_157 True True True +HumanEval_141 True True True +HumanEval_57 True True True +HumanEval_154 True True True +HumanEval_153 True True True +HumanEval_93 False True False +HumanEval_137 True True True +HumanEval_143 True True True +HumanEval_13 True True True +HumanEval_49 True True True +HumanEval_95 True True True +------------- --------- --------- --------- +Successes 14 16 15 +Failures 2 0 1 +Missing 0 0 0 +Total 16 16 16 + +CAUTION: 'autogenbench tabulate' is in early preview. +Please do not cite these values in academic work without first inspecting and verifying the results in the logs yourself. +``` + +From this output we can see the results of the three separate repetitions of each task, and final summary statistics of each run. In this case, the results were generated via GPT-4 (as defined in the OAI_CONFIG_LIST that was provided), and used the `TwoAgents` template. **It is important to remember that AutoGenBench evaluates *specific* end-to-end configurations of agents (as opposed to evaluating a model or cognitive framework more generally).** + +Finally, complete execution traces and logs can be found in the `Results` folder. See the [AutoGenBench README](https://github.com/microsoft/autogen/blob/main/samples/tools/testbed/README.md) for more details about command-line options and output formats. Each of these commands also offers extensive in-line help via: + +- `autogenbench --help` +- `autogenbench clone --help` +- `autogenbench run --help` +- `autogenbench tabulate --help` + + +## Roadmap +While we are announcing AutoGenBench, we note that it is very much an evolving project in its own right. Over the next few weeks and months we hope to: +- Onboard many additional benchmarks beyond those shipping today +- Greatly improve logging and telemetry +- Introduce new core metrics including total costs, task completion time, conversation turns, etc. +- Provide tighter integration with AgentEval and AutoGen Studio + +For an up to date tracking of our work items on this project, please see [AutoGenBench Work Items]( https://github.com/microsoft/autogen/issues/973) + +## Call for Participation +Finally, we want to end this blog post with an open call for contributions. AutoGenBench is still nascent, and has much opportunity for improvement. New benchmarks are constantly being published, and will need to be added. Everyone may have their own distinct set of metrics that they care most about optimizing, and these metrics should be onboarded. To this end, we welcome any and all contributions to this corner of the AutoGen project. If contributing is something that interests you, please see the [contributor’s guide](https://github.com/microsoft/autogen/blob/main/samples/tools/testbed/CONTRIBUTING.md) and join our [Discord](https://discord.gg/pAbnFJrkgZ) discussion in the [#autogenbench](https://discord.com/channels/1153072414184452236/1199851779328847902) channel!

D|DC<&>$=7^3&qr&0B%aR{h~ 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≺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�?ydqBIEIX>d6v|l!RQ|7yQn`r1brN{2UCcRudny9XPE+KP4 zm5OBXTKZgZ{haxP3ZzpbBB6PvS6Jien)LB++((&&UiqZ!pS^=!t;PD)GT#Y z{{Vzq=SG?YmKlj)Tlh~O;as2g3r-3=xK7yMpRHXUWUPBwdBxC+d)Vdm9YQ$d!ky2a z0PR`NKc0qLsT&`{g1f=ttslw2hYmWhsL%I zdm`mDmCi`8mPTN_D99tQ{{T3yi$RZV#4@vj>y{t)$*X=C@V&+6se5ZZyI@#M>aM*Q zkCc9Nz8M&}R69$cbYax4RFCUjJhnL9PJ7#9u3b^lRLPK7aZ^EX*8r9$K9!$m0iVlR z4pekG>sRih`#gWU;MP;N$SL`jW)b-$xv=Nv=};uEumRp8f0C z{5xazO%e!z#F7B+e!u-{@@*2#O{m(zjPH>_=ia@-)iD?#cEA-A)ueGnX>o~fZ)v`N zEfw4EI0JKjHOT5ZzxJ)rYq_2+DO(E+XXd%bbNi6xP;Tc4n>Jk_Q$ypB(>uAcsX^m$wG6~^A~k~tYhcAmA} zvN5L?Mvu%E$mV#xJDd#v~aamhVbInN-?vyJXs>A|MVM(NK zV@B%kT}N1&^5BCkvNO3*03?#c;Cq^-wPU-?qbeIDde-Klqouu^w>GyC%N$drBVZ?a zDY&-ofOZ+=QUa?g>jv&k(>;UmCbo!PX}_II`dW{W4B`-2r&q7 z2 zjAY`jUTLGlekGSqnnNrO0`FmrgWn#;wNmJE@+c)6ovwprrA1>TRtqF=DCElfvU`7= zRF;_gQC1v|3i?+!rCiBrb>z7`9xJDrqw`_BkNB{%59?7RLh~lOM##Y8r?F##TOddV?nH5pJ!;36&|)n6W~;$7TgqGsh%w~Sz@#DF`BTv%Tf4a+oGTs2gT+qLpoqG0?^I-qb_wh0Qp#0r*9V^BlSpZ@ zOI8A1#i~2IkkexyjYS)Q$vpZBW80ULaXfXWZ9@7QP|B$m9KMLm(W{}`lU1xCX zro#+JZ}+|HwldJ=s+w9I^T@Yf3pG1HK`_R{lh3Vu{JK5l@@L9u*FSmquVT=AOd_63 z38GfFk#OLSq;x;dvNhLhsbt$=hA>XmM%o+tis5|FsO+?7bqPh%N>5a9*4FmAXU?nU zN(U}`eJeih@>{n)Svc--#cogG`|Glijm*DzfWLHi=~*`RHxk|@t%MJ9^JmILQeTXA zA6n?A9&7hPsVgh=GhcHlAvb}I&FPxyyeAZv_ipMoY>ct`WK}eR`%Tj|IIf|V*%=Va z9I6+7*$w~%WROF2BO6Xh=yg0V3xg?cK#8|wwY z$m>&E$F|(BpsPA2iu#O`Pc+MGr@()Ibtm0VVd}>tkAHf_ut>3|+g$4NEuEBRSr}&> zln&(f6zME(ZSGV45u+XUp&Q`eN=n*WusrMlZk@&tOeiF7?_E0B#JY39$^Dn2#-*f= zX4x&!9bR_oss7Id^x$z`L-7ydbLwpR7lhTIvkT=*I&zjfruCb3($ekg5QT)!ttlXT~Eca>CJg-4Z2zq z@oZ83@brhl2Jc1UMob(WK(mf){EpTpm^btt5h zWm%r5;ieY)HIxhGFpeR*Nb!eaOCFtZkTc&E0>K5(iXxoAk-}t<7E}Pr$X9}U{Lbf} z?hi`jJUOl%56cp}NeqpdAlvsdz&Z6G1B_sTIrQ$eOPgzz@cepd-6XR4z^TR%g3@Qd zA;#AKY-Yq2S`FzZuCH|}t!uUgK*a&azY&#g}c zhywz7CaTRE!x}esqiY-xIc{({%@(*`#y^f=x0}Lu7c#SNwvhrZFxy+)YBuv}w~)D6RJE%KL$Dm20vSdQPv1X5(!D#ywqr{0W!<*Ba-d(Bis_<# zl8iP(u;(4PAp7FH>sr$7bopeHOGmes1p$iB=5CR3y+H#Q8;3b3)C}UcjY!!?utuBJ zv)V75FT=e&u=|ojk)E82@_!G5{jq&yF}&$_imRTxBXt@4fu)b)9=AV*KeE2bY9WM1 z8bTj@SYd`Fk_kTDdBCoBOxEtw_T*e&&0!?dA~Cm}pe`BLdXl3han24$d{UOJyY)Hn zH0n{K?B>4`t)1PxP-;so>K;gdiFT8``KnG2ZsGW9l$D zQTS9gI`Y|T7gu(AeT;%tA}B7PNl_1M0x^-(rD$u~_NS=a+s`c4!DEy6F$t6UX(iEX8opLZKTP)DKb=sjzt zI*r6ja$4@$XSjy0yf69K#&qMtEEIBX|y&0|TZA9^Tc{d_GyH@q}xr#P*vad0D3$Mmal}u^-+% zn#2;tbK-qEOk2-0Tf-h?l!XN&^7j%BK^rrSago=*O(mWE^UDRA+p2jHiBef(g$79^ zZDYKVj-+RwrDCOLbJ49%HDKw>sp$Uz4>c=EUh>u}#oilo+b%-j01NtOkUIYW4R<-O z8}Nn9dL_$84b_{=kR*oFBt`<@Zfs$R!TKH5=maXMCBYH87I4CZL?l`2z zD=l@nT1|n5X`kD%nf$Oh&MO{PYs`&&QdV8guCOo%y+?DZn4?xvoz#?D8)ody?c}60%<+HIA z-?Udo`JOTSLZka!OxO$wtP5w6?6S;BBA=q$zWEwPEzcRFV5r#h>0vItrz2ENLanxx;wc<^*XJ;GW#qm|UXEFk}Qacmus?>l)MQ zv%HSnb`?)eeYdG0Y`d34W*z*0YUTdSG zlHq(q;e5&dxmRq1AH|Npk6Q7s5!#=dLxzkfBOFbops4+awENs?+;$)-R&hjugF34{uBOv zD@NX99HmL+PAisqjg9$3xz_PZjvnRD$Bz+BzgsLJkUN=I>n2syy8_W>N#f+lSNq{{Sl6(e#MCVRxh0 zNhys>m1YN$xXwRJR_#aPXBxhUzodAg;@85`YgdmF+_liQg`8~|z$D|pQ&+~D;MS6D zNxVa{&fjiP9Z6^ENx;d;>^fJcUieqTcG?7X7uJ^Rr`v$RWatPVc%NT-!qYYX00~&P z**ra>%Pdj1FKJ{-=hUBHdgb`X|FGQEV^uL7yTmWVphQ506;u(&2_fg-K5eiPaM{=x80876B$2Dj32_Z zhdnErO|2R_6HRE(ov!vQxL$p!)=@`1fhm()$n0FxY}mzer>8ipNhCJzZ2g^VBv~=V zLwTqr%!`5Yfmd&tWU>vO)ymdeqmgy$zBr7dn-leZ}d zxe_Mack5QP%V9L-$iN=ePFM}IkVY!)mFR{&xb2G3>|4?qT*0h z82242^qvw&wJ8h!`yFe&@usC~W28unBCLRXxIHVzQ^i`9tkM`;c7KZRxT9)5fNupsadnX+9>s^!fl7dn> z;q2o-blcQ?w6IXEla86JU4;E}$f|JO++55=q~n|l(zQ+U5-@nLHnld|GQ`n!B$^Qc z3)ddh>vm~n+Bg{Hi0M(!oRJ<$^aC}wqj*!W4ATFkdCKK7};`|H_$b$ zjM6DPf}N)wYY{GWIDETSW{FZYxtc8Uph%}_9^kjQbc)R zGJigm$6s1&a$JIK%VX<1IEX&e0~L7=UaW$Sa)V z6)oHDh=Yv!*DC%lvo~I1Tb#3Gk;hu>d@rTX;>%NbgY1?{PWk5G40Xq@DkY_vqE<6( zkPX08nw_TWQ79aW(A0cc;Li=Yw9_DyTf1MqAUI6@YR-+U{{X^gZztKbNbRFhg5GD! zGCfUYE6LxXwL0FyYG)Y=13C|XTIgkk*;8*!ecCT{HMF_hgyuH+h7Cb2%lY_F+ednF zk196CVq=Jslr}m)2Sl&pYjs`<ik8k{g!W(O9*Jm{_>nK z`qrM6sPD0iy8z0iUp~ig^fmMpV7{)DPtLPCzwPpsMRj#=m-TZ`Q2I2+=O>??z{{Y%LgI#|KJ0l*5r?293`Bj~2=)0|)C+C~*5&SAT*Mo_u zr&iJEecoL{RIt(K-RjJ*5^DSG-PHWO6Z~t+H7kwG$GJ7x_`2;=5TU?PfnI%cF6Jk; zYkDg~TAP_Vlj7BY{t!v}*DdzkHq7l_hn}^+EbDS^5PFeZ{k`F|nVLA%$Osu_&M{p| zDo)MVn9fw!GzoXytIjA z)1hm4ZV@m{SsPUL`A+N~e_m;@YZ^_pzLF!-73PGiT|Dke&xaXZy)aY}+po7d-K`nX zCwQq_b2~_a=I-h%dwY3qjE*+N8WX(pz{g&P@~Jgjc8gJ&8quv)oU~}?<_n%PU9O$+ z8ZQfYnXh~^pj+D;yK^6w@|RUP$Y6cHmVIjnRM(qa(_pri-ZhpRspfe^Y8iv=+Pk{o z5a-k8_N%utIHNha-r5}MT9a@U{%&wEdz!th**%@`7y(^DQ;c-$Q|Q*;Y1Jd{iuwAQ z>vfG8bt@8!#PN*Aef>Wg&#jJ^K3OA%f&9R{m2vl2bq1{K`j(@prK{??gtulj3mB2f z*f0SAbR9oIRFPUchKYvLo-^3hiR1f4xd*RMJq=uKdoy{(Nhu=zmC?DiNi+M%xNmyu z^oyjpgu@}?MaEBKUVWt9`8Ne0?zhU{Ue(fCNYKmjo<8uRO4=NqBIE_`kYtK`sa%-HLLfQS37rP zEloJ{O6*d;$Vq3o60=1s1Zar#^yq`!xl6W$%HV;N z!NqzJNu!2t(y}(KuNXxnR|9N`*EJGNlGyKAk;`zBD7+b&lb?U{n$SfGs?F5bEk$N^ zQ@oKWW@Q=a){#hVBx;n(io6eMr!j1NyyLBB9Zh9rv0>IQyO91B6HZz1z4*l}Xj;^< z>q!EIakyjJnIpoKkQVD#Rh45T^&^U*G>6Uy914Ev8GG_64D?S2T?o7#b^*&Nb6x>> z%`T-of!0PR^RHR(_48OmEK(9;lymj27ge^3JwkH|e1*yBSw``jxvXs9jO!S9+}(XM zQjJPWNw713af8~V7PGC3>Igq~3k4=*8EoTJjn*!%%=fIQhg|F&(MrTK71c zrasn09~IhZ*ZP}Vw5ns+f90&7Duwpt$KVLAQ^2!H2CBBR zB#@-G&l}9#Bw^K=9G~tIqn@0PZ2E4W;8?XU6E}%9*qBDGzTVPLl(u=?VXz>IGrSSSo)mTWKc#9} z_-f7qTp`vj0XyWCjCRV|=Lf5t{s+>vjgruZi+o} zam8AnM7+F>gI&Qdk~vFgq@M?jjp(IMZ{qD;CEew;FBOgSje7C&mJu%4$8r0$;Bo8J zbKbJ!@uket6t;%m*4{kgF&HWU=m{9cco`itRj%UZxzEF6=Up&)gpDJAtZeu0LFE4c z^{di+4{HiREy)CzCKLpH#0}7WPc_D0uH8o7^hC)W$lO<^9dWp0@l*KMrFe44!=yzM ze6Ccn2eBtKOj3GiUVAdk?5E}HP{$r63OWPFy-Xx+ts8K0P{v4;aps`QLe0oHKzRbK z==Vu)szWTS5n)*+X%9%kM$fqYHq}EQ`E&GYw`cZyWx2neOLdOU-RF@mkhF5fFs_6E zPDpYG2a-5FDjvlx^f@0LYLn~sc6x=p+EBRjQYkIM7rc@{*eq4!8lmE)g-bHaWjVvBReryZP+`2J5GPPKL=jxT33km$SyREHXS7({o#Wfl3l}g z+$2Mj?rKjMYH-W1Lb`3Bm2K^#irn0wO~|p2-eIr^$tTm;=B_u3Uo&0KIybkwjzp5_ z>|J4rV_2EwS0I2{v7C23`qQj$?rkm>ODUQ;ZRUBz#d2S8+H?6I%+`g4rHohJUafg+ z_HD{cMtM#^JmI5O0H5nyS~jQsk*QqT*}-iUw6@RW&8SYXs-%N%@4LQC76c4%dB=Kk zm9M4Ic3Yg)&X+5Af;jHub!({(*`+>GyJK-Xp98q+M_O&&tG&ByW!nUsN@oX-I(``* zwQF9~Uh`hH`)$N}jBq4S`RrOjv3TIfG6yG&U{v~lh}u0q_ej#kyiwgqC0CL#Ohn)n z4Yv!NE=kAS{OS^wwKJ%!?uhIsoawXKNfIJT(M~Qd;#Lc@a^P+UJTq_sCj$biYxK>aeU3hPb?GQcpbm(hY#91)G%^Z(MLQ z&{WH&TiFBVGTbt9%uXUsndhr^ZEIagkXWox`Ptj#TSTmIagtSXKN^ZXR^6mabYjWB7(umEpXJSU6L@tkENmxB ziRRR9ZX=G+apeYg3O-ZGAQSDJXT5I-%1g1&TD2;2QJ(K(t%3Z@th-KjvuyK^I|~xM zdHbXedB`UNIIWX85cTG{%_8PoyBm9(rgoM{!xHRC=blL<*37SnYm!S+)6iJ1fgrb((v=G{m2t(as2*+zR-0F9-cU$8ZyBbAE-t|?DQ zkxDLAEJP7wQ&OSBD$5gZ#;74ACV*6yt^{+Mf%6DqEt^WW_>LK=Sc1aqMfeRaH<20-(RNTiZ8mq zLtGVo{d%30T85nunl)=Sx4CfR45#z=s@`{%aSh=A0Cqq-{{WF*gW}x?vdHMTc*zVs zYtI#S$oB8TNybR%dw#XkLTaQRb6Sz*mCbvI)@Zg9ki+FQKZoy{cwbMS{d5`Gp6luO ze=ntHY0@;Xy{w<>Vf(*IhURCn)NS<$I63);sqQ}!Q*MKO&blGy#PFVs2&*0p)Za?f zVV9Eja|gzHfO#K?$g1lqGe>a`F~|cC#-kuvYkU6yf?OKurzIt3aMpv4sPS(L*dFV{702@72VCVv_=Vv;Y&GwobkJ0C%91UI|w z@$J&70!!p|&1~s91eXw(ZU#80=dxRCf{oUON^7A(*|rWi#b2?w@@-k!c{LQE7>+t( zmeLoJGO>!66=IZ%Hxqf1WY*T3IF8dN)3s(?Sru=b;M1aW5*0s%Q$S01D3SMPjL~gu z(@HQi?N#|M2T@wqbEH8saKo)K88Ye2vqu>R_ly_wHRwJf@r}2G^f+Tpp}erDgXvt2 zoaXmawNb!V2DtBwchX7XMTa?9&|-7`w;s9Pa(RkCQR!S=lC!uiT`F8ku*UEml0}A6Jw5)UThKYKN=CyRod2VHn-rWmJBVZ`^thcvlrXEP(aBI=N z8tHrFpH!WGNPbAk?oVIHe>(A}D=VJ$uFKQe=wA=?7uEHxvQD6O%R8QmgZ%o}owta* zTk#K4jrYl-_!P^0TUbXNvA`a+&Ulwui%iqq*|zOcahgi)*c_R`{6z6Y zdQGE63k}%71HE`{)ztch!_7Q|l5$D@Rk7mjT3uUKV(duCIL%`y8?ZSG$R4%mLMkya z-Q4Ru1>kq_MyKXnpC?VW8|RdqWDeE2u6Wl?)-}t02Hrzw;Y&6V%CMb=aJ=$I@RBk3 z=Bvr$Rfpjf#+{@=_Gej^8D&<>O}sM}KAbKq%%XjbgUCH=4QAcVpqA|ELfTz;hFeV; zL{_&mZ9+#x`u?@MuIrQOklX0mBmJJ$6c&(i<}vO~aspU%_=MK&BTnOt9ultFYUw1R z7Xf_I6->V{UvhmbSZid=t3{`CuC%(IGR->17=;6bFZW*^TO9olv8{;jEJe4M zISU~jO>);h8NBeVjjpuvwbg~|lxYwi7z59->x!1r%5MU}FoB_wjj+pUj7_-xtnJf}GuFh9@ZUb~(v#5@Oj`bUdC#i+v2T&`nue#f`hBl(K; znXjf20Rth=P~EG`sQG5@cf;R;XvNpOB>Iiw$pot!hw}k%;U}$O>tfeI@+=nZ?lmj1 zwIhvx)vxh6ABn|wa!)UieCg01?*+ggnXWz^KSR^CD~s80MaAsr_mfX3XU2MQ(0dPW zN^c!C4`!Z+FZNy@b-k>kskNnutTfiRlMy1Oa8KO;a6c->O;R<6<5IbB9&hz(dWG#( zyhW|s-9;U&cMJAj9%85OW00fypTf8edtLLHmKMSe00y$77bbdgi?H7i>q6@CLamTb z70umTZMBj>dsR8^8Qh%H;gwxlIoEP9!4>L4%R`E!9_Br?hwUxT-Zd1~*qz!n+}X)Y z;YH2HLp;BVo8;eoCC7~aPpD4lSO90eE|UX-QF#uOuN+m4mFaUIp0 zV4cg@kx+f6Q~W9qt|~0Mm(3~Y5?^pplx1Ta38#A-1~rC;1w z$;=XA(TMcu-(E=kPqk>ZHl-_QL9D@V0>ya*MU13$I8-CA6*iH5B+RLB0N%%G_w=Q< zQF{y6%0S=u3H}!CSum`TTs6Fh%aS%D=HRIx{;I!=aZWaE9*Lq{sykar%D+Kfet{nC zTE+GM6)Kx^d--NV9hm z#0EKIP_wU-fq-f`=kp}k$2`=I6K5QIQ604jE4>MA&LBKgQL?0-xT!>{w)iy2HZzgb zQ$p+|%!uwb55}f;RkY84N`mDV&QP5FDqDuz3W6~}4}w<0?(Au^X*25D!bfWvA%{Fy zSK+@4+pVhV8qA9vL~g^J04j#B<0#|SA=CULXe2U9BJ;@kbM^ddH!f#nTvLj;vv^Kf ztXEF6KxJ%+IN()?yaS^6Zp2+{R~ur9bC+!7_u4DdEp4N;wezP7ziwRPsjms~o&K%k z4-_@LP|jixwg^>8;CJ^Xs*{wx)u5!hnirlw(jb@Y+D?`{rukZFMt6P&uh?1Y7t02x zX|+_6(K4^}HFv@P01OrtOWl6l05D^=ey81sZ>Q&71;>J#=TJ8{8qMT3v7g*p+Tt~d zf4kp5r76x4c8d~=XyANLqj;NB@}s#iv=XpY0zB_>anqpSl22f{tlt#)Pe{}4E$!j+ z?scoElG@7BB`qWT+n#n@{G%KIPCa{(={n8ymRg;@nWILRn&i>!h9+#sWe~>cWj(XF zcFS?rtaxw1*4hI}cRY=Is@#wCo9VfdPUH{nu6qwb=}t4cP3RKSVwZ;eBMzVB+}_*5 zvfHPXZ2}PMY=Dvm><#h~OBXx>&9necSH@m7(>x_+B)eff+l<>;t9;ISZf`~RKZRv@ zv*J42GwFU4gZo1J?wM3(IF3Nhe~66zJ$){#Dj(hu8Q1QQt=9^X$-3cL;LPD7l{G_6&{ty_iIrYgoIoYuADYny#c#7#_ zvxx6Dw(Jx+^^#6A>4DR?dbM%l-4nyniw_Oit>wWxRbgC5yWI72)BJ0k)gsq5g_h@9 zn2#tct+Jdd1~>y90pxYb{HtzT`U8{cp{1#Kqe{277B+EtmdG%+cE2QK`n&Z%g<~bp zhagGI zR;jN|JH|-@2);!uMl#GfKTOlI8&+M&VDOBvi))Q8X1TeEiJCSW0vuy%u*p2T*r6-3%t+*8nxo=vOUUppvu)+>`%=V)O!A|C;0M&F{ySDR9b?0G(X27u zYB$$SxR$nSZ~y?de7)>=^em_OS0%2S%Z)YGW>kjiYgLWOA9HQN+}I=ohF&{Sq>52# z+>gPY5^oc0dM%2<9FQA%^$Wi=5E*y0X&E4b0rHe&9F63R;}o7YuuTG8KI=i6;te}c zwpk-XDPbE98Zt@eX+8R8y8i%#8iU;0UD;_eK)RHWM{Lh4;qBo_)Cm!Bha1dmyXD$yBm5va!fExqoFcnPgj;P={Hh zjyX(t!2~e}jN^`ny=m!!SGl{tO%wZNUtDvCga7ry^h0ho3=Xd3ifr+v&5&fr$b%ArNP=$4b))T zQF6{R@~Ba}Ca9`KZ>vVnV-3jMen|shf8Qi?U7oWmXnJJ&ZQCdIgvza|%zzEJM3YwYI-9#OE}G9uvwJ-xpR%97a6t`mj{eJ@gZvG1*A~}zcC%ixGFl)W zTAb!qB&b#61m$0zpCb;V6}#cxCVv&{>uw`yZJ|>vkt1zsLXWt;_a2-ARC9xhq2i59 zT+e9~QLF~q1@jdXZ|`t3$7}$j9fE<{wWg9e>QCQLj^hrIbEn;yq9m)7C^3&PF&j^Q zxd#>B>vt*U*=X}yktUyQd3ht_9Ec=%`Rl_d{Bc}2g&f~_lSylt3$#~p95*B?I-koK z{Hv+cn)6=quZpc=-88FdbsQ_6Pw!GOkHM?0&I#$*^5+_osGQ!hPw{Ba@b`-@n^Hu+ z!&`8L@%K+-n)C_ZF|W84<2F}<6F1sSl6jHgLaywbaC6$by)VQE{t>G=FE>dme(ge$ zxqu}>&-?`Q?0fNCbC2G7QjadjM{#>^r`rkc+^VyP<5g16E1rmoIc7eBvhH=QL+tH) zcO-fqmL&3{Z!BEh-AE595&{!FiOB=h*6)TqKC;+>-zo2i1qfpNdC_dPbS9M4$*55@Tl}8a!>om(-^OMT&U4P%IxcJJY1s+ zB7(tLK|_5bTwKVc{qm6k8u8hH&usOl=>8#^S-iH{aVN}53uc7;xZDBzxfur@{Y7xE zb-79;MrXMNLdJj@GB%K)1B4uHTyRlH$3RP`Y9(cs_fKU-VQ;~!6&71#_mY4QIEK-D{WKFhxfjf zE;Y2sUG-gffdAaK<)-1(B>IvR%aakDAO;{aNwp@da*v~c3>JK#08A16n1`mF`{W^Yi?N&Cre1aQWt603JV6VUdy@~X! z{{RtMcyYW#buOQ$M+73wCzgr=nF|4sy$1ug*0!T->ME5Q*&ah>5oSgwg~xjLKMzi$ zL$__;!Yh-~yftAJjm@Bg%yW{N9OE1e{uQU8&$C0=-^0c#=Mx`k&8?2M8_48+s(Gm- zog?9nE1S|8rM<|`JJzP80>{bbu&HPpVdCn$86F_Dis6i?11k(a_}7kjt43=}r;%5T z5x7XB;wLYBXM7C~uLRknu zpY;7}vQKHEV!q2Q_-g3OYW9qNXufnF-N)zr>t9t})-YVB@E(89*13r1{>|27pQ3Kd z06QKJ_}5_)3E{Z3Js56Z^Z8L1%P%82N10gc{vPWq72lHVK$!+UqyGS`Yu019%m8P# zeBI&e_p-Q-PLf;6$Uers-feE$#?B_*C6-kMV_r$f&uZ;+f``4(_`G&qh@LZ=g3?P; ze-P?o$4X}h63Dwye=}L$6SFqfJERzSPUD{5l~OAktzPI|YC)1Pa;U=}nC;MN z$j&qON3D4{oJX|LY<)&wfmljhx4V0@$Nm2ULfB|S{rshVq|OLyogBqxr-rMN{i4U~p4xC2u6 zG>xn=8w?2@DtKkfS4(&b(-MmW#4bKW^a zf2>D!)I4LSt#$W0a-sRa!i@CmRqkT6y|a!S10;10ElEhy6OEi%z}nhL}Bs?aM(=Eauzn06mu@vZ5{+BRoe zylEtJup>E)&K1~#dUUSaPqry!j~GxrF^c0MVzZd@yJ5w4`b5F>)khqT-n6B3ti_|$ znk!BVKUU`z>>eD`*H7^6ow|$`P4D$?PxG%Tff{Rw;~gEsjC-6{vjzzd9fuWzPEt_` zNkR^9pssi-ybM)sFtUbI_f((fQ>1{#OoO#&HR!ies>5?0N2PCCDls@PRb6>pudCfN zYA9UdD3r1K*OPeLT$c7p0B?|X2D{G|>V8du+rv1;c>cRNXk7Z#P>t6!JLY2)F|cBASA$dBT%R{?c9lObH4Wv2@z}?GvAmaJq4wYw7|74BBlE7NBuH+) z*4RzahQ{Sjy4dfEj#&J*%J7a!U#4lcay&A}Gy~=Y5s{IR?0VLe&|lrh5&JaK#=x>F zp4q+ zs!b}?+%hzSJmG!KCN5=d3EoUsF+{usO?rXTxT02dVl>-BAWr{2>T4pOkl;7y`wtLo2zoq{GY*>El{yrib zqdvaXzOg0SeEc7JNs>E%-N(Tntx{pB<~Gu{@U4}WE)GYfXls^+{{T--YS)QkA}ggG zK>!7Bp#$>=(zHBBrQ&bxD@FO2nO7Z0ulUsnb&u?&X<*v%Orc&k=C&vKMH(|skdnD)}{*P-*T=qM?zNOM*~lHf!orjOEYt=+1ZhwHb4)!sI>W-Ih^A@ z^v~^9u<;GG_TAn~=M^$;&c(*e=sqV4>#b8s@Xd*2x0D8Oyl&_J0Igb{9?>nL@jacj zv`AsJ8D>${vHms4_~XRKQ`IeXO9$Fvk6{6m}g^q&t&YL?5)vymi_hE3}u zMh-yP^K?IrPw{8MT0DLnYloiSM~!Y|xnUKzojvUGV+si%lbzfQjzQoG^40O}qIi$b zzKZ7V%2?hvj_&$NcE-__5rBnP7;JN&UiGy)acb#Yi8goopTsR2!g}%4JTGbes}0m5 zJE`aMQb%Pu-MQDC?&J^dj+JWj#c+7a(I<}jTYDY2lFAm_xrpRQ!;|a=20d%bkBD`@ z?5Li@PlgLeLJ&cxiGDyx<=im?A-bPhm%=)|)%S@VuglE$Yax^OU@6Btj@~no>D>3N z;TWf}l<&%PdY_B*uMZ!xX%@leW3-i0v7sKV-1Rsd^{#IIEqlaGJL^}m+FQb_AWZy) zSRJGR)N$9XcdaF?@%a}vu+11Cg^>dc`tw`|jI{A>ss=anHMGhMR@<_%sNksQwnjMV z^yj8Li`3`$V|&sXRAf^Be)vvXhn9Gu-Q5#V>VV)SB+w?TE2UDDoqH%M4s1 z^Zx+q8Lped)|zT4j^fTIE_STgMm0 z-9N(BOR_VRd6tJ=rAcZH)s&IQbv5~*U-{~zmIvPefr8SDp+YI=>sExWPTY*0A=hDRisbRr5(yl}LXnq9>j+^J8zmGwoJvZcFGohLb$Xj%5=Vj8{vm`1?)NwVf+bYqb}8P5HQ4up=|u zyoJntbC9I3AP&89Gnw%Q(hCnBPb81!+3JXw5ziSVRJTbu{{UzbeTm0WQw!fwHGA0f zKZn{S=8NDhPS3Z?(R!Sj1fqQXQ$g> zgKKJ-E5YOz8(aPcYlyd9h~`B0jz z$CbY(HUu7pKU(-KlbJ;EU)-;d^NRxN(( z!#+RMbt^Kvqn=`_239~z70*2~6#IS^%{7!NL;b!Uy*5dnKj1j`{6$R_t($FWuI>yZ zjoTr?{4&*7cQAC|=H&XT7c|{k=F$nFiptJMxgk;RAdTHePrShG9Zo<8juC~potVcuA@329IeVkThyGR_5CZq_-%Ee zP2)&^vb5XV8PYiHh#2Ze3292nU08GjZQ!K~LviR$F5r z>9rSvar`S&hK-@k+}3YX)O;hNuAiZ$@q-r^X}#uk+F35fbS68aup+1CcOO-a@zhs4 zrD@anvsZ7n%;h5{d%3vW&iw%Go<9s9r8v@?l&e(vBOZ99qPlL(%jGFJyNpH);K(Bw zS0sJlIsn{)2Sx{`Kpo$OuiH@3;{mg?~50I|rjDE1?t=~_uHT?ukhS4VLh2x4VDX=6k`F{qkFmP3v+QM`pdR5=xp zg>0E}yo!JC1fJB$+vGeA)nXmbAXQTBj9?7V1D#pG0Cn$E1F|qPQ=2QmP&KTJoB@@^ zbyhwWwbSg4=BnOfh0fl>rO6~;D`zJJQpqRoV|MxqnUX|rE$daUOb+CNLlES z1VGqTjdD&ae%95lqPM)gibc31EP)B<9eDSrFLhmnV%xGZuXSY7^+>JdOonA$$EI6> z{OgFly=ZRlZi};d&*j9fI8(o<{4#6Rq|z;&jIwFd%P}7*Xc*()r?o+E;a@)FMLwCn z*^I2r>4UkKYW`b^tQ3*g2g`e-pYXhXf7vWAZZUOok`|Eg6aqNU(ADiTMO3+;P8Sir z$u^-^&f#4yt>Gx`w8*5=t$)&Ca6!*FJU1q}?+y4{P>00P+})z?1OY^QkM z=P$FgotB3|W8s5oCz_0M2TI5B9=B(vpDuR!`+;5E?aEnLNi?m1tCL?o_`g&adW^D@ zxtXvAXr#TXyo;Ca{temKX`UjrzPj=v7~Rsk6tnwkv_NFzBir7%uL}5J{f)IZxlQmU z_l`bF4|?l-8L9oFMAh|)8~0JjVvKqOT%XT%JVU1nH;(&QnjG~jUSE0Uo-xoD_s<$_ z!x19?0M9lR=j$Yv*AX;;hL0ccHR=4g{427v)+V;pWg4p!9r4`E-2-RueLEliwPfr1 zL56uFLCS+9UhAG$@ay^3bla!i$g1{ntdfz-DY{c_9k={0T@K^@Wu4?vo(lvXpIT)8I?%O;kL>rYAC}vW z@K-+7Xm{>+!r&TJdNl6~Pr^jy< z()`2gS{GNgtuK@|oO{&`GsC)+_fH&fF(2P!fm`|ert=r5 z=~eF6_oQ>ytLe|N&T8;sAuak<6V!${N!og2v8zoQgOW4pTmBgEw7T=Nv-vO~!EAC* zwQFm>9@6wU<0~skBOkiPqUpO2RIXs?@iUUDGCeBWL-t#OanGe(uqIg$TSOI4dY4$z zgh}R`g)Dnkx+h~bsg=LY3?=!GU@ELSbQ@50i%UH-!g?3>&bAfikw=#s zb?2Z1*S%p+srZM*auw6y`z@*HP+)yCj(-Zp&iia_7sa4oxY8K&CCAmE(Y$xqK#QWzcWhI z>CP*)Az~2{{VPbu3pRJX|hUxfpcCp;cE?f zbxT{VUI7#_y0dO0BE6W+I%*1p?mEcq)M=Omv> z$*`U7-R@kSuEmsk3SSWYpGdV`9>Mc0=Q0ueBBbk$$ZkdV6W6g5f>5-xvbB?(c(@9zh9TijO zY|7N4hBby_xdq1jcOI2_JV!O>hUe36ZDU(8GfeQ_?QS$zt^;#TU+nC%22dzZb zP^j7FG-DXSQz_!Qoqv6v_5`Sb;bj zq;3^G$*I_?Gv#M9;q`0#m{QakC6$6~1CmX53*kr5wKy)dLXU6{?yaFx{$@rFPw?Y8 z+;{{6YoO9}FAw}R(q_B!QcHjjnR`DUf{w%3a!q+(jQ$~Ac#Y(?vu3uiWAn)0eqr?N zJ*Zcni(x)qeq@c`h`uE7&Z(q9X>N@+tpjXXko>W(bAyf#TvwaH1;M`$9Bi^iLu>;C zcL&y&PP(zQ)a|FZwrQ=RaLpO#%)!q-#;@wy@AkI8{i9`S@XYVbVYdg*7{)V6LB;aL zY24|AmoBVd6<7toiuq+SMW{w63| zFP#`8=2qy-^f(-QS2m6F9QW^v(hap{M53UOBQmz+$YIFA#c56fwT|{3su{LA02C?O z2Tc2CfB@b2RY>j3vAe6TQ}}Rlf6u*IwpkY1U`n)PtL!5szHAUZ`RVCaUD*`s!F)z+Y!5Z3VW!+(xCn$t-b2uTjb765K1^;I>s^#=iE0;& z{{Sj)i}j5a{6A%=o3)DS)kG?hAb&FhkVj*j{#BnlO)lnOSU@TlaBoN=DgQfnq5Ow7xymZCy?#lz>o$# zKfqS+goB^7OZ)|wZ@w}Q^sa^GXHq>;>e?oOBsx4gtmRrcKnWjGNjT=RJX_&w-C?4& zvUOe3E=kG2&30Y^yIog6lTMKCYj@nE(OaMAn(N`yZS5m0zy`)kp2E3gMXW+;h*parc)RC9T4(mzeto1djPEbCQ2r>Aod+$u49%R-Go; zQ~hHP;`Xi|!CFZ17lGX>(jc=@VQk0!Lh&e_HRnE8(p#!z}mu z&BOxlZWcFj@(gFEtv|)yHMNgUYfU!Y-bs*zy5F!ya7bmqW08PzKoyEpZKRE5Qadr< z5j=UPctXebVk^B}Ik&fBFpNe6G3+o;*VuDj2ckjZ?-BUcEq*%-olg4J_9?Czz#B+# zz#NbOz~Bxs?N^sru(7*n84+$|xpbQK*=Lqe$fb)J0d7k&J=6Q8cWq8L`~*20K?oX{aaM##6q=AI0wy9e>7&rBABrdS00rjtiJ2Od|Z* zc?u~zjtDsFa0PjPh;EaT}2qXY`2j(S!0CCef;1vh0t*aBWMtAn>J+fU|X~pAw{PLCnbo5po>kC@2)OBb^ zpo;9#vu%X{U{h-33;~mnc);NIs1L&}a^aL;>K2h)KS-KGBV?XO9A~dg)_$+x-xGK} zP28$HwdBnKAgK2Q1CHIrYbLL`jBKT&EW_dXA=B-3Eoxu0>C-a#j}gg*S+;I$a&UU6 z=K%DmzR?TG3|<-0q-{bO(b-`pWSLZxo;fGxI6j9Rg>U#bS<`i-h943`_Pcl`^F_Nh z5vCμ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 z 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 HZh!yTKRKVp literal 0 HcmV?d00001 diff --git a/website/blog/2024-01-25-AutoGenBench/index.mdx b/website/blog/2024-01-25-AutoGenBench/index.mdx new file mode 100644 index 000000000000..a1f34efeb28c --- /dev/null +++ b/website/blog/2024-01-25-AutoGenBench/index.mdx @@ -0,0 +1,131 @@ +--- +title: "AutoGenBench -- A Tool for Measuring and Evaluating AutoGen Agents" +authors: + - afourney + - qingyunwu +tags: [AutoGen] +--- + +![AutoGenBench](img/teaser.jpg) +