diff --git a/.github/workflows/contrib-openai.yml b/.github/workflows/contrib-openai.yml index b1b3e35e478f..7e8fb0033177 100644 --- a/.github/workflows/contrib-openai.yml +++ b/.github/workflows/contrib-openai.yml @@ -111,46 +111,7 @@ jobs: with: file: ./coverage.xml flags: unittests - CompressionTest: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.9"] - runs-on: ${{ matrix.os }} - environment: openai1 - steps: - # checkout to pr branch - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies - run: | - docker --version - python -m pip install --upgrade pip wheel - pip install -e . - python -c "import autogen" - pip install pytest-cov>=5 pytest-asyncio - - name: Install packages for test when needed - run: | - pip install docker - - name: Coverage - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} - AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} - OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} - run: | - pytest test/agentchat/contrib/test_compressible_agent.py - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests + GPTAssistantAgent: strategy: matrix: @@ -306,44 +267,7 @@ jobs: with: file: ./coverage.xml flags: unittests - ContextHandling: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.11"] - runs-on: ${{ matrix.os }} - environment: openai1 - steps: - # checkout to pr branch - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies - run: | - docker --version - python -m pip install --upgrade pip wheel - pip install -e . - python -c "import autogen" - pip install pytest-cov>=5 - - name: Coverage - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} - AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} - OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }} - BING_API_KEY: ${{ secrets.BING_API_KEY }} - run: | - pytest test/agentchat/contrib/capabilities/test_context_handling.py - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests + ImageGen: strategy: matrix: diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml index 7d8a932b0254..3abe257dfad6 100644 --- a/.github/workflows/contrib-tests.yml +++ b/.github/workflows/contrib-tests.yml @@ -9,6 +9,8 @@ on: paths: - "autogen/**" - "test/agentchat/contrib/**" + - "test/test_browser_utils.py" + - "test/test_retrieve_utils.py" - ".github/workflows/contrib-tests.yml" - "setup.py" @@ -85,6 +87,10 @@ jobs: --health-retries 5 ports: - 5432:5432 + mongodb: + image: mongodb/mongodb-atlas-local:latest + ports: + - 27017:27017 steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -102,6 +108,9 @@ jobs: - name: Install pgvector when on linux run: | pip install -e .[retrievechat-pgvector] + - name: Install mongodb when on linux + run: | + pip install -e .[retrievechat-mongodb] - name: Install unstructured when python-version is 3.9 and on linux if: matrix.python-version == '3.9' run: | @@ -154,41 +163,6 @@ jobs: file: ./coverage.xml flags: unittests - CompressionTest: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-2019] - python-version: ["3.10"] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies for all tests - run: | - python -m pip install --upgrade pip wheel - pip install pytest-cov>=5 - - name: Install packages and dependencies for Compression - run: | - pip install -e . - - name: Set AUTOGEN_USE_DOCKER based on OS - shell: bash - run: | - if [[ ${{ matrix.os }} != ubuntu-latest ]]; then - echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV - fi - - name: Coverage - run: | - pytest test/agentchat/contrib/test_compressible_agent.py --skip-openai - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests - GPTAssistantAgent: runs-on: ${{ matrix.os }} strategy: @@ -375,41 +349,6 @@ jobs: file: ./coverage.xml flags: unittests - ContextHandling: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-2019] - python-version: ["3.11"] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install packages and dependencies for all tests - run: | - python -m pip install --upgrade pip wheel - pip install pytest-cov>=5 - - name: Install packages and dependencies for Context Handling - run: | - pip install -e . - - name: Set AUTOGEN_USE_DOCKER based on OS - shell: bash - run: | - if [[ ${{ matrix.os }} != ubuntu-latest ]]; then - echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV - fi - - name: Coverage - run: | - pytest test/agentchat/contrib/capabilities/test_context_handling.py --skip-openai - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests - TransformMessages: runs-on: ${{ matrix.os }} strategy: @@ -476,7 +415,6 @@ jobs: file: ./coverage.xml flags: unittests - AnthropicTest: runs-on: ${{ matrix.os }} strategy: @@ -598,3 +536,119 @@ jobs: with: file: ./coverage.xml flags: unittests + + GroqTest: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-2019] + python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - os: macos-latest + python-version: "3.9" + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies for all tests + run: | + python -m pip install --upgrade pip wheel + pip install pytest-cov>=5 + - name: Install packages and dependencies for Groq + run: | + pip install -e .[groq,test] + - name: Set AUTOGEN_USE_DOCKER based on OS + shell: bash + run: | + if [[ ${{ matrix.os }} != ubuntu-latest ]]; then + echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV + fi + - name: Coverage + run: | + pytest test/oai/test_groq.py --skip-openai + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + + CohereTest: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies for all tests + run: | + python -m pip install --upgrade pip wheel + pip install pytest-cov>=5 + - name: Install packages and dependencies for Cohere + run: | + pip install -e .[cohere,test] + - name: Set AUTOGEN_USE_DOCKER based on OS + shell: bash + run: | + if [[ ${{ matrix.os }} != ubuntu-latest ]]; then + echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV + fi + - name: Coverage + run: | + pytest test/oai/test_cohere.py --skip-openai + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + + BedrockTest: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-2019] + python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - os: macos-latest + python-version: "3.9" + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install packages and dependencies for all tests + run: | + python -m pip install --upgrade pip wheel + pip install pytest-cov>=5 + - name: Install packages and dependencies for Amazon Bedrock + run: | + pip install -e .[boto3,test] + - name: Set AUTOGEN_USE_DOCKER based on OS + shell: bash + run: | + if [[ ${{ matrix.os }} != ubuntu-latest ]]; then + echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV + fi + - name: Coverage + run: | + pytest test/oai/test_bedrock.py --skip-openai + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index f4074b061693..6aac54d3818c 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -43,24 +43,47 @@ jobs: if: steps.filter.outputs.workflows == 'true' build: name: Dotnet Build - runs-on: ubuntu-latest needs: paths-filter if: needs.paths-filter.outputs.hasChanges == 'true' defaults: run: working-directory: dotnet + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest ] + python-version: ["3.11"] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: dotnet/global.json + dotnet-version: '8.0.x' - name: Restore dependencies run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config dotnet restore -bl + - name: Format check + run: | + echo "Format check" + echo "If you see any error in this step, please run 'dotnet format' locally to format the code." + dotnet format --verify-no-changes -v diag --no-restore - name: Build run: | echo "Build AutoGen" @@ -87,7 +110,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - global-json-file: dotnet/global.json + dotnet-version: '8.0.x' - name: publish AOT testApp, assert static analysis warning count, and run the app shell: pwsh @@ -105,6 +128,18 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -176,12 +211,14 @@ jobs: env: AZURE_ARTIFACTS_FEED_URL: https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/AutoGen/nuget/v3/index.json NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }} + continue-on-error: true - name: Publish nightly package to github package run: | echo "Publish nightly package to github package" echo "ls output directory" ls -R ./output/nightly dotnet nuget push --api-key ${{ secrets.GITHUB_TOKEN }} --source "https://nuget.pkg.github.com/microsoft/index.json" ./output/nightly/*.nupkg --skip-duplicate + continue-on-error: true - name: Publish nightly package to agentchat myget feed run: | echo "Publish nightly package to agentchat myget feed" @@ -190,3 +227,5 @@ jobs: dotnet nuget push --api-key ${{ secrets.MYGET_TOKEN }} --source "https://www.myget.org/F/agentchat/api/v3/index.json" ./output/nightly/*.nupkg --skip-duplicate env: MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} + continue-on-error: true + diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index 2877d058377b..23f4258a0e0c 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -29,10 +29,22 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: dotnet/global.json + dotnet-version: '8.0.x' - name: Restore dependencies run: | dotnet restore -bl diff --git a/CITATION.cff b/CITATION.cff index bc9a03f375a8..5e4c468067f7 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,7 @@ preferred-citation: given-names: "Qingyun" affiliation: "Penn State University, University Park PA USA" - family-names: "Bansal" - given-names: "Gargan" + given-names: "Gagan" affiliation: "Microsoft Research, Redmond WA USA" - family-names: "Zhang" given-names: "Jieyu" @@ -43,6 +43,7 @@ preferred-citation: - family-names: "Wang" given-names: "Chi" affiliation: "Microsoft Research, Redmond WA USA" - booktitle: "ArXiv preprint arXiv:2308.08155" + booktitle: "COLM" title: "AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation Framework" - year: 2023 + year: 2024 + url: "https://aka.ms/autogen-pdf" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000000..4726588453b4 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,43 @@ +# Contributors + +## Special thanks to all the people who help this project: +> These individuals dedicate their time and expertise to improve this project. We are deeply grateful for their contributions. + +| Name | GitHub Handle | Organization | Features | Roadmap Lead | Additional Information | +|---|---|---|---|---|---| +| Qingyun Wu | [qingyun-wu](https://github.com/qingyun-wu) | Penn State University | all, alt-models, autobuilder | Yes | Available most of the time (US Eastern Time) | +| Chi Wang | [sonichi](https://github.com/sonichi) | - | all | Yes | | +| Li Jiang | [thinkall](https://github.com/thinkall) | Microsoft | rag, autobuilder, group chat | Yes | [Issue #1657](https://github.com/microsoft/autogen/issues/1657) - Beijing, GMT+8 | +| Mark Sze | [marklysze](https://github.com/marklysze) | - | alt-models, group chat | No | Generally available (Sydney, AU time) - Group Chat "auto" speaker selection | +| Hrushikesh Dokala | [Hk669](https://github.com/Hk669) | - | alt-models, swebench, logging, rag | No | [Issue #2946](https://github.com/microsoft/autogen/issues/2946), [Pull Request #2933](https://github.com/microsoft/autogen/pull/2933) - Available most of the time (India, GMT+5:30) | +| Jiale Liu | [LeoLjl](https://github.com/LeoLjl) | Penn State University | autobuild, group chat | No | | +| Shaokun Zhang | [skzhang1](https://github.com/skzhang1) | Penn State University | AgentOptimizer, Teachability | Yes | [Issue #521](https://github.com/microsoft/autogen/issues/521) | +| Rajan Chari | [rajan-chari](https://github.com/rajan-chari) | Microsoft Research | CAP, Survey of other frameworks | No | | +| Victor Dibia | [victordibia](https://github.com/victordibia) | Microsoft Research | autogenstudio | Yes | [Issue #737](https://github.com/microsoft/autogen/issues/737) | +| Yixuan Zhai | [randombet](https://github.com/randombet) | Meta | group chat, sequential_chats, rag | No | | +| Xiaoyun Zhang | [LittleLittleCloud](https://github.com/LittleLittleCloud) | Microsoft | AutoGen.Net, group chat | Yes | [Backlog - AutoGen.Net](https://github.com/microsoft/autogen/issues) - Available most of the time (PST) | +| Yiran Wu | [yiranwu0](https://github.com/yiranwu0) | Penn State University | alt-models, group chat, logging | Yes | | +| Beibin Li | [BeibinLi](https://github.com/BeibinLi) | Microsoft Research | alt-models | Yes | | +| Gagan Bansal | [gagb](https://github.com/gagb) | Microsoft Research | All | | | +| Adam Fourney | [afourney](https://github.com/afourney) | Microsoft Research | Complex Tasks | | | +| Ricky Loynd | [rickyloynd-microsoft](https://github.com/rickyloynd-microsoft) | Microsoft Research | Teachability | | | +| Eric Zhu | [ekzhu](https://github.com/ekzhu) | Microsoft Research | All, Infra | | | +| Jack Gerrits | [jackgerrits](https://github.com/jackgerrits) | Microsoft Research | All, Infra | | | +| David Luong | [DavidLuong98](https://github.com/DavidLuong98) | Microsoft | AutoGen.Net | | | +| Davor Runje | [davorrunje](https://github.com/davorrunje) | airt.ai | Tool calling, IO | | Available most of the time (Central European Time) | +| Friederike Niedtner | [Friderike](https://www.microsoft.com/en-us/research/people/fniedtner/) | Microsoft Research | PM | | | +| Rafah Hosn | [Rafah](https://www.microsoft.com/en-us/research/people/raaboulh/) | Microsoft Research | PM | | | +| Robin Moeur | [Robin](https://www.linkedin.com/in/rmoeur/) | Microsoft Research | PM | | | +| Jingya Chen | [jingyachen](https://github.com/JingyaChen) | Microsoft | UX Design, AutoGen Studio | | | +| Suff Syed | [suffsyed](https://github.com/suffsyed) | Microsoft | UX Design, AutoGen Studio | | | + +## I would like to join this list. How can I help the project? +> We're always looking for new contributors to join our team and help improve the project. For more information, please refer to our [CONTRIBUTING](https://microsoft.github.io/autogen/docs/contributor-guide/contributing) guide. + + +## Are you missing from this list? +> Please open a PR to help us fix this. + + +## Acknowledgements +This template was adapted from [GitHub Template Guide](https://github.com/cezaraugusto/github-template-guidelines/blob/master/.github/CONTRIBUTORS.md) by [cezaraugusto](https://github.com/cezaraugusto). diff --git a/README.md b/README.md index 5bff3300a50e..8595bb60506c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,57 @@ -[![PyPI version](https://badge.fury.io/py/pyautogen.svg)](https://badge.fury.io/py/pyautogen) -[![Build](https://github.com/microsoft/autogen/actions/workflows/python-package.yml/badge.svg)](https://github.com/microsoft/autogen/actions/workflows/python-package.yml) -![Python Version](https://img.shields.io/badge/3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue) + +
+ +AutoGen Logo + +![Python Version](https://img.shields.io/badge/3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue) [![PyPI version](https://img.shields.io/badge/PyPI-v0.2.34-blue.svg)](https://pypi.org/project/pyautogen/) +[![NuGet version](https://badge.fury.io/nu/AutoGen.Core.svg)](https://badge.fury.io/nu/AutoGen.Core) + [![Downloads](https://static.pepy.tech/badge/pyautogen/week)](https://pepy.tech/project/pyautogen) [![Discord](https://img.shields.io/discord/1153072414184452236?logo=discord&style=flat)](https://aka.ms/autogen-dc) + [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40pyautogen)](https://twitter.com/pyautogen) -[![NuGet version](https://badge.fury.io/nu/AutoGen.Core.svg)](https://badge.fury.io/nu/AutoGen.Core) +
# AutoGen -[📚 Cite paper](#related-papers). - + +AutoGen is an open-source programming framework for building AI agents and facilitating cooperation among multiple agents to solve tasks. AutoGen aims to streamline the development and research of agentic AI, much like PyTorch does for Deep Learning. It offers features such as agents capable of interacting with each other, facilitates the use of various large language models (LLMs) and tool use support, autonomous and human-in-the-loop workflows, and multi-agent conversation patterns. + +> [!IMPORTANT] +> *Note for contributors and users*: [microsoft/autogen](https://aka.ms/autogen-gh) is the official repository of AutoGen project and it is under active development and maintenance under MIT license. We welcome contributions from developers and organizations worldwide. Our goal is to foster a collaborative and inclusive community where diverse perspectives and expertise can drive innovation and enhance the project's capabilities. We acknowledge the invaluable contributions from our existing contributors, as listed in [contributors.md](./CONTRIBUTORS.md). Whether you are an individual contributor or represent an organization, we invite you to join us in shaping the future of this project. For further information please also see [Microsoft open-source contributing guidelines](https://github.com/microsoft/autogen?tab=readme-ov-file#contributing). +> +> -_Maintainers (Sept 6th, 2024)_ + + +![AutoGen Overview](https://github.com/microsoft/autogen/blob/main/website/static/img/autogen_agentchat.png) + +- AutoGen enables building next-gen LLM applications based on [multi-agent conversations](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat) with minimal effort. It simplifies the orchestration, automation, and optimization of a complex LLM workflow. It maximizes the performance of LLM models and overcomes their weaknesses. +- It supports [diverse conversation patterns](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat#supporting-diverse-conversation-patterns) for complex workflows. With customizable and conversable agents, developers can use AutoGen to build a wide range of conversation patterns concerning conversation autonomy, + the number of agents, and agent conversation topology. +- It provides a collection of working systems with different complexities. These systems span a [wide range of applications](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat#diverse-applications-implemented-with-autogen) from various domains and complexities. This demonstrates how AutoGen can easily support diverse conversation patterns. +- AutoGen provides [enhanced LLM inference](https://microsoft.github.io/autogen/docs/Use-Cases/enhanced_inference#api-unification). It offers utilities like API unification and caching, and advanced usage patterns, such as error handling, multi-config inference, context programming, etc. + +AutoGen was created out of collaborative [research](https://microsoft.github.io/autogen/docs/Research) from Microsoft, Penn State University, and the University of Washington. + +

+ + ↑ Back to Top ↑ + +

+ + + +## News +
+ +Expand + +:fire: June 6, 2024: WIRED publishes a new article on AutoGen: [Chatbot Teamwork Makes the AI Dream Work](https://www.wired.com/story/chatbot-teamwork-makes-the-ai-dream-work/) based on interview with [Adam Fourney](https://github.com/afourney). + +:fire: June 4th, 2024: Microsoft Research Forum publishes new update and video on [AutoGen and Complex Tasks](https://www.microsoft.com/en-us/research/video/autogen-update-complex-tasks-and-agents/) presented by [Adam Fourney](https://github.com/afourney). + :fire: May 29, 2024: DeepLearning.ai launched a new short course [AI Agentic Design Patterns with AutoGen](https://www.deeplearning.ai/short-courses/ai-agentic-design-patterns-with-autogen), made in collaboration with Microsoft and Penn State University, and taught by AutoGen creators [Chi Wang](https://github.com/sonichi) and [Qingyun Wu](https://github.com/qingyun-wu). :fire: May 24, 2024: Foundation Capital published an article on [Forbes: The Promise of Multi-Agent AI](https://www.forbes.com/sites/joannechen/2024/05/24/the-promise-of-multi-agent-ai/?sh=2c1e4f454d97) and a video [AI in the Real World Episode 2: Exploring Multi-Agent AI and AutoGen with Chi Wang](https://www.youtube.com/watch?v=RLwyXRVvlNk). @@ -23,7 +60,7 @@ :fire: May 11, 2024: [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://openreview.net/pdf?id=uAjxFFing2) received the best paper award at the [ICLR 2024 LLM Agents Workshop](https://llmagents.github.io/). -:fire: Apr 26, 2024: [AutoGen.NET](https://microsoft.github.io/autogen-for-net/) is available for .NET developers! +:fire: Apr 26, 2024: [AutoGen.NET](https://microsoft.github.io/autogen-for-net/) is available for .NET developers! Thanks [XiaoYun Zhang](https://www.linkedin.com/in/xiaoyun-zhang-1b531013a/) :fire: Apr 17, 2024: Andrew Ng cited AutoGen in [The Batch newsletter](https://www.deeplearning.ai/the-batch/issue-245/) and [What's next for AI agentic workflows](https://youtu.be/sal78ACtGTc?si=JduUzN_1kDnMq0vF) at Sequoia Capital's AI Ascent (Mar 26). @@ -58,31 +95,7 @@ :fire: FLAML supports Code-First AutoML & Tuning – Private Preview in [Microsoft Fabric Data Science](https://learn.microsoft.com/en-us/fabric/data-science/). --> -

- - ↑ Back to Top ↑ - -

- -## What is AutoGen - -AutoGen is a framework that enables the development of LLM applications using multiple agents that can converse with each other to solve tasks. AutoGen agents are customizable, conversable, and seamlessly allow human participation. They can operate in various modes that employ combinations of LLMs, human inputs, and tools. - -![AutoGen Overview](https://github.com/microsoft/autogen/blob/main/website/static/img/autogen_agentchat.png) - -- AutoGen enables building next-gen LLM applications based on [multi-agent conversations](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat) with minimal effort. It simplifies the orchestration, automation, and optimization of a complex LLM workflow. It maximizes the performance of LLM models and overcomes their weaknesses. -- It supports [diverse conversation patterns](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat#supporting-diverse-conversation-patterns) for complex workflows. With customizable and conversable agents, developers can use AutoGen to build a wide range of conversation patterns concerning conversation autonomy, - the number of agents, and agent conversation topology. -- It provides a collection of working systems with different complexities. These systems span a [wide range of applications](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat#diverse-applications-implemented-with-autogen) from various domains and complexities. This demonstrates how AutoGen can easily support diverse conversation patterns. -- AutoGen provides [enhanced LLM inference](https://microsoft.github.io/autogen/docs/Use-Cases/enhanced_inference#api-unification). It offers utilities like API unification and caching, and advanced usage patterns, such as error handling, multi-config inference, context programming, etc. - -AutoGen is created out of collaborative [research](https://microsoft.github.io/autogen/docs/Research) from Microsoft, Penn State University, and the University of Washington. - -

- - ↑ Back to Top ↑ - -

+
## Roadmaps @@ -242,16 +255,25 @@ In addition, you can find: ## Related Papers -[AutoGen](https://arxiv.org/abs/2308.08155) +[AutoGen Studio](https://www.microsoft.com/en-us/research/publication/autogen-studio-a-no-code-developer-tool-for-building-and-debugging-multi-agent-systems/) + +``` +@inproceedings{dibia2024studio, + title={AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems}, + author={Victor Dibia and Jingya Chen and Gagan Bansal and Suff Syed and Adam Fourney and Erkang (Eric) Zhu and Chi Wang and Saleema Amershi}, + year={2024}, + booktitle={Pre-Print} +} +``` + +[AutoGen](https://aka.ms/autogen-pdf) ``` @inproceedings{wu2023autogen, title={AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation Framework}, author={Qingyun Wu and Gagan Bansal and Jieyu Zhang and Yiran Wu and Beibin Li and Erkang Zhu and Li Jiang and Xiaoyun Zhang and Shaokun Zhang and Jiale Liu and Ahmed Hassan Awadallah and Ryen W White and Doug Burger and Chi Wang}, - year={2023}, - eprint={2308.08155}, - archivePrefix={arXiv}, - primaryClass={cs.AI} + year={2024}, + booktitle={COLM}, } ``` @@ -288,6 +310,16 @@ In addition, you can find: } ``` +[StateFlow](https://arxiv.org/abs/2403.11322) +``` +@article{wu2024stateflow, + title={StateFlow: Enhancing LLM Task-Solving through State-Driven Workflows}, + author={Wu, Yiran and Yue, Tianwei and Zhang, Shaokun and Wang, Chi and Wu, Qingyun}, + journal={arXiv preprint arXiv:2403.11322}, + year={2024} +} +``` +

↑ Back to Top ↑ @@ -339,7 +371,7 @@ may be either trademarks or registered trademarks of Microsoft in the United Sta The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. -Privacy information can be found at https://privacy.microsoft.com/en-us/ +Privacy information can be found at https://go.microsoft.com/fwlink/?LinkId=521839 Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel, or otherwise. diff --git a/TRANSPARENCY_FAQS.md b/TRANSPARENCY_FAQS.md index 206af084748b..addf29d8b8d3 100644 --- a/TRANSPARENCY_FAQS.md +++ b/TRANSPARENCY_FAQS.md @@ -31,6 +31,8 @@ While AutoGen automates LLM workflows, decisions about how to use specific LLM o - Current version of AutoGen was evaluated on six applications to illustrate its potential in simplifying the development of high-performance multi-agent applications. These applications are selected based on their real-world relevance, problem difficulty and problem solving capabilities enabled by AutoGen, and innovative potential. - These applications involve using AutoGen to solve math problems, question answering, decision making in text world environments, supply chain optimization, etc. For each of these domains AutoGen was evaluated on various success based metrics (i.e., how often the AutoGen based implementation solved the task). And, in some cases, AutoGen based approach was also evaluated on implementation efficiency (e.g., to track reductions in developer effort to build). More details can be found at: https://aka.ms/AutoGen/TechReport - The team has conducted tests where a “red” agent attempts to get the default AutoGen assistant to break from its alignment and guardrails. The team has observed that out of 70 attempts to break guardrails, only 1 was successful in producing text that would have been flagged as problematic by Azure OpenAI filters. The team has not observed any evidence that AutoGen (or GPT models as hosted by OpenAI or Azure) can produce novel code exploits or jailbreak prompts, since direct prompts to “be a hacker”, “write exploits”, or “produce a phishing email” are refused by existing filters. +- We also evaluated [a team of AutoGen agents](https://github.com/microsoft/autogen/tree/gaia_multiagent_v01_march_1st/samples/tools/autogenbench/scenarios/GAIA/Templates/Orchestrator) on the [GAIA benchmarks](https://arxiv.org/abs/2311.12983), and got [SOTA results](https://huggingface.co/spaces/gaia-benchmark/leaderboard) as of + March 1, 2024. ## What are the limitations of AutoGen? How can users minimize the impact of AutoGen’s limitations when using the system? AutoGen relies on existing LLMs. Experimenting with AutoGen would retain common limitations of large language models; including: diff --git a/autogen/agentchat/chat.py b/autogen/agentchat/chat.py index 97411e9fc004..d07b4d15cb62 100644 --- a/autogen/agentchat/chat.py +++ b/autogen/agentchat/chat.py @@ -107,6 +107,15 @@ def __find_async_chat_order(chat_ids: Set[int], prerequisites: List[Prerequisite return chat_order +def _post_process_carryover_item(carryover_item): + if isinstance(carryover_item, str): + return carryover_item + elif isinstance(carryover_item, dict) and "content" in carryover_item: + return str(carryover_item["content"]) + else: + return str(carryover_item) + + def __post_carryover_processing(chat_info: Dict[str, Any]) -> None: iostream = IOStream.get_default() @@ -116,7 +125,7 @@ def __post_carryover_processing(chat_info: Dict[str, Any]) -> None: UserWarning, ) print_carryover = ( - ("\n").join([t for t in chat_info["carryover"]]) + ("\n").join([_post_process_carryover_item(t) for t in chat_info["carryover"]]) if isinstance(chat_info["carryover"], list) else chat_info["carryover"] ) @@ -153,7 +162,7 @@ def initiate_chats(chat_queue: List[Dict[str, Any]]) -> List[ChatResult]: For example: - `"sender"` - the sender agent. - `"recipient"` - the recipient agent. - - `"clear_history" (bool) - whether to clear the chat history with the agent. + - `"clear_history"` (bool) - whether to clear the chat history with the agent. Default is True. - `"silent"` (bool or None) - (Experimental) whether to print the messages in this conversation. Default is False. diff --git a/autogen/agentchat/contrib/agent_builder.py b/autogen/agentchat/contrib/agent_builder.py index c9a2d79607dd..430017d13fc9 100644 --- a/autogen/agentchat/contrib/agent_builder.py +++ b/autogen/agentchat/contrib/agent_builder.py @@ -103,7 +103,7 @@ class AgentBuilder: """ AGENT_NAME_PROMPT = """# Your task -Suggest no more then {max_agents} experts with their name according to the following user requirement. +Suggest no more than {max_agents} experts with their name according to the following user requirement. ## User requirement {task} diff --git a/autogen/agentchat/contrib/agent_eval/README.md b/autogen/agentchat/contrib/agent_eval/README.md index 6588a1ec6113..478f28fd74ec 100644 --- a/autogen/agentchat/contrib/agent_eval/README.md +++ b/autogen/agentchat/contrib/agent_eval/README.md @@ -1,7 +1,9 @@ -Agents for running the AgentEval pipeline. +Agents for running the [AgentEval](https://microsoft.github.io/autogen/blog/2023/11/20/AgentEval/) pipeline. AgentEval is a process for evaluating a LLM-based system's performance on a given task. When given a task to evaluate and a few example runs, the critic and subcritic agents create evaluation criteria for evaluating a system's solution. Once the criteria has been created, the quantifier agent can evaluate subsequent task solutions based on the generated criteria. For more information see: [AgentEval Integration Roadmap](https://github.com/microsoft/autogen/issues/2162) + +See our [blog post](https://microsoft.github.io/autogen/blog/2024/06/21/AgentEval) for usage examples and general explanations. diff --git a/autogen/agentchat/contrib/capabilities/context_handling.py b/autogen/agentchat/contrib/capabilities/context_handling.py deleted file mode 100644 index 44b10259f1b7..000000000000 --- a/autogen/agentchat/contrib/capabilities/context_handling.py +++ /dev/null @@ -1,138 +0,0 @@ -import sys -from typing import Dict, List, Optional -from warnings import warn - -import tiktoken -from termcolor import colored - -from autogen import ConversableAgent, token_count_utils - -warn( - "Context handling with TransformChatHistory is deprecated and will be removed in `0.2.30`. " - "Please use `TransformMessages`, documentation can be found at https://microsoft.github.io/autogen/docs/topics/handling_long_contexts/intro_to_transform_messages", - DeprecationWarning, - stacklevel=2, -) - - -class TransformChatHistory: - """ - An agent's chat history with other agents is a common context that it uses to generate a reply. - This capability allows the agent to transform its chat history prior to using it to generate a reply. - It does not permanently modify the chat history, but rather processes it on every invocation. - - This capability class enables various strategies to transform chat history, such as: - - Truncate messages: Truncate each message to first maximum number of tokens. - - Limit number of messages: Truncate the chat history to a maximum number of (recent) messages. - - Limit number of tokens: Truncate the chat history to number of recent N messages that fit in - maximum number of tokens. - Note that the system message, because of its special significance, is always kept as is. - - The three strategies can be combined. For example, when each of these parameters are specified - they are used in the following order: - 1. First truncate messages to a maximum number of tokens - 2. Second, it limits the number of message to keep - 3. Third, it limits the total number of tokens in the chat history - - When adding this capability to an agent, the following are modified: - - A hook is added to the hookable method `process_all_messages_before_reply` to transform the - received messages for possible truncation. - Not modifying the stored message history. - """ - - def __init__( - self, - *, - max_tokens_per_message: Optional[int] = None, - max_messages: Optional[int] = None, - max_tokens: Optional[int] = None, - ): - """ - Args: - max_tokens_per_message (Optional[int]): Maximum number of tokens to keep in each message. - max_messages (Optional[int]): Maximum number of messages to keep in the context. - max_tokens (Optional[int]): Maximum number of tokens to keep in the context. - """ - self.max_tokens_per_message = max_tokens_per_message if max_tokens_per_message else sys.maxsize - self.max_messages = max_messages if max_messages else sys.maxsize - self.max_tokens = max_tokens if max_tokens else sys.maxsize - - def add_to_agent(self, agent: ConversableAgent): - """ - Adds TransformChatHistory capability to the given agent. - """ - agent.register_hook(hookable_method="process_all_messages_before_reply", hook=self._transform_messages) - - def _transform_messages(self, messages: List[Dict]) -> List[Dict]: - """ - Args: - messages: List of messages to process. - - Returns: - List of messages with the first system message and the last max_messages messages, - ensuring each message does not exceed max_tokens_per_message. - """ - temp_messages = messages.copy() - processed_messages = [] - system_message = None - processed_messages_tokens = 0 - - if messages[0]["role"] == "system": - system_message = messages[0].copy() - temp_messages.pop(0) - - total_tokens = sum( - token_count_utils.count_token(msg["content"]) for msg in temp_messages - ) # Calculate tokens for all messages - - # Truncate each message's content to a maximum token limit of each message - - # Process recent messages first - for msg in reversed(temp_messages[-self.max_messages :]): - msg["content"] = truncate_str_to_tokens(msg["content"], self.max_tokens_per_message) - msg_tokens = token_count_utils.count_token(msg["content"]) - if processed_messages_tokens + msg_tokens > self.max_tokens: - break - # append the message to the beginning of the list to preserve order - processed_messages = [msg] + processed_messages - processed_messages_tokens += msg_tokens - if system_message: - processed_messages.insert(0, system_message) - # Optionally, log the number of truncated messages and tokens if needed - num_truncated = len(messages) - len(processed_messages) - - if num_truncated > 0 or total_tokens > processed_messages_tokens: - print( - colored( - f"Truncated {num_truncated} messages. Reduced from {len(messages)} to {len(processed_messages)}.", - "yellow", - ) - ) - print( - colored( - f"Truncated {total_tokens - processed_messages_tokens} tokens. Tokens reduced from {total_tokens} to {processed_messages_tokens}", - "yellow", - ) - ) - return processed_messages - - -def truncate_str_to_tokens(text: str, max_tokens: int, model: str = "gpt-3.5-turbo-0613") -> str: - """Truncate a string so that the number of tokens is less than or equal to max_tokens using tiktoken. - - Args: - text: The string to truncate. - max_tokens: The maximum number of tokens to keep. - model: The target OpenAI model for tokenization alignment. - - Returns: - The truncated string. - """ - - encoding = tiktoken.encoding_for_model(model) # Get the appropriate tokenizer - - encoded_tokens = encoding.encode(text) - truncated_tokens = encoded_tokens[:max_tokens] - truncated_text = encoding.decode(truncated_tokens) # Decode back to text - - return truncated_text diff --git a/autogen/agentchat/contrib/capabilities/transform_messages.py b/autogen/agentchat/contrib/capabilities/transform_messages.py index e96dc39fa7bc..1ce219bdadfa 100644 --- a/autogen/agentchat/contrib/capabilities/transform_messages.py +++ b/autogen/agentchat/contrib/capabilities/transform_messages.py @@ -1,9 +1,8 @@ import copy from typing import Dict, List -from autogen import ConversableAgent - from ....formatting_utils import colored +from ...conversable_agent import ConversableAgent from .transforms import MessageTransform diff --git a/autogen/agentchat/contrib/capabilities/transforms.py b/autogen/agentchat/contrib/capabilities/transforms.py index dad3fc335edf..d9ad365b91b3 100644 --- a/autogen/agentchat/contrib/capabilities/transforms.py +++ b/autogen/agentchat/contrib/capabilities/transforms.py @@ -53,13 +53,16 @@ class MessageHistoryLimiter: It trims the conversation history by removing older messages, retaining only the most recent messages. """ - def __init__(self, max_messages: Optional[int] = None): + def __init__(self, max_messages: Optional[int] = None, keep_first_message: bool = False): """ Args: max_messages Optional[int]: Maximum number of messages to keep in the context. Must be greater than 0 if not None. + keep_first_message bool: Whether to keep the original first message in the conversation history. + Defaults to False. """ self._validate_max_messages(max_messages) self._max_messages = max_messages + self._keep_first_message = keep_first_message def apply_transform(self, messages: List[Dict]) -> List[Dict]: """Truncates the conversation history to the specified maximum number of messages. @@ -75,10 +78,31 @@ def apply_transform(self, messages: List[Dict]) -> List[Dict]: List[Dict]: A new list containing the most recent messages up to the specified maximum. """ - if self._max_messages is None: + if self._max_messages is None or len(messages) <= self._max_messages: return messages - return messages[-self._max_messages :] + truncated_messages = [] + remaining_count = self._max_messages + + # Start with the first message if we need to keep it + if self._keep_first_message: + truncated_messages = [messages[0]] + remaining_count -= 1 + + # Loop through messages in reverse + for i in range(len(messages) - 1, 0, -1): + if remaining_count > 1: + truncated_messages.insert(1 if self._keep_first_message else 0, messages[i]) + if remaining_count == 1: + # If there's only 1 slot left and it's a 'tools' message, ignore it. + if messages[i].get("role") != "tool": + truncated_messages.insert(1, messages[i]) + + remaining_count -= 1 + if remaining_count == 0: + break + + return truncated_messages def get_logs(self, pre_transform_messages: List[Dict], post_transform_messages: List[Dict]) -> Tuple[str, bool]: pre_transform_messages_len = len(pre_transform_messages) @@ -421,3 +445,95 @@ def _compress_text(self, text: str) -> Tuple[str, int]: def _validate_min_tokens(self, min_tokens: Optional[int]): if min_tokens is not None and min_tokens <= 0: raise ValueError("min_tokens must be greater than 0 or None") + + +class TextMessageContentName: + """A transform for including the agent's name in the content of a message.""" + + def __init__( + self, + position: str = "start", + format_string: str = "{name}:\n", + deduplicate: bool = True, + filter_dict: Optional[Dict] = None, + exclude_filter: bool = True, + ): + """ + Args: + position (str): The position to add the name to the content. The possible options are 'start' or 'end'. Defaults to 'start'. + format_string (str): The f-string to format the message name with. Use '{name}' as a placeholder for the agent's name. Defaults to '{name}:\n' and must contain '{name}'. + deduplicate (bool): Whether to deduplicate the formatted string so it doesn't appear twice (sometimes the LLM will add it to new messages itself). Defaults to True. + filter_dict (None or dict): A dictionary to filter out messages that you want/don't want to compress. + If None, no filters will be applied. + exclude_filter (bool): If exclude filter is True (the default value), messages that match the filter will be + excluded from compression. If False, messages that match the filter will be compressed. + """ + + assert isinstance(position, str) and position is not None + assert position in ["start", "end"] + assert isinstance(format_string, str) and format_string is not None + assert "{name}" in format_string + assert isinstance(deduplicate, bool) and deduplicate is not None + + self._position = position + self._format_string = format_string + self._deduplicate = deduplicate + self._filter_dict = filter_dict + self._exclude_filter = exclude_filter + + # Track the number of messages changed for logging + self._messages_changed = 0 + + def apply_transform(self, messages: List[Dict]) -> List[Dict]: + """Applies the name change to the message based on the position and format string. + + Args: + messages (List[Dict]): A list of message dictionaries. + + Returns: + List[Dict]: A list of dictionaries with the message content updated with names. + """ + # Make sure there is at least one message + if not messages: + return messages + + messages_changed = 0 + processed_messages = copy.deepcopy(messages) + for message in processed_messages: + # Some messages may not have content. + if not transforms_util.is_content_right_type( + message.get("content") + ) or not transforms_util.is_content_right_type(message.get("name")): + continue + + if not transforms_util.should_transform_message(message, self._filter_dict, self._exclude_filter): + continue + + if transforms_util.is_content_text_empty(message["content"]) or transforms_util.is_content_text_empty( + message["name"] + ): + continue + + # Get and format the name in the content + content = message["content"] + formatted_name = self._format_string.format(name=message["name"]) + + if self._position == "start": + if not self._deduplicate or not content.startswith(formatted_name): + message["content"] = f"{formatted_name}{content}" + + messages_changed += 1 + else: + if not self._deduplicate or not content.endswith(formatted_name): + message["content"] = f"{content}{formatted_name}" + + messages_changed += 1 + + self._messages_changed = messages_changed + return processed_messages + + def get_logs(self, pre_transform_messages: List[Dict], post_transform_messages: List[Dict]) -> Tuple[str, bool]: + if self._messages_changed > 0: + return f"{self._messages_changed} message(s) changed to incorporate name.", True + else: + return "No messages changed to incorporate name.", False diff --git a/autogen/agentchat/contrib/compressible_agent.py b/autogen/agentchat/contrib/compressible_agent.py deleted file mode 100644 index bea4058b94ac..000000000000 --- a/autogen/agentchat/contrib/compressible_agent.py +++ /dev/null @@ -1,436 +0,0 @@ -import copy -import inspect -import logging -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union -from warnings import warn - -from autogen import Agent, ConversableAgent, OpenAIWrapper -from autogen.token_count_utils import count_token, get_max_token_limit, num_tokens_from_functions - -from ...formatting_utils import colored - -logger = logging.getLogger(__name__) - -warn( - "Context handling with CompressibleAgent is deprecated and will be removed in `0.2.30`. " - "Please use `TransformMessages`, documentation can be found at https://microsoft.github.io/autogen/docs/topics/handling_long_contexts/intro_to_transform_messages", - DeprecationWarning, - stacklevel=2, -) - - -class CompressibleAgent(ConversableAgent): - """CompressibleAgent agent. While this agent retains all the default functionalities of the `AssistantAgent`, - it also provides the added feature of compression when activated through the `compress_config` setting. - - `compress_config` is set to False by default, making this agent equivalent to the `AssistantAgent`. - This agent does not work well in a GroupChat: The compressed messages will not be sent to all the agents in the group. - The default system message is the same as AssistantAgent. - `human_input_mode` is default to "NEVER" - and `code_execution_config` is default to False. - This agent doesn't execute code or function call by default. - """ - - DEFAULT_SYSTEM_MESSAGE = """You are a helpful AI assistant. -Solve tasks using your coding and language skills. -In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. - 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. - 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. -Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. -When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. -If you want the user to save the code in a file before executing it, put # filename: inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. -If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. -When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. -Reply "TERMINATE" in the end when everything is done. - """ - DEFAULT_COMPRESS_CONFIG = { - "mode": "TERMINATE", - "compress_function": None, - "trigger_count": 0.7, - "async": False, - "broadcast": True, - "verbose": False, - "leave_last_n": 2, - } - - def __init__( - self, - name: str, - system_message: Optional[str] = DEFAULT_SYSTEM_MESSAGE, - is_termination_msg: Optional[Callable[[Dict], bool]] = None, - max_consecutive_auto_reply: Optional[int] = None, - human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "NEVER", - function_map: Optional[Dict[str, Callable]] = None, - code_execution_config: Optional[Union[Dict, bool]] = False, - llm_config: Optional[Union[Dict, bool]] = None, - default_auto_reply: Optional[Union[str, Dict, None]] = "", - compress_config: Optional[Dict] = False, - description: Optional[str] = None, - **kwargs, - ): - """ - Args: - name (str): agent name. - system_message (str): system message for the ChatCompletion inference. - Please override this attribute if you want to reprogram the agent. - llm_config (dict): llm inference configuration. - Note: you must set `model` in llm_config. It will be used to compute the token count. - Please refer to [OpenAIWrapper.create](/docs/reference/oai/client#create) - for available options. - is_termination_msg (function): a function that takes a message in the form of a dictionary - and returns a boolean value indicating if this received message is a termination message. - The dict can contain the following keys: "content", "role", "name", "function_call". - max_consecutive_auto_reply (int): the maximum number of consecutive auto replies. - default to None (no limit provided, class attribute MAX_CONSECUTIVE_AUTO_REPLY will be used as the limit in this case). - The limit only plays a role when human_input_mode is not "ALWAYS". - compress_config (dict or True/False): config for compression before oai_reply. Default to False. - You should contain the following keys: - - "mode" (Optional, str, default to "TERMINATE"): Choose from ["COMPRESS", "TERMINATE", "CUSTOMIZED"]. - 1. `TERMINATE`: terminate the conversation ONLY when token count exceeds the max limit of current model. `trigger_count` is NOT used in this mode. - 2. `COMPRESS`: compress the messages when the token count exceeds the limit. - 3. `CUSTOMIZED`: pass in a customized function to compress the messages. - - "compress_function" (Optional, callable, default to None): Must be provided when mode is "CUSTOMIZED". - The function should takes a list of messages and returns a tuple of (is_compress_success: bool, compressed_messages: List[Dict]). - - "trigger_count" (Optional, float, int, default to 0.7): the threshold to trigger compression. - If a float between (0, 1], it is the percentage of token used. if a int, it is the number of tokens used. - - "async" (Optional, bool, default to False): whether to compress asynchronously. - - "broadcast" (Optional, bool, default to True): whether to update the compressed message history to sender. - - "verbose" (Optional, bool, default to False): Whether to print the content before and after compression. Used when mode="COMPRESS". - - "leave_last_n" (Optional, int, default to 0): If provided, the last n messages will not be compressed. Used when mode="COMPRESS". - description (str): a short description of the agent. This description is used by other agents - (e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message) - **kwargs (dict): Please refer to other kwargs in - [ConversableAgent](../conversable_agent#__init__). - """ - super().__init__( - name=name, - system_message=system_message, - is_termination_msg=is_termination_msg, - max_consecutive_auto_reply=max_consecutive_auto_reply, - human_input_mode=human_input_mode, - function_map=function_map, - code_execution_config=code_execution_config, - llm_config=llm_config, - default_auto_reply=default_auto_reply, - description=description, - **kwargs, - ) - - self._set_compress_config(compress_config) - - # create a separate client for compression. - if llm_config is False: - self.llm_compress_config = False - self.compress_client = None - else: - if "model" not in llm_config: - raise ValueError("llm_config must contain the 'model' field.") - self.llm_compress_config = self.llm_config.copy() - # remove functions - if "functions" in self.llm_compress_config: - del self.llm_compress_config["functions"] - self.compress_client = OpenAIWrapper(**self.llm_compress_config) - - self._reply_func_list.clear() - self.register_reply([Agent, None], ConversableAgent.generate_oai_reply) - self.register_reply([Agent], CompressibleAgent.on_oai_token_limit) # check token limit - self.register_reply([Agent, None], ConversableAgent.generate_code_execution_reply) - self.register_reply([Agent, None], ConversableAgent.generate_function_call_reply) - self.register_reply([Agent, None], ConversableAgent.check_termination_and_human_reply) - - def _set_compress_config(self, compress_config: Optional[Dict] = False): - if compress_config: - if compress_config is True: - compress_config = {} - if not isinstance(compress_config, dict): - raise ValueError("compress_config must be a dict or True/False.") - - allowed_modes = ["COMPRESS", "TERMINATE", "CUSTOMIZED"] - if compress_config.get("mode", "TERMINATE") not in allowed_modes: - raise ValueError(f"Invalid compression mode. Allowed values are: {', '.join(allowed_modes)}") - - self.compress_config = self.DEFAULT_COMPRESS_CONFIG.copy() - self.compress_config.update(compress_config) - - if not isinstance(self.compress_config["leave_last_n"], int) or self.compress_config["leave_last_n"] < 0: - raise ValueError("leave_last_n must be a non-negative integer.") - - # convert trigger_count to int, default to 0.7 - trigger_count = self.compress_config["trigger_count"] - if not (isinstance(trigger_count, int) or isinstance(trigger_count, float)) or trigger_count <= 0: - raise ValueError("trigger_count must be a positive number.") - if isinstance(trigger_count, float) and 0 < trigger_count <= 1: - self.compress_config["trigger_count"] = int( - trigger_count * get_max_token_limit(self.llm_config["model"]) - ) - trigger_count = self.compress_config["trigger_count"] - init_count = self._compute_init_token_count() - if trigger_count < init_count: - print( - f"Warning: trigger_count {trigger_count} is less than the initial token count {init_count} (system message + function description if passed), compression will be disabled. Please increase trigger_count if you want to enable compression." - ) - self.compress_config = False - - if self.compress_config["mode"] == "CUSTOMIZED" and self.compress_config["compress_function"] is None: - raise ValueError("compress_function must be provided when mode is CUSTOMIZED.") - if self.compress_config["mode"] != "CUSTOMIZED" and self.compress_config["compress_function"] is not None: - print("Warning: compress_function is provided but mode is not 'CUSTOMIZED'.") - - else: - self.compress_config = False - - def generate_reply( - self, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - exclude: Optional[List[Callable]] = None, - ) -> Union[str, Dict, None]: - """ - - Adding to line 202: - ``` - if messages is not None and messages != self._oai_messages[sender]: - messages = self._oai_messages[sender] - ``` - """ - if all((messages is None, sender is None)): - error_msg = f"Either {messages=} or {sender=} must be provided." - logger.error(error_msg) - raise AssertionError(error_msg) - - if messages is None: - messages = self._oai_messages[sender] - - for reply_func_tuple in self._reply_func_list: - reply_func = reply_func_tuple["reply_func"] - if exclude and reply_func in exclude: - continue - if inspect.iscoroutinefunction(reply_func): - continue - if self._match_trigger(reply_func_tuple["trigger"], sender): - final, reply = reply_func(self, messages=messages, sender=sender, config=reply_func_tuple["config"]) - if messages is not None and sender is not None and messages != self._oai_messages[sender]: - messages = self._oai_messages[sender] - if final: - return reply - return self._default_auto_reply - - def _compute_init_token_count(self): - """Check if the agent is LLM-based and compute the initial token count.""" - if self.llm_config is False: - return 0 - - func_count = 0 - if "functions" in self.llm_config: - func_count = num_tokens_from_functions(self.llm_config["functions"], self.llm_config["model"]) - - return func_count + count_token(self._oai_system_message, self.llm_config["model"]) - - def _manage_history_on_token_limit(self, messages, token_used, max_token_allowed, model): - """Manage the message history with different modes when token limit is reached. - Return: - final (bool): whether to terminate the agent. - compressed_messages (List[Dict]): the compressed messages. None if no compression or compression failed. - """ - # 1. mode = "TERMINATE", terminate the agent if no token left. - if self.compress_config["mode"] == "TERMINATE": - if max_token_allowed - token_used <= 0: - # Terminate if no token left. - print( - colored( - f'Warning: Terminate Agent "{self.name}" due to no token left for oai reply. max token for {model}: {max_token_allowed}, existing token count: {token_used}', - "yellow", - ), - flush=True, - ) - return True, None - return False, None - - # if token_used is less than trigger_count, no compression will be used. - if token_used < self.compress_config["trigger_count"]: - return False, None - - # 2. mode = "COMPRESS" or mode = "CUSTOMIZED", compress the messages - copied_messages = copy.deepcopy(messages) - if self.compress_config["mode"] == "COMPRESS": - _, compress_messages = self.compress_messages(copied_messages) - elif self.compress_config["mode"] == "CUSTOMIZED": - _, compress_messages = self.compress_config["compress_function"](copied_messages) - else: - raise ValueError(f"Unknown compression mode: {self.compress_config['mode']}") - - if compress_messages is not None: - for i in range(len(compress_messages)): - compress_messages[i] = self._get_valid_oai_message(compress_messages[i]) - return False, compress_messages - - def _get_valid_oai_message(self, message): - """Convert a message into a valid OpenAI ChatCompletion message.""" - oai_message = {k: message[k] for k in ("content", "function_call", "name", "context", "role") if k in message} - if "content" not in oai_message: - if "function_call" in oai_message: - oai_message["content"] = None # if only function_call is provided, content will be set to None. - else: - raise ValueError( - "Message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." - ) - if "function_call" in oai_message: - oai_message["role"] = "assistant" # only messages with role 'assistant' can have a function call. - oai_message["function_call"] = dict(oai_message["function_call"]) - return oai_message - - def _print_compress_info(self, init_token_count, token_used, token_after_compression): - to_print = "Token Count (including {} tokens from system msg and function descriptions). Before compression : {} | After: {}".format( - init_token_count, - token_used, - token_after_compression, - ) - print(colored(to_print, "magenta"), flush=True) - print("-" * 80, flush=True) - - def on_oai_token_limit( - self, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - config: Optional[Any] = None, - ) -> Tuple[bool, Union[str, Dict, None]]: - """(Experimental) Compress previous messages when a threshold of tokens is reached. - - TODO: async compress - TODO: maintain a list for old oai messages (messages before compression) - """ - llm_config = self.llm_config if config is None else config - if self.compress_config is False: - return False, None - if messages is None: - messages = self._oai_messages[sender] - - model = llm_config["model"] - init_token_count = self._compute_init_token_count() - token_used = init_token_count + count_token(messages, model) - final, compressed_messages = self._manage_history_on_token_limit( - messages, token_used, get_max_token_limit(model), model - ) - - # update message history with compressed messages - if compressed_messages is not None: - self._print_compress_info( - init_token_count, token_used, count_token(compressed_messages, model) + init_token_count - ) - self._oai_messages[sender] = compressed_messages - if self.compress_config["broadcast"]: - # update the compressed message history to sender - sender._oai_messages[self] = copy.deepcopy(compressed_messages) - # switching the role of the messages for the sender - for i in range(len(sender._oai_messages[self])): - cmsg = sender._oai_messages[self][i] - if "function_call" in cmsg or cmsg["role"] == "user": - cmsg["role"] = "assistant" - elif cmsg["role"] == "assistant": - cmsg["role"] = "user" - sender._oai_messages[self][i] = cmsg - - # successfully compressed, return False, None for generate_oai_reply to be called with the updated messages - return False, None - return final, None - - def compress_messages( - self, - messages: Optional[List[Dict]] = None, - config: Optional[Any] = None, - ) -> Tuple[bool, Union[str, Dict, None, List]]: - """Compress a list of messages into one message. - - The first message (the initial prompt) will not be compressed. - The rest of the messages will be compressed into one message, the model is asked to distinguish the role of each message: USER, ASSISTANT, FUNCTION_CALL, FUNCTION_RETURN. - Check out the compress_sys_msg. - - TODO: model used in compression agent is different from assistant agent: For example, if original model used by is gpt-4; we start compressing at 70% of usage, 70% of 8092 = 5664; and we use gpt 3.5 here max_toke = 4096, it will raise error. choosinng model automatically? - """ - # 1. use the compression client - client = self.compress_client if config is None else config - - # 2. stop if there is only one message in the list - leave_last_n = self.compress_config.get("leave_last_n", 0) - if leave_last_n + 1 >= len(messages): - logger.warning( - f"Warning: Compression skipped at trigger count threshold. The first msg and last {leave_last_n} msgs will not be compressed. current msg count: {len(messages)}. Consider raising trigger_count." - ) - return False, None - - # 3. put all history into one, except the first one - if self.compress_config["verbose"]: - print(colored("*" * 30 + "Start compressing the following content:" + "*" * 30, "magenta"), flush=True) - - compressed_prompt = "Below is the compressed content from the previous conversation, evaluate the process and continue if necessary:\n" - chat_to_compress = "To be compressed:\n" - - for m in messages[1 : len(messages) - leave_last_n]: # 0, 1, 2, 3, 4 - # Handle function role - if m.get("role") == "function": - chat_to_compress += f"##FUNCTION_RETURN## (from function \"{m['name']}\"): \n{m['content']}\n" - - # If name exists in the message - elif "name" in m: - chat_to_compress += f"##{m['name']}({m['role'].upper()})## {m['content']}\n" - - # Handle case where content is not None and name is absent - elif m.get("content"): # This condition will also handle None and empty string - if compressed_prompt in m["content"]: - chat_to_compress += m["content"].replace(compressed_prompt, "") + "\n" - else: - chat_to_compress += f"##{m['role'].upper()}## {m['content']}\n" - - # Handle function_call in the message - if "function_call" in m: - function_name = m["function_call"].get("name") - function_args = m["function_call"].get("arguments") - - if not function_name or not function_args: - chat_to_compress += f"##FUNCTION_CALL## {m['function_call']}\n" - else: - chat_to_compress += f"##FUNCTION_CALL## \nName: {function_name}\nArgs: {function_args}\n" - - chat_to_compress = [{"role": "user", "content": chat_to_compress}] - - if self.compress_config["verbose"]: - print(chat_to_compress[0]["content"]) - - # 4. use LLM to compress - compress_sys_msg = """You are a helpful assistant that will summarize and compress conversation history. -Rules: -1. Please summarize each of the message and reserve the exact titles: ##USER##, ##ASSISTANT##, ##FUNCTION_CALL##, ##FUNCTION_RETURN##, ##SYSTEM##, ##()## (e.g. ##Bob(ASSISTANT)##). -2. Try to compress the content but reserve important information (a link, a specific number, etc.). -3. Use words to summarize the code blocks or functions calls (##FUNCTION_CALL##) and their goals. For code blocks, please use ##CODE## to mark it. -4. For returns from functions (##FUNCTION_RETURN##) or returns from code execution: summarize the content and indicate the status of the return (e.g. success, error, etc.). -""" - try: - response = client.create( - context=None, - messages=[{"role": "system", "content": compress_sys_msg}] + chat_to_compress, - ) - except Exception as e: - print(colored(f"Failed to compress the content due to {e}", "red"), flush=True) - return False, None - - compressed_message = self.client.extract_text_or_completion_object(response)[0] - assert isinstance(compressed_message, str), f"compressed_message should be a string: {compressed_message}" - if self.compress_config["verbose"]: - print( - colored("*" * 30 + "Content after compressing:" + "*" * 30, "magenta"), - flush=True, - ) - print(compressed_message, colored("\n" + "*" * 80, "magenta")) - - # 5. add compressed message to the first message and return - return ( - True, - [ - messages[0], - { - "content": compressed_prompt + compressed_message, - "role": "system", - }, - ] - + messages[len(messages) - leave_last_n :], - ) diff --git a/autogen/agentchat/contrib/gpt_assistant_agent.py b/autogen/agentchat/contrib/gpt_assistant_agent.py index 0dcad27b16d5..244f5ed81894 100644 --- a/autogen/agentchat/contrib/gpt_assistant_agent.py +++ b/autogen/agentchat/contrib/gpt_assistant_agent.py @@ -209,10 +209,12 @@ def _invoke_assistant( for message in pending_messages: if message["content"].strip() == "": continue + # Convert message roles to 'user' or 'assistant', by calling _map_role_for_api, to comply with OpenAI API spec + api_role = self._map_role_for_api(message["role"]) self._openai_client.beta.threads.messages.create( thread_id=assistant_thread.id, content=message["content"], - role=message["role"], + role=api_role, ) # Create a new run to get responses from the assistant @@ -240,6 +242,28 @@ def _invoke_assistant( self._unread_index[sender] = len(self._oai_messages[sender]) + 1 return True, response + def _map_role_for_api(self, role: str) -> str: + """ + Maps internal message roles to the roles expected by the OpenAI Assistant API. + + Args: + role (str): The role from the internal message. + + Returns: + str: The mapped role suitable for the API. + """ + if role in ["function", "tool"]: + return "assistant" + elif role == "system": + return "system" + elif role == "user": + return "user" + elif role == "assistant": + return "assistant" + else: + # Default to 'assistant' for any other roles not recognized by the API + return "assistant" + def _get_run_response(self, thread, run): """ Waits for and processes the response of a run from the OpenAI assistant. diff --git a/autogen/agentchat/contrib/graph_rag/__init__.py b/autogen/agentchat/contrib/graph_rag/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogen/agentchat/contrib/graph_rag/document.py b/autogen/agentchat/contrib/graph_rag/document.py new file mode 100644 index 000000000000..9730269c7ab6 --- /dev/null +++ b/autogen/agentchat/contrib/graph_rag/document.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from enum import Enum, auto +from typing import Optional + + +class DocumentType(Enum): + """ + Enum for supporting document type. + """ + + TEXT = auto() + HTML = auto() + PDF = auto() + + +@dataclass +class Document: + """ + A wrapper of graph store query results. + """ + + doctype: DocumentType + data: Optional[object] = None + path_or_url: Optional[str] = "" diff --git a/autogen/agentchat/contrib/graph_rag/graph_query_engine.py b/autogen/agentchat/contrib/graph_rag/graph_query_engine.py new file mode 100644 index 000000000000..28ef6ede84a6 --- /dev/null +++ b/autogen/agentchat/contrib/graph_rag/graph_query_engine.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass, field +from typing import List, Optional, Protocol + +from .document import Document + + +@dataclass +class GraphStoreQueryResult: + """ + A wrapper of graph store query results. + + answer: human readable answer to question/query. + results: intermediate results to question/query, e.g. node entities. + """ + + answer: Optional[str] = None + results: list = field(default_factory=list) + + +class GraphQueryEngine(Protocol): + """An abstract base class that represents a graph query engine on top of a underlying graph database. + + This interface defines the basic methods for graph rag. + """ + + def init_db(self, input_doc: List[Document] | None = None): + """ + This method initializes graph database with the input documents or records. + Usually, it takes the following steps, + 1. connecting to a graph database. + 2. extract graph nodes, edges based on input data, graph schema and etc. + 3. build indexes etc. + + Args: + input_doc: a list of input documents that are used to build the graph in database. + + Returns: GraphStore + """ + pass + + def add_records(self, new_records: List) -> bool: + """ + Add new records to the underlying database and add to the graph if required. + """ + pass + + def query(self, question: str, n_results: int = 1, **kwargs) -> GraphStoreQueryResult: + """ + This method transform a string format question into database query and return the result. + """ + pass diff --git a/autogen/agentchat/contrib/graph_rag/graph_rag_capability.py b/autogen/agentchat/contrib/graph_rag/graph_rag_capability.py new file mode 100644 index 000000000000..b6412305e069 --- /dev/null +++ b/autogen/agentchat/contrib/graph_rag/graph_rag_capability.py @@ -0,0 +1,56 @@ +from autogen.agentchat.contrib.capabilities.agent_capability import AgentCapability +from autogen.agentchat.conversable_agent import ConversableAgent + +from .graph_query_engine import GraphQueryEngine + + +class GraphRagCapability(AgentCapability): + """ + A graph rag capability uses a graph query engine to give a conversable agent the graph rag ability. + + An agent class with graph rag capability could + 1. create a graph in the underlying database with input documents. + 2. retrieved relevant information based on messages received by the agent. + 3. generate answers from retrieved information and send messages back. + + For example, + graph_query_engine = GraphQueryEngine(...) + graph_query_engine.init_db([Document(doc1), Document(doc2), ...]) + + graph_rag_agent = ConversableAgent( + name="graph_rag_agent", + max_consecutive_auto_reply=3, + ... + ) + graph_rag_capability = GraphRagCapbility(graph_query_engine) + graph_rag_capability.add_to_agent(graph_rag_agent) + + user_proxy = UserProxyAgent( + name="user_proxy", + code_execution_config=False, + is_termination_msg=lambda msg: "TERMINATE" in msg["content"], + human_input_mode="ALWAYS", + ) + user_proxy.initiate_chat(graph_rag_agent, message="Name a few actors who've played in 'The Matrix'") + + # ChatResult( + # chat_id=None, + # chat_history=[ + # {'content': 'Name a few actors who've played in \'The Matrix\'', 'role': 'graph_rag_agent'}, + # {'content': 'A few actors who have played in The Matrix are: + # - Keanu Reeves + # - Laurence Fishburne + # - Carrie-Anne Moss + # - Hugo Weaving', + # 'role': 'user_proxy'}, + # ...) + + """ + + def __init__(self, query_engine: GraphQueryEngine): + """ + initialize graph rag capability with a graph query engine + """ + ... + + def add_to_agent(self, agent: ConversableAgent): ... diff --git a/autogen/agentchat/contrib/llamaindex_conversable_agent.py b/autogen/agentchat/contrib/llamaindex_conversable_agent.py index f7a9c3e615dc..dbf6f274ae87 100644 --- a/autogen/agentchat/contrib/llamaindex_conversable_agent.py +++ b/autogen/agentchat/contrib/llamaindex_conversable_agent.py @@ -8,15 +8,14 @@ try: from llama_index.core.agent.runner.base import AgentRunner + from llama_index.core.base.llms.types import ChatMessage from llama_index.core.chat_engine.types import AgentChatResponse - from llama_index_client import ChatMessage except ImportError as e: logger.fatal("Failed to import llama-index. Try running 'pip install llama-index'") raise e class LLamaIndexConversableAgent(ConversableAgent): - def __init__( self, name: str, diff --git a/autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py b/autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py index ea81de6dff11..f1cc6947d50e 100644 --- a/autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py +++ b/autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py @@ -1,3 +1,4 @@ +import warnings from typing import Callable, Dict, List, Literal, Optional from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent @@ -93,6 +94,11 @@ def __init__( **kwargs (dict): other kwargs in [UserProxyAgent](../user_proxy_agent#__init__). """ + warnings.warn( + "The QdrantRetrieveUserProxyAgent is deprecated. Please use the RetrieveUserProxyAgent instead, set `vector_db` to `qdrant`.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(name, human_input_mode, is_termination_msg, retrieve_config, **kwargs) self._client = self._retrieve_config.get("client", QdrantClient(":memory:")) self._embedding_model = self._retrieve_config.get("embedding_model", "BAAI/bge-small-en-v1.5") diff --git a/autogen/agentchat/contrib/retrieve_assistant_agent.py b/autogen/agentchat/contrib/retrieve_assistant_agent.py index 9b5ace200dc6..173bc4432e78 100644 --- a/autogen/agentchat/contrib/retrieve_assistant_agent.py +++ b/autogen/agentchat/contrib/retrieve_assistant_agent.py @@ -1,3 +1,4 @@ +import warnings from typing import Any, Dict, List, Optional, Tuple, Union from autogen.agentchat.agent import Agent @@ -16,6 +17,11 @@ class RetrieveAssistantAgent(AssistantAgent): """ def __init__(self, *args, **kwargs): + warnings.warn( + "The RetrieveAssistantAgent is deprecated. Please use the AssistantAgent instead.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(*args, **kwargs) self.register_reply(Agent, RetrieveAssistantAgent._generate_retrieve_assistant_reply) diff --git a/autogen/agentchat/contrib/retrieve_user_proxy_agent.py b/autogen/agentchat/contrib/retrieve_user_proxy_agent.py index 59a4abccb1d6..b247d7a158f6 100644 --- a/autogen/agentchat/contrib/retrieve_user_proxy_agent.py +++ b/autogen/agentchat/contrib/retrieve_user_proxy_agent.py @@ -1,6 +1,7 @@ import hashlib import os import re +import uuid from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union from IPython import get_ipython @@ -135,7 +136,7 @@ def __init__( - `client` (Optional, chromadb.Client) - the chromadb client. If key not provided, a default client `chromadb.Client()` will be used. If you want to use other vector db, extend this class and override the `retrieve_docs` function. - **Deprecated**: use `vector_db` instead. + *[Deprecated]* use `vector_db` instead. - `docs_path` (Optional, Union[str, List[str]]) - the path to the docs directory. It can also be the path to a single file, the url to a single file or a list of directories, files and urls. Default is None, which works only if the @@ -149,7 +150,7 @@ def __init__( By default, "extra_docs" is set to false, starting document IDs from zero. This poses a risk as new documents might overwrite existing ones, potentially causing unintended loss or alteration of data in the collection. - **Deprecated**: use `new_docs` when use `vector_db` instead of `client`. + *[Deprecated]* use `new_docs` when use `vector_db` instead of `client`. - `new_docs` (Optional, bool) - when True, only adds new documents to the collection; when False, updates existing documents and adds new ones. Default is True. Document id is used to determine if a document is new or existing. By default, the @@ -172,12 +173,12 @@ def __init__( models can be found at `https://www.sbert.net/docs/pretrained_models.html`. The default model is a fast model. If you want to use a high performance model, `all-mpnet-base-v2` is recommended. - **Deprecated**: no need when use `vector_db` instead of `client`. + *[Deprecated]* no need when use `vector_db` instead of `client`. - `embedding_function` (Optional, Callable) - the embedding function for creating the vector db. Default is None, SentenceTransformer with the given `embedding_model` will be used. If you want to use OpenAI, Cohere, HuggingFace or other embedding functions, you can pass it here, - follow the examples in `https://docs.trychroma.com/embeddings`. + follow the examples in `https://docs.trychroma.com/guides/embeddings`. - `customized_prompt` (Optional, str) - the customized prompt for the retrieve chat. Default is None. - `customized_answer_prefix` (Optional, str) - the customized answer prefix for the @@ -188,7 +189,7 @@ def __init__( interactive retrieval. Default is True. - `collection_name` (Optional, str) - the name of the collection. If key not provided, a default name `autogen-docs` will be used. - - `get_or_create` (Optional, bool) - Whether to get the collection if it exists. Default is True. + - `get_or_create` (Optional, bool) - Whether to get the collection if it exists. Default is False. - `overwrite` (Optional, bool) - Whether to overwrite the collection if it exists. Default is False. Case 1. if the collection does not exist, create the collection. Case 2. the collection exists, if overwrite is True, it will overwrite the collection. @@ -219,7 +220,7 @@ def __init__( Example of overriding retrieve_docs - If you have set up a customized vector db, and it's not compatible with chromadb, you can easily plug in it with below code. - **Deprecated**: Use `vector_db` instead. You can extend VectorDB and pass it to the agent. + *[Deprecated]* use `vector_db` instead. You can extend VectorDB and pass it to the agent. ```python class MyRetrieveUserProxyAgent(RetrieveUserProxyAgent): def query_vector_db( @@ -305,6 +306,10 @@ def retrieve_docs(self, problem: str, n_results: int = 20, search_string: str = self._db_config["embedding_function"] = self._embedding_function self._vector_db = VectorDBFactory.create_vector_db(db_type=self._vector_db, **self._db_config) self.register_reply(Agent, RetrieveUserProxyAgent._generate_retrieve_user_reply, position=2) + self.register_hook( + hookable_method="process_message_before_send", + hook=self._check_update_context_before_send, + ) def _init_db(self): if not self._vector_db: @@ -365,7 +370,11 @@ def _init_db(self): else: all_docs_ids = set() - chunk_ids = [hashlib.blake2b(chunk.encode("utf-8")).hexdigest()[:HASH_LENGTH] for chunk in chunks] + chunk_ids = ( + [hashlib.blake2b(chunk.encode("utf-8")).hexdigest()[:HASH_LENGTH] for chunk in chunks] + if not self._vector_db.type == "qdrant" + else [str(uuid.UUID(hex=hashlib.md5(chunk.encode("utf-8")).hexdigest())) for chunk in chunks] + ) chunk_ids_set = set(chunk_ids) chunk_ids_set_idx = [chunk_ids.index(hash_value) for hash_value in chunk_ids_set] docs = [ @@ -395,6 +404,34 @@ def _is_termination_msg_retrievechat(self, message): update_context_case1, update_context_case2 = self._check_update_context(message) return not (contain_code or update_context_case1 or update_context_case2) + def _check_update_context_before_send(self, sender, message, recipient, silent): + if not isinstance(message, (str, dict)): + return message + elif isinstance(message, dict): + msg_text = message.get("content", message) + else: + msg_text = message + + if "UPDATE CONTEXT" == msg_text.strip().upper(): + doc_contents = self._get_context(self._results) + + # Always use self.problem as the query text to retrieve docs, but each time we replace the context with the + # next similar docs in the retrieved doc results. + if not doc_contents: + for _tmp_retrieve_count in range(1, 5): + self._reset(intermediate=True) + self.retrieve_docs( + self.problem, self.n_results * (2 * _tmp_retrieve_count + 1), self._search_string + ) + doc_contents = self._get_context(self._results) + if doc_contents or self.n_results * (2 * _tmp_retrieve_count + 1) >= len(self._results[0]): + break + msg_text = self._generate_message(doc_contents, task=self._task) + + if isinstance(message, dict): + message["content"] = msg_text + return message + @staticmethod def get_max_tokens(model="gpt-3.5-turbo"): if "32k" in model: @@ -514,7 +551,7 @@ def _generate_retrieve_user_reply( self.problem, self.n_results * (2 * _tmp_retrieve_count + 1), self._search_string ) doc_contents = self._get_context(self._results) - if doc_contents: + if doc_contents or self.n_results * (2 * _tmp_retrieve_count + 1) >= len(self._results[0]): break elif update_context_case2: # Use the current intermediate info as the query text to retrieve docs, and each time we append the top similar @@ -526,7 +563,7 @@ def _generate_retrieve_user_reply( ) self._get_context(self._results) doc_contents = "\n".join(self._doc_contents) # + "\n" + "\n".join(self._intermediate_answers) - if doc_contents: + if doc_contents or self.n_results * (2 * _tmp_retrieve_count + 1) >= len(self._results[0]): break self.clear_history() diff --git a/autogen/agentchat/contrib/society_of_mind_agent.py b/autogen/agentchat/contrib/society_of_mind_agent.py index 2f6be5088a4d..e76768187c9f 100644 --- a/autogen/agentchat/contrib/society_of_mind_agent.py +++ b/autogen/agentchat/contrib/society_of_mind_agent.py @@ -39,6 +39,7 @@ def __init__( code_execution_config: Union[Dict, Literal[False]] = False, llm_config: Optional[Union[Dict, Literal[False]]] = False, default_auto_reply: Optional[Union[str, Dict, None]] = "", + **kwargs, ): super().__init__( name=name, @@ -50,6 +51,7 @@ def __init__( code_execution_config=code_execution_config, llm_config=llm_config, default_auto_reply=default_auto_reply, + **kwargs, ) self.update_chat_manager(chat_manager) diff --git a/autogen/agentchat/contrib/vectordb/base.py b/autogen/agentchat/contrib/vectordb/base.py index 29a080086193..d7d49d6200ca 100644 --- a/autogen/agentchat/contrib/vectordb/base.py +++ b/autogen/agentchat/contrib/vectordb/base.py @@ -1,4 +1,16 @@ -from typing import Any, List, Mapping, Optional, Protocol, Sequence, Tuple, TypedDict, Union, runtime_checkable +from typing import ( + Any, + Callable, + List, + Mapping, + Optional, + Protocol, + Sequence, + Tuple, + TypedDict, + Union, + runtime_checkable, +) Metadata = Union[Mapping[str, Any], None] Vector = Union[Sequence[float], Sequence[int]] @@ -49,6 +61,9 @@ class VectorDB(Protocol): active_collection: Any = None type: str = "" + embedding_function: Optional[Callable[[List[str]], List[List[float]]]] = ( + None # embeddings = embedding_function(sentences) + ) def create_collection(self, collection_name: str, overwrite: bool = False, get_or_create: bool = True) -> Any: """ @@ -171,7 +186,8 @@ def get_docs_by_ids( ids: List[ItemID] | A list of document ids. If None, will return all the documents. Default is None. collection_name: str | The name of the collection. Default is None. include: List[str] | The fields to include. Default is None. - If None, will include ["metadatas", "documents"], ids will always be included. + If None, will include ["metadatas", "documents"], ids will always be included. This may differ + depending on the implementation. kwargs: dict | Additional keyword arguments. Returns: @@ -185,7 +201,7 @@ class VectorDBFactory: Factory class for creating vector databases. """ - PREDEFINED_VECTOR_DB = ["chroma", "pgvector"] + PREDEFINED_VECTOR_DB = ["chroma", "pgvector", "mongodb", "qdrant"] @staticmethod def create_vector_db(db_type: str, **kwargs) -> VectorDB: @@ -207,6 +223,14 @@ def create_vector_db(db_type: str, **kwargs) -> VectorDB: from .pgvectordb import PGVectorDB return PGVectorDB(**kwargs) + if db_type.lower() in ["mdb", "mongodb", "atlas"]: + from .mongodb import MongoDBAtlasVectorDB + + return MongoDBAtlasVectorDB(**kwargs) + if db_type.lower() in ["qdrant", "qdrantdb"]: + from .qdrant import QdrantVectorDB + + return QdrantVectorDB(**kwargs) else: raise ValueError( f"Unsupported vector database type: {db_type}. Valid types are {VectorDBFactory.PREDEFINED_VECTOR_DB}." diff --git a/autogen/agentchat/contrib/vectordb/chromadb.py b/autogen/agentchat/contrib/vectordb/chromadb.py index 1ed8708409d3..bef4a1090219 100644 --- a/autogen/agentchat/contrib/vectordb/chromadb.py +++ b/autogen/agentchat/contrib/vectordb/chromadb.py @@ -14,6 +14,11 @@ except ImportError: raise ImportError("Please install chromadb: `pip install chromadb`") +try: + from chromadb.errors import ChromaError +except ImportError: + ChromaError = Exception + CHROMADB_MAX_BATCH_SIZE = os.environ.get("CHROMADB_MAX_BATCH_SIZE", 40000) logger = get_logger(__name__) @@ -84,7 +89,7 @@ def create_collection( collection = self.active_collection else: collection = self.client.get_collection(collection_name, embedding_function=self.embedding_function) - except ValueError: + except (ValueError, ChromaError): collection = None if collection is None: return self.client.create_collection( diff --git a/autogen/agentchat/contrib/vectordb/mongodb.py b/autogen/agentchat/contrib/vectordb/mongodb.py new file mode 100644 index 000000000000..2e0580fe826b --- /dev/null +++ b/autogen/agentchat/contrib/vectordb/mongodb.py @@ -0,0 +1,553 @@ +from copy import deepcopy +from time import monotonic, sleep +from typing import Any, Callable, Dict, Iterable, List, Literal, Mapping, Set, Tuple, Union + +import numpy as np +from pymongo import MongoClient, UpdateOne, errors +from pymongo.collection import Collection +from pymongo.driver_info import DriverInfo +from pymongo.operations import SearchIndexModel +from sentence_transformers import SentenceTransformer + +from .base import Document, ItemID, QueryResults, VectorDB +from .utils import get_logger + +logger = get_logger(__name__) + +DEFAULT_INSERT_BATCH_SIZE = 100_000 +_SAMPLE_SENTENCE = ["The weather is lovely today in paradise."] +_DELAY = 0.5 + + +def with_id_rename(docs: Iterable) -> List[Dict[str, Any]]: + """Utility changes _id field from Collection into id for Document.""" + return [{**{k: v for k, v in d.items() if k != "_id"}, "id": d["_id"]} for d in docs] + + +class MongoDBAtlasVectorDB(VectorDB): + """ + A Collection object for MongoDB. + """ + + def __init__( + self, + connection_string: str = "", + database_name: str = "vector_db", + embedding_function: Callable = SentenceTransformer("all-MiniLM-L6-v2").encode, + collection_name: str = None, + index_name: str = "vector_index", + overwrite: bool = False, + wait_until_index_ready: float = None, + wait_until_document_ready: float = None, + ): + """ + Initialize the vector database. + + Args: + connection_string: str | The MongoDB connection string to connect to. Default is ''. + database_name: str | The name of the database. Default is 'vector_db'. + embedding_function: Callable | The embedding function used to generate the vector representation. + collection_name: str | The name of the collection to create for this vector database + Defaults to None + index_name: str | Index name for the vector database, defaults to 'vector_index' + overwrite: bool = False + wait_until_index_ready: float | None | Blocking call to wait until the + database indexes are ready. None, the default, means no wait. + wait_until_document_ready: float | None | Blocking call to wait until the + database indexes are ready. None, the default, means no wait. + """ + self.embedding_function = embedding_function + self.index_name = index_name + self._wait_until_index_ready = wait_until_index_ready + self._wait_until_document_ready = wait_until_document_ready + + # This will get the model dimension size by computing the embeddings dimensions + self.dimensions = self._get_embedding_size() + + try: + self.client = MongoClient(connection_string, driver=DriverInfo(name="autogen")) + self.client.admin.command("ping") + logger.debug("Successfully created MongoClient") + except errors.ServerSelectionTimeoutError as err: + raise ConnectionError("Could not connect to MongoDB server") from err + + self.db = self.client[database_name] + logger.debug(f"Atlas Database name: {self.db.name}") + if collection_name: + self.active_collection = self.create_collection(collection_name, overwrite) + else: + self.active_collection = None + + def _is_index_ready(self, collection: Collection, index_name: str): + """Check for the index name in the list of available search indexes to see if the + specified index is of status READY + + Args: + collection (Collection): MongoDB Collection to for the search indexes + index_name (str): Vector Search Index name + + Returns: + bool : True if the index is present and READY false otherwise + """ + for index in collection.list_search_indexes(index_name): + if index["type"] == "vectorSearch" and index["status"] == "READY": + return True + return False + + def _wait_for_index(self, collection: Collection, index_name: str, action: str = "create"): + """Waits for the index action to be completed. Otherwise throws a TimeoutError. + + Timeout set on instantiation. + action: "create" or "delete" + """ + assert action in ["create", "delete"], f"{action=} must be create or delete." + start = monotonic() + while monotonic() - start < self._wait_until_index_ready: + if action == "create" and self._is_index_ready(collection, index_name): + return + elif action == "delete" and len(list(collection.list_search_indexes())) == 0: + return + sleep(_DELAY) + + raise TimeoutError(f"Index {self.index_name} is not ready!") + + def _wait_for_document(self, collection: Collection, index_name: str, doc: Document): + start = monotonic() + while monotonic() - start < self._wait_until_document_ready: + query_result = _vector_search( + embedding_vector=np.array(self.embedding_function(doc["content"])).tolist(), + n_results=1, + collection=collection, + index_name=index_name, + ) + if query_result and query_result[0][0]["_id"] == doc["id"]: + return + sleep(_DELAY) + + raise TimeoutError(f"Document {self.index_name} is not ready!") + + def _get_embedding_size(self): + return len(self.embedding_function(_SAMPLE_SENTENCE)[0]) + + def list_collections(self): + """ + List the collections in the vector database. + + Returns: + List[str] | The list of collections. + """ + return self.db.list_collection_names() + + def create_collection( + self, + collection_name: str, + overwrite: bool = False, + get_or_create: bool = True, + ) -> Collection: + """ + Create a collection in the vector database and create a vector search index in the collection. + + Args: + collection_name: str | The name of the collection. + overwrite: bool | Whether to overwrite the collection if it exists. Default is False. + get_or_create: bool | Whether to get or create the collection. Default is True + """ + if overwrite: + self.delete_collection(collection_name) + + if collection_name not in self.db.list_collection_names(): + # Create a new collection + coll = self.db.create_collection(collection_name) + self.create_index_if_not_exists(index_name=self.index_name, collection=coll) + return coll + + if get_or_create: + # The collection already exists, return it. + coll = self.db[collection_name] + self.create_index_if_not_exists(index_name=self.index_name, collection=coll) + return coll + else: + # get_or_create is False and the collection already exists, raise an error. + raise ValueError(f"Collection {collection_name} already exists.") + + def create_index_if_not_exists(self, index_name: str = "vector_index", collection: Collection = None) -> None: + """ + Creates a vector search index on the specified collection in MongoDB. + + Args: + MONGODB_INDEX (str, optional): The name of the vector search index to create. Defaults to "vector_search_index". + collection (Collection, optional): The MongoDB collection to create the index on. Defaults to None. + """ + if not self._is_index_ready(collection, index_name): + self.create_vector_search_index(collection, index_name) + + def get_collection(self, collection_name: str = None) -> Collection: + """ + Get the collection from the vector database. + + Args: + collection_name: str | The name of the collection. Default is None. If None, return the + current active collection. + + Returns: + Collection | The collection object. + """ + if collection_name is None: + if self.active_collection is None: + raise ValueError("No collection is specified.") + else: + logger.debug( + f"No collection is specified. Using current active collection {self.active_collection.name}." + ) + else: + self.active_collection = self.db[collection_name] + + return self.active_collection + + def delete_collection(self, collection_name: str) -> None: + """ + Delete the collection from the vector database. + + Args: + collection_name: str | The name of the collection. + """ + for index in self.db[collection_name].list_search_indexes(): + self.db[collection_name].drop_search_index(index["name"]) + if self._wait_until_index_ready: + self._wait_for_index(self.db[collection_name], index["name"], "delete") + return self.db[collection_name].drop() + + def create_vector_search_index( + self, + collection: Collection, + index_name: Union[str, None] = "vector_index", + similarity: Literal["euclidean", "cosine", "dotProduct"] = "cosine", + ) -> None: + """Create a vector search index in the collection. + + Args: + collection: An existing Collection in the Atlas Database. + index_name: Vector Search Index name. + similarity: Algorithm used for measuring vector similarity. + kwargs: Additional keyword arguments. + + Returns: + None + """ + search_index_model = SearchIndexModel( + definition={ + "fields": [ + { + "type": "vector", + "numDimensions": self.dimensions, + "path": "embedding", + "similarity": similarity, + }, + ] + }, + name=index_name, + type="vectorSearch", + ) + # Create the search index + try: + collection.create_search_index(model=search_index_model) + if self._wait_until_index_ready: + self._wait_for_index(collection, index_name, "create") + logger.debug(f"Search index {index_name} created successfully.") + except Exception as e: + logger.error( + f"Error creating search index: {e}. \n" + f"Your client must be connected to an Atlas cluster. " + f"You may have to manually create a Collection and Search Index " + f"if you are on a free/shared cluster." + ) + raise e + + def insert_docs( + self, + docs: List[Document], + collection_name: str = None, + upsert: bool = False, + batch_size=DEFAULT_INSERT_BATCH_SIZE, + **kwargs, + ) -> None: + """Insert Documents and Vector Embeddings into the collection of the vector database. + + For large numbers of Documents, insertion is performed in batches. + + Args: + docs: List[Document] | A list of documents. Each document is a TypedDict `Document`. + collection_name: str | The name of the collection. Default is None. + upsert: bool | Whether to update the document if it exists. Default is False. + batch_size: Number of documents to be inserted in each batch + """ + if not docs: + logger.info("No documents to insert.") + return + + collection = self.get_collection(collection_name) + if upsert: + self.update_docs(docs, collection.name, upsert=True) + else: + # Sanity checking the first document + if docs[0].get("content") is None: + raise ValueError("The document content is required.") + if docs[0].get("id") is None: + raise ValueError("The document id is required.") + + input_ids = set() + result_ids = set() + id_batch = [] + text_batch = [] + metadata_batch = [] + size = 0 + i = 0 + for doc in docs: + id = doc["id"] + text = doc["content"] + metadata = doc.get("metadata", {}) + id_batch.append(id) + text_batch.append(text) + metadata_batch.append(metadata) + id_size = 1 if isinstance(id, int) else len(id) + size += len(text) + len(metadata) + id_size + if (i + 1) % batch_size == 0 or size >= 47_000_000: + result_ids.update(self._insert_batch(collection, text_batch, metadata_batch, id_batch)) + input_ids.update(id_batch) + id_batch = [] + text_batch = [] + metadata_batch = [] + size = 0 + i += 1 + if text_batch: + result_ids.update(self._insert_batch(collection, text_batch, metadata_batch, id_batch)) # type: ignore + input_ids.update(id_batch) + + if result_ids != input_ids: + logger.warning( + "Possible data corruption. " + "input_ids not in result_ids: {in_diff}.\n" + "result_ids not in input_ids: {out_diff}".format( + in_diff=input_ids.difference(result_ids), out_diff=result_ids.difference(input_ids) + ) + ) + if self._wait_until_document_ready and docs: + self._wait_for_document(collection, self.index_name, docs[-1]) + + def _insert_batch( + self, collection: Collection, texts: List[str], metadatas: List[Mapping[str, Any]], ids: List[ItemID] + ) -> Set[ItemID]: + """Compute embeddings for and insert a batch of Documents into the Collection. + + For performance reasons, we chose to call self.embedding_function just once, + with the hopefully small tradeoff of having recreating Document dicts. + + Args: + collection: MongoDB Collection + texts: List of the main contents of each document + metadatas: List of metadata mappings + ids: List of ids. Note that these are stored as _id in Collection. + + Returns: + List of ids inserted. + """ + n_texts = len(texts) + if n_texts == 0: + return [] + # Embed and create the documents + embeddings = self.embedding_function(texts).tolist() + assert ( + len(embeddings) == n_texts + ), f"The number of embeddings produced by self.embedding_function ({len(embeddings)} does not match the number of texts provided to it ({n_texts})." + to_insert = [ + {"_id": i, "content": t, "metadata": m, "embedding": e} + for i, t, m, e in zip(ids, texts, metadatas, embeddings) + ] + # insert the documents in MongoDB Atlas + insert_result = collection.insert_many(to_insert) # type: ignore + return insert_result.inserted_ids # TODO Remove this. Replace by log like update_docs + + def update_docs(self, docs: List[Document], collection_name: str = None, **kwargs: Any) -> None: + """Update documents, including their embeddings, in the Collection. + + Optionally allow upsert as kwarg. + + Uses deepcopy to avoid changing docs. + + Args: + docs: List[Document] | A list of documents. + collection_name: str | The name of the collection. Default is None. + kwargs: Any | Use upsert=True` to insert documents whose ids are not present in collection. + """ + + n_docs = len(docs) + logger.info(f"Preparing to embed and update {n_docs=}") + # Compute the embeddings + embeddings: list[list[float]] = self.embedding_function([doc["content"] for doc in docs]).tolist() + # Prepare the updates + all_updates = [] + for i in range(n_docs): + doc = deepcopy(docs[i]) + doc["embedding"] = embeddings[i] + doc["_id"] = doc.pop("id") + + all_updates.append(UpdateOne({"_id": doc["_id"]}, {"$set": doc}, upsert=kwargs.get("upsert", False))) + # Perform update in bulk + collection = self.get_collection(collection_name) + result = collection.bulk_write(all_updates) + + if self._wait_until_document_ready and docs: + self._wait_for_document(collection, self.index_name, docs[-1]) + + # Log a result summary + logger.info( + "Matched: %s, Modified: %s, Upserted: %s", + result.matched_count, + result.modified_count, + result.upserted_count, + ) + + def delete_docs(self, ids: List[ItemID], collection_name: str = None, **kwargs): + """ + Delete documents from the collection of the vector database. + + Args: + ids: List[ItemID] | A list of document ids. Each id is a typed `ItemID`. + collection_name: str | The name of the collection. Default is None. + """ + collection = self.get_collection(collection_name) + return collection.delete_many({"_id": {"$in": ids}}) + + def get_docs_by_ids( + self, ids: List[ItemID] = None, collection_name: str = None, include: List[str] = None, **kwargs + ) -> List[Document]: + """ + Retrieve documents from the collection of the vector database based on the ids. + + Args: + ids: List[ItemID] | A list of document ids. If None, will return all the documents. Default is None. + collection_name: str | The name of the collection. Default is None. + include: List[str] | The fields to include. + If None, will include ["metadata", "content"], ids will always be included. + Basically, use include to choose whether to include embedding and metadata + kwargs: dict | Additional keyword arguments. + + Returns: + List[Document] | The results. + """ + if include is None: + include_fields = {"_id": 1, "content": 1, "metadata": 1} + else: + include_fields = {k: 1 for k in set(include).union({"_id"})} + collection = self.get_collection(collection_name) + if ids is not None: + docs = collection.find({"_id": {"$in": ids}}, include_fields) + # Return with _id field from Collection into id for Document + return with_id_rename(docs) + else: + docs = collection.find({}, include_fields) + # Return with _id field from Collection into id for Document + return with_id_rename(docs) + + def retrieve_docs( + self, + queries: List[str], + collection_name: str = None, + n_results: int = 10, + distance_threshold: float = -1, + **kwargs, + ) -> QueryResults: + """ + Retrieve documents from the collection of the vector database based on the queries. + + Args: + queries: List[str] | A list of queries. Each query is a string. + collection_name: str | The name of the collection. Default is None. + n_results: int | The number of relevant documents to return. Default is 10. + distance_threshold: float | The threshold for the distance score, only distance smaller than it will be + returned. Don't filter with it if < 0. Default is -1. + kwargs: Dict | Additional keyword arguments. Ones of importance follow: + oversampling_factor: int | This times n_results is 'ef' in the HNSW algorithm. + It determines the number of nearest neighbor candidates to consider during the search phase. + A higher value leads to more accuracy, but is slower. Default is 10 + + Returns: + QueryResults | For each query string, a list of nearest documents and their scores. + """ + collection = self.get_collection(collection_name) + # Trivial case of an empty collection + if collection.count_documents({}) == 0: + return [] + + logger.debug(f"Using index: {self.index_name}") + results = [] + for query_text in queries: + # Compute embedding vector from semantic query + logger.debug(f"Query: {query_text}") + query_vector = np.array(self.embedding_function([query_text])).tolist()[0] + # Find documents with similar vectors using the specified index + query_result = _vector_search( + query_vector, + n_results, + collection, + self.index_name, + distance_threshold, + **kwargs, + oversampling_factor=kwargs.get("oversampling_factor", 10), + ) + # Change each _id key to id. with_id_rename, but with (doc, score) tuples + results.append( + [({**{k: v for k, v in d[0].items() if k != "_id"}, "id": d[0]["_id"]}, d[1]) for d in query_result] + ) + return results + + +def _vector_search( + embedding_vector: List[float], + n_results: int, + collection: Collection, + index_name: str, + distance_threshold: float = -1.0, + oversampling_factor=10, + include_embedding=False, +) -> List[Tuple[Dict, float]]: + """Core $vectorSearch Aggregation pipeline. + + Args: + embedding_vector: Embedding vector of semantic query + n_results: Number of documents to return. Defaults to 4. + collection: MongoDB Collection with vector index + index_name: Name of the vector index + distance_threshold: Only distance measures smaller than this will be returned. + Don't filter with it if 1 < x < 0. Default is -1. + oversampling_factor: int | This times n_results is 'ef' in the HNSW algorithm. + It determines the number of nearest neighbor candidates to consider during the search phase. + A higher value leads to more accuracy, but is slower. Default = 10 + + Returns: + List of tuples of length n_results from Collection. + Each tuple contains a document dict and a score. + """ + + pipeline = [ + { + "$vectorSearch": { + "index": index_name, + "limit": n_results, + "numCandidates": n_results * oversampling_factor, + "queryVector": embedding_vector, + "path": "embedding", + } + }, + {"$set": {"score": {"$meta": "vectorSearchScore"}}}, + ] + if distance_threshold >= 0.0: + similarity_threshold = 1.0 - distance_threshold + pipeline.append({"$match": {"score": {"$gte": similarity_threshold}}}) + + if not include_embedding: + pipeline.append({"$project": {"embedding": 0}}) + + logger.debug("pipeline: %s", pipeline) + agg = collection.aggregate(pipeline) + return [(doc, doc.pop("score")) for doc in agg] diff --git a/autogen/agentchat/contrib/vectordb/qdrant.py b/autogen/agentchat/contrib/vectordb/qdrant.py new file mode 100644 index 000000000000..d9c4ee1d2e5a --- /dev/null +++ b/autogen/agentchat/contrib/vectordb/qdrant.py @@ -0,0 +1,328 @@ +import abc +import logging +import os +from typing import Callable, List, Optional, Sequence, Tuple, Union + +from .base import Document, ItemID, QueryResults, VectorDB +from .utils import get_logger + +try: + from qdrant_client import QdrantClient, models +except ImportError: + raise ImportError("Please install qdrant-client: `pip install qdrant-client`") + +logger = get_logger(__name__) + +Embeddings = Union[Sequence[float], Sequence[int]] + + +class EmbeddingFunction(abc.ABC): + @abc.abstractmethod + def __call__(self, inputs: List[str]) -> List[Embeddings]: + raise NotImplementedError + + +class FastEmbedEmbeddingFunction(EmbeddingFunction): + """Embedding function implementation using FastEmbed - https://qdrant.github.io/fastembed.""" + + def __init__( + self, + model_name: str = "BAAI/bge-small-en-v1.5", + batch_size: int = 256, + cache_dir: Optional[str] = None, + threads: Optional[int] = None, + parallel: Optional[int] = None, + **kwargs, + ): + """Initialize fastembed.TextEmbedding. + + Args: + model_name (str): The name of the model to use. Defaults to `"BAAI/bge-small-en-v1.5"`. + batch_size (int): Batch size for encoding. Higher values will use more memory, but be faster.\ + Defaults to 256. + cache_dir (str, optional): The path to the model cache directory.\ + Can also be set using the `FASTEMBED_CACHE_PATH` env variable. + threads (int, optional): The number of threads single onnxruntime session can use. + parallel (int, optional): If `>1`, data-parallel encoding will be used, recommended for large datasets.\ + If `0`, use all available cores.\ + If `None`, don't use data-parallel processing, use default onnxruntime threading.\ + Defaults to None. + **kwargs: Additional options to pass to fastembed.TextEmbedding + Raises: + ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-small-en-v1.5. + """ + try: + from fastembed import TextEmbedding + except ImportError as e: + raise ValueError( + "The 'fastembed' package is not installed. Please install it with `pip install fastembed`", + ) from e + self._batch_size = batch_size + self._parallel = parallel + self._model = TextEmbedding(model_name=model_name, cache_dir=cache_dir, threads=threads, **kwargs) + + def __call__(self, inputs: List[str]) -> List[Embeddings]: + embeddings = self._model.embed(inputs, batch_size=self._batch_size, parallel=self._parallel) + + return [embedding.tolist() for embedding in embeddings] + + +class QdrantVectorDB(VectorDB): + """ + A vector database implementation that uses Qdrant as the backend. + """ + + def __init__( + self, + *, + client=None, + embedding_function: EmbeddingFunction = None, + content_payload_key: str = "_content", + metadata_payload_key: str = "_metadata", + collection_options: dict = {}, + **kwargs, + ) -> None: + """ + Initialize the vector database. + + Args: + client: qdrant_client.QdrantClient | An instance of QdrantClient. + embedding_function: Callable | The embedding function used to generate the vector representation + of the documents. Defaults to FastEmbedEmbeddingFunction. + collection_options: dict | The options for creating the collection. + kwargs: dict | Additional keyword arguments. + """ + self.client: QdrantClient = client or QdrantClient(location=":memory:") + self.embedding_function = embedding_function or FastEmbedEmbeddingFunction() + self.collection_options = collection_options + self.content_payload_key = content_payload_key + self.metadata_payload_key = metadata_payload_key + self.type = "qdrant" + + def create_collection(self, collection_name: str, overwrite: bool = False, get_or_create: bool = True) -> None: + """ + Create a collection in the vector database. + Case 1. if the collection does not exist, create the collection. + Case 2. the collection exists, if overwrite is True, it will overwrite the collection. + Case 3. the collection exists and overwrite is False, if get_or_create is True, it will get the collection, + otherwise it raise a ValueError. + + Args: + collection_name: str | The name of the collection. + overwrite: bool | Whether to overwrite the collection if it exists. Default is False. + get_or_create: bool | Whether to get the collection if it exists. Default is True. + + Returns: + Any | The collection object. + """ + embeddings_size = len(self.embedding_function(["test"])[0]) + + if self.client.collection_exists(collection_name) and overwrite: + self.client.delete_collection(collection_name) + + if not self.client.collection_exists(collection_name): + self.client.create_collection( + collection_name, + vectors_config=models.VectorParams(size=embeddings_size, distance=models.Distance.COSINE), + **self.collection_options, + ) + elif not get_or_create: + raise ValueError(f"Collection {collection_name} already exists.") + + def get_collection(self, collection_name: str = None): + """ + Get the collection from the vector database. + + Args: + collection_name: str | The name of the collection. + + Returns: + Any | The collection object. + """ + if collection_name is None: + raise ValueError("The collection name is required.") + + return self.client.get_collection(collection_name) + + def delete_collection(self, collection_name: str) -> None: + """Delete the collection from the vector database. + + Args: + collection_name: str | The name of the collection. + + Returns: + Any + """ + return self.client.delete_collection(collection_name) + + def insert_docs(self, docs: List[Document], collection_name: str = None, upsert: bool = False) -> None: + """ + Insert documents into the collection of the vector database. + + Args: + docs: List[Document] | A list of documents. Each document is a TypedDict `Document`. + collection_name: str | The name of the collection. Default is None. + upsert: bool | Whether to update the document if it exists. Default is False. + kwargs: Dict | Additional keyword arguments. + + Returns: + None + """ + if not docs: + return + if any(doc.get("content") is None for doc in docs): + raise ValueError("The document content is required.") + if any(doc.get("id") is None for doc in docs): + raise ValueError("The document id is required.") + + if not upsert and not self._validate_upsert_ids(collection_name, [doc["id"] for doc in docs]): + logger.log("Some IDs already exist. Skipping insert", level=logging.WARN) + + self.client.upsert(collection_name, points=self._documents_to_points(docs)) + + def update_docs(self, docs: List[Document], collection_name: str = None) -> None: + if not docs: + return + if any(doc.get("id") is None for doc in docs): + raise ValueError("The document id is required.") + if any(doc.get("content") is None for doc in docs): + raise ValueError("The document content is required.") + if self._validate_update_ids(collection_name, [doc["id"] for doc in docs]): + return self.client.upsert(collection_name, points=self._documents_to_points(docs)) + + raise ValueError("Some IDs do not exist. Skipping update") + + def delete_docs(self, ids: List[ItemID], collection_name: str = None, **kwargs) -> None: + """ + Delete documents from the collection of the vector database. + + Args: + ids: List[ItemID] | A list of document ids. Each id is a typed `ItemID`. + collection_name: str | The name of the collection. Default is None. + kwargs: Dict | Additional keyword arguments. + + Returns: + None + """ + self.client.delete(collection_name, ids) + + def retrieve_docs( + self, + queries: List[str], + collection_name: str = None, + n_results: int = 10, + distance_threshold: float = 0, + **kwargs, + ) -> QueryResults: + """ + Retrieve documents from the collection of the vector database based on the queries. + + Args: + queries: List[str] | A list of queries. Each query is a string. + collection_name: str | The name of the collection. Default is None. + n_results: int | The number of relevant documents to return. Default is 10. + distance_threshold: float | The threshold for the distance score, only distance smaller than it will be + returned. Don't filter with it if < 0. Default is 0. + kwargs: Dict | Additional keyword arguments. + + Returns: + QueryResults | The query results. Each query result is a list of list of tuples containing the document and + the distance. + """ + embeddings = self.embedding_function(queries) + requests = [ + models.SearchRequest( + vector=embedding, + limit=n_results, + score_threshold=distance_threshold, + with_payload=True, + with_vector=False, + ) + for embedding in embeddings + ] + + batch_results = self.client.search_batch(collection_name, requests) + return [self._scored_points_to_documents(results) for results in batch_results] + + def get_docs_by_ids( + self, ids: List[ItemID] = None, collection_name: str = None, include=True, **kwargs + ) -> List[Document]: + """ + Retrieve documents from the collection of the vector database based on the ids. + + Args: + ids: List[ItemID] | A list of document ids. If None, will return all the documents. Default is None. + collection_name: str | The name of the collection. Default is None. + include: List[str] | The fields to include. Default is True. + If None, will include ["metadatas", "documents"], ids will always be included. + kwargs: dict | Additional keyword arguments. + + Returns: + List[Document] | The results. + """ + if ids is None: + results = self.client.scroll(collection_name=collection_name, with_payload=include, with_vectors=True)[0] + else: + results = self.client.retrieve(collection_name, ids=ids, with_payload=include, with_vectors=True) + return [self._point_to_document(result) for result in results] + + def _point_to_document(self, point) -> Document: + return { + "id": point.id, + "content": point.payload.get(self.content_payload_key, ""), + "metadata": point.payload.get(self.metadata_payload_key, {}), + "embedding": point.vector, + } + + def _points_to_documents(self, points) -> List[Document]: + return [self._point_to_document(point) for point in points] + + def _scored_point_to_document(self, scored_point: models.ScoredPoint) -> Tuple[Document, float]: + return self._point_to_document(scored_point), scored_point.score + + def _documents_to_points(self, documents: List[Document]): + contents = [document["content"] for document in documents] + embeddings = self.embedding_function(contents) + points = [ + models.PointStruct( + id=documents[i]["id"], + vector=embeddings[i], + payload={ + self.content_payload_key: documents[i].get("content"), + self.metadata_payload_key: documents[i].get("metadata"), + }, + ) + for i in range(len(documents)) + ] + return points + + def _scored_points_to_documents(self, scored_points: List[models.ScoredPoint]) -> List[Tuple[Document, float]]: + return [self._scored_point_to_document(scored_point) for scored_point in scored_points] + + def _validate_update_ids(self, collection_name: str, ids: List[str]) -> bool: + """ + Validates all the IDs exist in the collection + """ + retrieved_ids = [ + point.id for point in self.client.retrieve(collection_name, ids=ids, with_payload=False, with_vectors=False) + ] + + if missing_ids := set(ids) - set(retrieved_ids): + logger.log(f"Missing IDs: {missing_ids}. Skipping update", level=logging.WARN) + return False + + return True + + def _validate_upsert_ids(self, collection_name: str, ids: List[str]) -> bool: + """ + Validate none of the IDs exist in the collection + """ + retrieved_ids = [ + point.id for point in self.client.retrieve(collection_name, ids=ids, with_payload=False, with_vectors=False) + ] + + if existing_ids := set(ids) & set(retrieved_ids): + logger.log(f"Existing IDs: {existing_ids}.", level=logging.WARN) + return False + + return True diff --git a/autogen/agentchat/contrib/web_surfer.py b/autogen/agentchat/contrib/web_surfer.py index af07be6d3432..f74915a9b403 100644 --- a/autogen/agentchat/contrib/web_surfer.py +++ b/autogen/agentchat/contrib/web_surfer.py @@ -41,6 +41,7 @@ def __init__( summarizer_llm_config: Optional[Union[Dict, Literal[False]]] = None, default_auto_reply: Optional[Union[str, Dict, None]] = "", browser_config: Optional[Union[Dict, None]] = None, + **kwargs, ): super().__init__( name=name, @@ -53,6 +54,7 @@ def __init__( code_execution_config=code_execution_config, llm_config=llm_config, default_auto_reply=default_auto_reply, + **kwargs, ) self._create_summarizer_client(summarizer_llm_config, llm_config) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index b434fc648eb1..e19cbd56de2b 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -11,6 +11,7 @@ from openai import BadRequestError +from autogen.agentchat.chat import _post_process_carryover_item from autogen.exception_utils import InvalidCarryOverType, SenderRequired from .._pydantic import model_dump @@ -77,6 +78,7 @@ def __init__( default_auto_reply: Union[str, Dict] = "", description: Optional[str] = None, chat_messages: Optional[Dict[Agent, List[Dict]]] = None, + silent: Optional[bool] = None, ): """ Args: @@ -125,6 +127,8 @@ def __init__( chat_messages (dict or None): the previous chat messages that this agent had in the past with other agents. Can be used to give the agent a memory by providing the chat history. This will allow the agent to resume previous had conversations. Defaults to an empty chat history. + silent (bool or None): (Experimental) whether to print the message sent. If None, will use the value of + silent in each function. """ # we change code_execution_config below and we have to make sure we don't change the input # in case of UserProxyAgent, without this we could even change the default value {} @@ -146,6 +150,7 @@ def __init__( if is_termination_msg is not None else (lambda x: content_str(x.get("content")) == "TERMINATE") ) + self.silent = silent # Take a copy to avoid modifying the given dict if isinstance(llm_config, dict): try: @@ -262,6 +267,10 @@ def _validate_llm_config(self, llm_config): ) self.client = None if self.llm_config is False else OpenAIWrapper(**self.llm_config) + @staticmethod + def _is_silent(agent: Agent, silent: Optional[bool] = False) -> bool: + return agent.silent if agent.silent is not None else silent + @property def name(self) -> str: """Get the name of the agent.""" @@ -368,9 +377,9 @@ def replace_reply_func(self, old_reply_func: Callable, new_reply_func: Callable) f["reply_func"] = new_reply_func @staticmethod - def _summary_from_nested_chats( + def _get_chats_to_run( chat_queue: List[Dict[str, Any]], recipient: Agent, messages: Union[str, Callable], sender: Agent, config: Any - ) -> Tuple[bool, str]: + ) -> List[Dict[str, Any]]: """A simple chat reply function. This function initiate one or a sequence of chats between the "recipient" and the agents in the chat_queue. @@ -397,22 +406,59 @@ def _summary_from_nested_chats( if message: current_c["message"] = message chat_to_run.append(current_c) + return chat_to_run + + @staticmethod + def _summary_from_nested_chats( + chat_queue: List[Dict[str, Any]], recipient: Agent, messages: Union[str, Callable], sender: Agent, config: Any + ) -> Tuple[bool, Union[str, None]]: + """A simple chat reply function. + This function initiate one or a sequence of chats between the "recipient" and the agents in the + chat_queue. + + It extracts and returns a summary from the nested chat based on the "summary_method" in each chat in chat_queue. + + Returns: + Tuple[bool, str]: A tuple where the first element indicates the completion of the chat, and the second element contains the summary of the last chat if any chats were initiated. + """ + chat_to_run = ConversableAgent._get_chats_to_run(chat_queue, recipient, messages, sender, config) if not chat_to_run: return True, None res = initiate_chats(chat_to_run) return True, res[-1].summary + @staticmethod + async def _a_summary_from_nested_chats( + chat_queue: List[Dict[str, Any]], recipient: Agent, messages: Union[str, Callable], sender: Agent, config: Any + ) -> Tuple[bool, Union[str, None]]: + """A simple chat reply function. + This function initiate one or a sequence of chats between the "recipient" and the agents in the + chat_queue. + + It extracts and returns a summary from the nested chat based on the "summary_method" in each chat in chat_queue. + + Returns: + Tuple[bool, str]: A tuple where the first element indicates the completion of the chat, and the second element contains the summary of the last chat if any chats were initiated. + """ + chat_to_run = ConversableAgent._get_chats_to_run(chat_queue, recipient, messages, sender, config) + if not chat_to_run: + return True, None + res = await a_initiate_chats(chat_to_run) + index_of_last_chat = chat_to_run[-1]["chat_id"] + return True, res[index_of_last_chat].summary + def register_nested_chats( self, chat_queue: List[Dict[str, Any]], trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List], reply_func_from_nested_chats: Union[str, Callable] = "summary_from_nested_chats", position: int = 2, + use_async: Union[bool, None] = None, **kwargs, ) -> None: """Register a nested chat reply function. Args: - chat_queue (list): a list of chat objects to be initiated. + chat_queue (list): a list of chat objects to be initiated. If use_async is used, then all messages in chat_queue must have a chat-id associated with them. trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. @@ -427,15 +473,33 @@ def reply_func_from_nested_chats( ) -> Tuple[bool, Union[str, Dict, None]]: ``` position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. + use_async: Uses a_initiate_chats internally to start nested chats. If the original chat is initiated with a_initiate_chats, you may set this to true so nested chats do not run in sync. kwargs: Ref to `register_reply` for details. """ - if reply_func_from_nested_chats == "summary_from_nested_chats": - reply_func_from_nested_chats = self._summary_from_nested_chats - if not callable(reply_func_from_nested_chats): - raise ValueError("reply_func_from_nested_chats must be a callable") + if use_async: + for chat in chat_queue: + if chat.get("chat_id") is None: + raise ValueError("chat_id is required for async nested chats") + + if use_async: + if reply_func_from_nested_chats == "summary_from_nested_chats": + reply_func_from_nested_chats = self._a_summary_from_nested_chats + if not callable(reply_func_from_nested_chats) or not inspect.iscoroutinefunction( + reply_func_from_nested_chats + ): + raise ValueError("reply_func_from_nested_chats must be a callable and a coroutine") - def wrapped_reply_func(recipient, messages=None, sender=None, config=None): - return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) + async def wrapped_reply_func(recipient, messages=None, sender=None, config=None): + return await reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) + + else: + if reply_func_from_nested_chats == "summary_from_nested_chats": + reply_func_from_nested_chats = self._summary_from_nested_chats + if not callable(reply_func_from_nested_chats): + raise ValueError("reply_func_from_nested_chats must be a callable") + + def wrapped_reply_func(recipient, messages=None, sender=None, config=None): + return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) functools.update_wrapper(wrapped_reply_func, reply_func_from_nested_chats) @@ -445,7 +509,9 @@ def wrapped_reply_func(recipient, messages=None, sender=None, config=None): position, kwargs.get("config"), kwargs.get("reset_config"), - ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat"), + ignore_async_in_sync_chat=( + not use_async if use_async is not None else kwargs.get("ignore_async_in_sync_chat") + ), ) @property @@ -555,7 +621,7 @@ def _assert_valid_name(name): raise ValueError(f"Invalid name: {name}. Name must be less than 64 characters.") return name - def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: Agent) -> bool: + def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: Agent, is_sending: bool) -> bool: """Append a message to the ChatCompletion conversation. If the message received is a string, it will be put in the "content" field of the new dictionary. @@ -567,6 +633,7 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: message (dict or str): message to be appended to the ChatCompletion conversation. role (str): role of the message, can be "assistant" or "function". conversation_id (Agent): id of the conversation, should be the recipient or sender. + is_sending (bool): If the agent (aka self) is sending to the conversation_id agent, otherwise receiving. Returns: bool: whether the message is appended to the ChatCompletion conversation. @@ -596,7 +663,15 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: if oai_message.get("function_call", False) or oai_message.get("tool_calls", False): oai_message["role"] = "assistant" # only messages with role 'assistant' can have a function call. + elif "name" not in oai_message: + # If we don't have a name field, append it + if is_sending: + oai_message["name"] = self.name + else: + oai_message["name"] = conversation_id.name + self._oai_messages[conversation_id].append(oai_message) + return True def _process_message_before_send( @@ -605,7 +680,9 @@ def _process_message_before_send( """Process the message before sending it to the recipient.""" hook_list = self.hook_lists["process_message_before_send"] for hook in hook_list: - message = hook(sender=self, message=message, recipient=recipient, silent=silent) + message = hook( + sender=self, message=message, recipient=recipient, silent=ConversableAgent._is_silent(self, silent) + ) return message def send( @@ -647,10 +724,10 @@ def send( Raises: ValueError: if the message can't be converted into a valid ChatCompletion message. """ - message = self._process_message_before_send(message, recipient, silent) + message = self._process_message_before_send(message, recipient, ConversableAgent._is_silent(self, silent)) # When the agent composes and sends the message, the role of the message is "assistant" # unless it's "function". - valid = self._append_oai_message(message, "assistant", recipient) + valid = self._append_oai_message(message, "assistant", recipient, is_sending=True) if valid: recipient.receive(message, self, request_reply, silent) else: @@ -697,10 +774,10 @@ async def a_send( Raises: ValueError: if the message can't be converted into a valid ChatCompletion message. """ - message = self._process_message_before_send(message, recipient, silent) + message = self._process_message_before_send(message, recipient, ConversableAgent._is_silent(self, silent)) # When the agent composes and sends the message, the role of the message is "assistant" # unless it's "function". - valid = self._append_oai_message(message, "assistant", recipient) + valid = self._append_oai_message(message, "assistant", recipient, is_sending=True) if valid: await recipient.a_receive(message, self, request_reply, silent) else: @@ -771,7 +848,7 @@ def _print_received_message(self, message: Union[Dict, str], sender: Agent): def _process_received_message(self, message: Union[Dict, str], sender: Agent, silent: bool): # When the agent receives a message, the role of the message is "user". (If 'role' exists and is 'function', it will remain unchanged.) - valid = self._append_oai_message(message, "user", sender) + valid = self._append_oai_message(message, "user", sender, is_sending=False) if logging_enabled(): log_event(self, "received_message", message=message, sender=sender.name, valid=valid) @@ -779,7 +856,8 @@ def _process_received_message(self, message: Union[Dict, str], sender: Agent, si raise ValueError( "Received message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." ) - if not silent: + + if not ConversableAgent._is_silent(sender, silent): self._print_received_message(message, sender) def receive( @@ -1580,8 +1658,8 @@ async def a_generate_function_call_reply( if messages is None: messages = self._oai_messages[sender] message = messages[-1] - if "function_call" in message: - func_call = message["function_call"] + func_call = message.get("function_call") + if func_call: func_name = func_call.get("name", "") func = self._function_map.get(func_name, None) if func and inspect.iscoroutinefunction(func): @@ -1722,7 +1800,7 @@ def check_termination_and_human_reply( sender_name = "the sender" if sender is None else sender.name if self.human_input_mode == "ALWAYS": reply = self.get_human_input( - f"Provide feedback to {sender_name}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: " + f"Replying as {self.name}. Provide feedback to {sender_name}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: " ) no_human_input_msg = "NO HUMAN INPUT RECEIVED." if not reply else "" # if the human input is empty, and the message is a termination message, then we will terminate the conversation @@ -1835,7 +1913,7 @@ async def a_check_termination_and_human_reply( sender_name = "the sender" if sender is None else sender.name if self.human_input_mode == "ALWAYS": reply = await self.a_get_human_input( - f"Provide feedback to {sender_name}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: " + f"Replying as {self.name}. Provide feedback to {sender_name}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: " ) no_human_input_msg = "NO HUMAN INPUT RECEIVED." if not reply else "" # if the human input is empty, and the message is a termination message, then we will terminate the conversation @@ -2184,7 +2262,7 @@ def _format_json_str(jstr): Ex 2: "{\n \"location\": \"Boston, MA\"\n}" -> "{"location": "Boston, MA"}" - 2. this function also handles JSON escape sequences inside quotes, + 2. this function also handles JSON escape sequences inside quotes. Ex 1: '{"args": "a\na\na\ta"}' -> '{"args": "a\\na\\na\\ta"}' """ @@ -2233,7 +2311,7 @@ def execute_function(self, func_call, verbose: bool = False) -> Tuple[bool, Dict arguments = json.loads(input_string) except json.JSONDecodeError as e: arguments = None - content = f"Error: {e}\n You argument should follow json format." + content = f"Error: {e}\n The argument must be in JSON format." # Try to execute the function if arguments is not None: @@ -2290,7 +2368,7 @@ async def a_execute_function(self, func_call): arguments = json.loads(input_string) except json.JSONDecodeError as e: arguments = None - content = f"Error: {e}\n You argument should follow json format." + content = f"Error: {e}\n The argument must be in JSON format." # Try to execute the function if arguments is not None: @@ -2364,7 +2442,7 @@ def _process_carryover(self, content: str, kwargs: dict) -> str: if isinstance(kwargs["carryover"], str): content += "\nContext: \n" + kwargs["carryover"] elif isinstance(kwargs["carryover"], list): - content += "\nContext: \n" + ("\n").join([t for t in kwargs["carryover"]]) + content += "\nContext: \n" + ("\n").join([_post_process_carryover_item(t) for t in kwargs["carryover"]]) else: raise InvalidCarryOverType( "Carryover should be a string or a list of strings. Not adding carryover to the message." @@ -2526,14 +2604,16 @@ def _wrap_function(self, func: F) -> F: @functools.wraps(func) def _wrapped_func(*args, **kwargs): retval = func(*args, **kwargs) - log_function_use(self, func, kwargs, retval) + if logging_enabled(): + log_function_use(self, func, kwargs, retval) return serialize_to_str(retval) @load_basemodels_if_needed @functools.wraps(func) async def _a_wrapped_func(*args, **kwargs): retval = await func(*args, **kwargs) - log_function_use(self, func, kwargs, retval) + if logging_enabled(): + log_function_use(self, func, kwargs, retval) return serialize_to_str(retval) wrapped_func = _a_wrapped_func if inspect.iscoroutinefunction(func) else _wrapped_func diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 48f11d526cc6..c6355a13b94d 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -5,7 +5,7 @@ import re import sys from dataclasses import dataclass, field -from typing import Callable, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union from ..code_utils import content_str from ..exception_utils import AgentNameConflict, NoEligibleSpeaker, UndefinedNextAgent @@ -17,6 +17,12 @@ from .chat import ChatResult from .conversable_agent import ConversableAgent +try: + # Non-core module + from .contrib.capabilities import transform_messages +except ImportError: + transform_messages = None + logger = logging.getLogger(__name__) @@ -76,6 +82,8 @@ def custom_speaker_selection_func( of times until a single agent is returned or it exhausts the maximum attempts. Applies only to "auto" speaker selection method. Default is 2. + - select_speaker_transform_messages: (optional) the message transformations to apply to the nested select speaker agent-to-agent chat messages. + Takes a TransformMessages object, defaults to None and is only utilised when the speaker selection method is "auto". - select_speaker_auto_verbose: whether to output the select speaker responses and selections If set to True, the outputs from the two agents in the nested select speaker chat will be output, along with whether the responses were successful, or not, in selecting an agent @@ -132,6 +140,7 @@ def custom_speaker_selection_func( The names are case-sensitive and should not be abbreviated or changed. The only names that are accepted are {agentlist}. Respond with ONLY the name of the speaker and DO NOT provide a reason.""" + select_speaker_transform_messages: Optional[Any] = None select_speaker_auto_verbose: Optional[bool] = False role_for_select_speaker_messages: Optional[str] = "system" @@ -249,6 +258,20 @@ def __post_init__(self): elif self.max_retries_for_selecting_speaker < 0: raise ValueError("max_retries_for_selecting_speaker must be greater than or equal to zero") + # Load message transforms here (load once for the Group Chat so we don't have to re-initiate it and it maintains the cache across subsequent select speaker calls) + self._speaker_selection_transforms = None + if self.select_speaker_transform_messages is not None: + if transform_messages is not None: + if isinstance(self.select_speaker_transform_messages, transform_messages.TransformMessages): + self._speaker_selection_transforms = self.select_speaker_transform_messages + else: + raise ValueError("select_speaker_transform_messages must be None or MessageTransforms.") + else: + logger.warning( + "TransformMessages could not be loaded, the 'select_speaker_transform_messages' transform" + "will not apply." + ) + # Validate select_speaker_auto_verbose if self.select_speaker_auto_verbose is None or not isinstance(self.select_speaker_auto_verbose, bool): raise ValueError("select_speaker_auto_verbose cannot be None or non-bool") @@ -649,11 +672,16 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un if self.select_speaker_prompt_template is not None: start_message = { "content": self.select_speaker_prompt(agents), + "name": "checking_agent", "override_role": self.role_for_select_speaker_messages, } else: start_message = messages[-1] + # Add the message transforms, if any, to the speaker selection agent + if self._speaker_selection_transforms is not None: + self._speaker_selection_transforms.add_to_agent(speaker_selection_agent) + # Run the speaker selection chat result = checking_agent.initiate_chat( speaker_selection_agent, @@ -748,6 +776,10 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un else: start_message = messages[-1] + # Add the message transforms, if any, to the speaker selection agent + if self._speaker_selection_transforms is not None: + self._speaker_selection_transforms.add_to_agent(speaker_selection_agent) + # Run the speaker selection chat result = await checking_agent.a_initiate_chat( speaker_selection_agent, @@ -813,6 +845,7 @@ def _validate_speaker_name( return True, { "content": self.select_speaker_auto_multiple_template.format(agentlist=agentlist), + "name": "checking_agent", "override_role": self.role_for_select_speaker_messages, } else: @@ -842,6 +875,7 @@ def _validate_speaker_name( return True, { "content": self.select_speaker_auto_none_template.format(agentlist=agentlist), + "name": "checking_agent", "override_role": self.role_for_select_speaker_messages, } else: @@ -965,6 +999,7 @@ def __init__( # Store groupchat self._groupchat = groupchat + self._last_speaker = None self._silent = silent # Order of register_reply is important. @@ -1006,6 +1041,53 @@ def _prepare_chat( if (recipient != agent or prepare_recipient) and isinstance(agent, ConversableAgent): agent._prepare_chat(self, clear_history, False, reply_at_receive) + @property + def last_speaker(self) -> Agent: + """Return the agent who sent the last message to group chat manager. + + In a group chat, an agent will always send a message to the group chat manager, and the group chat manager will + send the message to all other agents in the group chat. So, when an agent receives a message, it will always be + from the group chat manager. With this property, the agent receiving the message can know who actually sent the + message. + + Example: + ```python + from autogen import ConversableAgent + from autogen import GroupChat, GroupChatManager + + + def print_messages(recipient, messages, sender, config): + # Print the message immediately + print( + f"Sender: {sender.name} | Recipient: {recipient.name} | Message: {messages[-1].get('content')}" + ) + print(f"Real Sender: {sender.last_speaker.name}") + assert sender.last_speaker.name in messages[-1].get("content") + return False, None # Required to ensure the agent communication flow continues + + + agent_a = ConversableAgent("agent A", default_auto_reply="I'm agent A.") + agent_b = ConversableAgent("agent B", default_auto_reply="I'm agent B.") + agent_c = ConversableAgent("agent C", default_auto_reply="I'm agent C.") + for agent in [agent_a, agent_b, agent_c]: + agent.register_reply( + [ConversableAgent, None], reply_func=print_messages, config=None + ) + group_chat = GroupChat( + [agent_a, agent_b, agent_c], + messages=[], + max_round=6, + speaker_selection_method="random", + allow_repeat_speaker=True, + ) + chat_manager = GroupChatManager(group_chat) + groupchat_result = agent_a.initiate_chat( + chat_manager, message="Hi, there, I'm agent A." + ) + ``` + """ + return self._last_speaker + def run_chat( self, messages: Optional[List[Dict]] = None, @@ -1034,6 +1116,7 @@ def run_chat( a.previous_cache = a.client_cache a.client_cache = self.client_cache for i in range(groupchat.max_round): + self._last_speaker = speaker groupchat.append(message, speaker) # broadcast the message to all agents except the speaker for agent in groupchat.agents: @@ -1212,11 +1295,10 @@ def resume( if not message_speaker_agent and message["name"] == self.name: message_speaker_agent = self - # Add previous messages to each agent (except their own messages and the last message, as we'll kick off the conversation with it) + # Add previous messages to each agent (except the last message, as we'll kick off the conversation with it) if i != len(messages) - 1: for agent in self._groupchat.agents: - if agent.name != message["name"]: - self.send(message, self._groupchat.agent_by_name(agent.name), request_reply=False, silent=True) + self.send(message, self._groupchat.agent_by_name(agent.name), request_reply=False, silent=True) # Add previous message to the new groupchat, if it's an admin message the name may not match so add the message directly if message_speaker_agent: @@ -1258,7 +1340,7 @@ def resume( async def a_resume( self, messages: Union[List[Dict], str], - remove_termination_string: Union[str, Callable[[str], str]], + remove_termination_string: Union[str, Callable[[str], str]] = None, silent: Optional[bool] = False, ) -> Tuple[ConversableAgent, Dict]: """Resumes a group chat using the previous messages as a starting point, asynchronously. Requires the agents, group chat, and group chat manager to be established @@ -1316,13 +1398,12 @@ async def a_resume( if not message_speaker_agent and message["name"] == self.name: message_speaker_agent = self - # Add previous messages to each agent (except their own messages and the last message, as we'll kick off the conversation with it) + # Add previous messages to each agent (except the last message, as we'll kick off the conversation with it) if i != len(messages) - 1: for agent in self._groupchat.agents: - if agent.name != message["name"]: - await self.a_send( - message, self._groupchat.agent_by_name(agent.name), request_reply=False, silent=True - ) + await self.a_send( + message, self._groupchat.agent_by_name(agent.name), request_reply=False, silent=True + ) # Add previous message to the new groupchat, if it's an admin message the name may not match so add the message directly if message_speaker_agent: diff --git a/autogen/agentchat/user_proxy_agent.py b/autogen/agentchat/user_proxy_agent.py index a80296a8355a..d50e4d8b89c5 100644 --- a/autogen/agentchat/user_proxy_agent.py +++ b/autogen/agentchat/user_proxy_agent.py @@ -35,6 +35,7 @@ def __init__( llm_config: Optional[Union[Dict, Literal[False]]] = False, system_message: Optional[Union[str, List]] = "", description: Optional[str] = None, + **kwargs, ): """ Args: @@ -79,6 +80,8 @@ def __init__( Only used when llm_config is not False. Use it to reprogram the agent. description (str): a short description of the agent. This description is used by other agents (e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message) + **kwargs (dict): Please refer to other kwargs in + [ConversableAgent](conversable_agent#__init__). """ super().__init__( name=name, @@ -93,6 +96,7 @@ def __init__( description=( description if description is not None else self.DEFAULT_USER_PROXY_AGENT_DESCRIPTIONS[human_input_mode] ), + **kwargs, ) if logging_enabled(): diff --git a/autogen/coding/base.py b/autogen/coding/base.py index ccbfe6b92932..7c9e19d73f33 100644 --- a/autogen/coding/base.py +++ b/autogen/coding/base.py @@ -4,7 +4,6 @@ from pydantic import BaseModel, Field -from ..agentchat.agent import LLMAgent from ..types import UserMessageImageContentPart, UserMessageTextContentPart __all__ = ("CodeBlock", "CodeResult", "CodeExtractor", "CodeExecutor", "CodeExecutionConfig") diff --git a/autogen/coding/func_with_reqs.py b/autogen/coding/func_with_reqs.py index 6f199573822b..f255f1df0179 100644 --- a/autogen/coding/func_with_reqs.py +++ b/autogen/coding/func_with_reqs.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from importlib.abc import SourceLoader from textwrap import dedent, indent -from typing import Any, Callable, Generic, List, TypeVar, Union +from typing import Any, Callable, Generic, List, Set, TypeVar, Union from typing_extensions import ParamSpec @@ -159,12 +159,12 @@ def _build_python_functions_file( funcs: List[Union[FunctionWithRequirements[Any, P], Callable[..., Any], FunctionWithRequirementsStr]] ) -> str: # First collect all global imports - global_imports = set() + global_imports: Set[str] = set() for func in funcs: if isinstance(func, (FunctionWithRequirements, FunctionWithRequirementsStr)): - global_imports.update(func.global_imports) + global_imports.update(map(_import_to_str, func.global_imports)) - content = "\n".join(map(_import_to_str, global_imports)) + "\n\n" + content = "\n".join(global_imports) + "\n\n" for func in funcs: content += _to_code(func) + "\n\n" diff --git a/autogen/coding/jupyter/jupyter_client.py b/autogen/coding/jupyter/jupyter_client.py index b3de374fce9b..787009dafe2f 100644 --- a/autogen/coding/jupyter/jupyter_client.py +++ b/autogen/coding/jupyter/jupyter_client.py @@ -39,6 +39,10 @@ def _get_headers(self) -> Dict[str, str]: return {} return {"Authorization": f"token {self._connection_info.token}"} + def _get_cookies(self) -> str: + cookies = self._session.cookies.get_dict() + return "; ".join([f"{name}={value}" for name, value in cookies.items()]) + def _get_api_base_url(self) -> str: protocol = "https" if self._connection_info.use_https else "http" port = f":{self._connection_info.port}" if self._connection_info.port else "" @@ -87,7 +91,7 @@ def restart_kernel(self, kernel_id: str) -> None: def get_kernel_client(self, kernel_id: str) -> JupyterKernelClient: ws_url = f"{self._get_ws_base_url()}/api/kernels/{kernel_id}/channels" - ws = websocket.create_connection(ws_url, header=self._get_headers()) + ws = websocket.create_connection(ws_url, header=self._get_headers(), cookie=self._get_cookies()) return JupyterKernelClient(ws) diff --git a/autogen/coding/local_commandline_code_executor.py b/autogen/coding/local_commandline_code_executor.py index 620b359a4aee..2280f7f030d8 100644 --- a/autogen/coding/local_commandline_code_executor.py +++ b/autogen/coding/local_commandline_code_executor.py @@ -221,7 +221,12 @@ def _setup_functions(self) -> None: cmd = [py_executable, "-m", "pip", "install"] + required_packages try: result = subprocess.run( - cmd, cwd=self._work_dir, capture_output=True, text=True, timeout=float(self._timeout) + cmd, + cwd=self._work_dir, + capture_output=True, + text=True, + timeout=float(self._timeout), + encoding="utf-8", ) except subprocess.TimeoutExpired as e: raise ValueError("Pip install timed out") from e @@ -303,7 +308,13 @@ def _execute_code_dont_check_setup(self, code_blocks: List[CodeBlock]) -> Comman try: result = subprocess.run( - cmd, cwd=self._work_dir, capture_output=True, text=True, timeout=float(self._timeout), env=env + cmd, + cwd=self._work_dir, + capture_output=True, + text=True, + timeout=float(self._timeout), + env=env, + encoding="utf-8", ) except subprocess.TimeoutExpired: logs_all += "\n" + TIMEOUT_MSG diff --git a/autogen/logger/file_logger.py b/autogen/logger/file_logger.py index af5583587f66..07c9c3b76a76 100644 --- a/autogen/logger/file_logger.py +++ b/autogen/logger/file_logger.py @@ -18,7 +18,10 @@ if TYPE_CHECKING: from autogen import Agent, ConversableAgent, OpenAIWrapper from autogen.oai.anthropic import AnthropicClient + from autogen.oai.bedrock import BedrockClient + from autogen.oai.cohere import CohereClient from autogen.oai.gemini import GeminiClient + from autogen.oai.groq import GroqClient from autogen.oai.mistral import MistralAIClient from autogen.oai.together import TogetherClient @@ -87,7 +90,7 @@ def log_chat_completion( thread_id = threading.get_ident() source_name = None if isinstance(source, str): - source_name = source + source_name = getattr(source, "name", "unknown") else: source_name = source.name try: @@ -204,7 +207,17 @@ def log_new_wrapper( def log_new_client( self, - client: AzureOpenAI | OpenAI | GeminiClient | AnthropicClient | MistralAIClient | TogetherClient, + client: ( + AzureOpenAI + | OpenAI + | GeminiClient + | AnthropicClient + | MistralAIClient + | TogetherClient + | GroqClient + | CohereClient + | BedrockClient + ), wrapper: OpenAIWrapper, init_args: Dict[str, Any], ) -> None: diff --git a/autogen/logger/sqlite_logger.py b/autogen/logger/sqlite_logger.py index 969a943017e3..f76d039ce9de 100644 --- a/autogen/logger/sqlite_logger.py +++ b/autogen/logger/sqlite_logger.py @@ -19,7 +19,10 @@ if TYPE_CHECKING: from autogen import Agent, ConversableAgent, OpenAIWrapper from autogen.oai.anthropic import AnthropicClient + from autogen.oai.bedrock import BedrockClient + from autogen.oai.cohere import CohereClient from autogen.oai.gemini import GeminiClient + from autogen.oai.groq import GroqClient from autogen.oai.mistral import MistralAIClient from autogen.oai.together import TogetherClient @@ -391,7 +394,17 @@ def log_function_use(self, source: Union[str, Agent], function: F, args: Dict[st def log_new_client( self, - client: Union[AzureOpenAI, OpenAI, GeminiClient, AnthropicClient, MistralAIClient, TogetherClient], + client: Union[ + AzureOpenAI, + OpenAI, + GeminiClient, + AnthropicClient, + MistralAIClient, + TogetherClient, + GroqClient, + CohereClient, + BedrockClient, + ], wrapper: OpenAIWrapper, init_args: Dict[str, Any], ) -> None: diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index e2448929e618..7bb4608bd258 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -16,6 +16,27 @@ ] assistant = autogen.AssistantAgent("assistant", llm_config={"config_list": config_list}) + +Example usage for Anthropic Bedrock: + +Install the `anthropic` package by running `pip install --upgrade anthropic`. +- https://docs.anthropic.com/en/docs/quickstart-guide + +import autogen + +config_list = [ + { + "model": "anthropic.claude-3-5-sonnet-20240620-v1:0", + "aws_access_key":<accessKey>, + "aws_secret_key":<secretKey>, + "aws_session_token":<sessionTok>, + "aws_region":"us-east-1", + "api_type": "anthropic", + } +] + +assistant = autogen.AssistantAgent("assistant", llm_config={"config_list": config_list}) + """ from __future__ import annotations @@ -28,7 +49,7 @@ import warnings from typing import Any, Dict, List, Tuple, Union -from anthropic import Anthropic +from anthropic import Anthropic, AnthropicBedrock from anthropic import __version__ as anthropic_version from anthropic.types import Completion, Message, TextBlock, ToolUseBlock from openai.types.chat import ChatCompletion, ChatCompletionMessageToolCall @@ -64,14 +85,38 @@ def __init__(self, **kwargs: Any): api_key (str): The API key for the Anthropic API or set the `ANTHROPIC_API_KEY` environment variable. """ self._api_key = kwargs.get("api_key", None) + self._aws_access_key = kwargs.get("aws_access_key", None) + self._aws_secret_key = kwargs.get("aws_secret_key", None) + self._aws_session_token = kwargs.get("aws_session_token", None) + self._aws_region = kwargs.get("aws_region", None) if not self._api_key: self._api_key = os.getenv("ANTHROPIC_API_KEY") - if self._api_key is None: - raise ValueError("API key is required to use the Anthropic API.") + if not self._aws_access_key: + self._aws_access_key = os.getenv("AWS_ACCESS_KEY") + + if not self._aws_secret_key: + self._aws_secret_key = os.getenv("AWS_SECRET_KEY") + + if not self._aws_region: + self._aws_region = os.getenv("AWS_REGION") + + if self._api_key is None and ( + self._aws_access_key is None or self._aws_secret_key is None or self._aws_region is None + ): + raise ValueError("API key or AWS credentials are required to use the Anthropic API.") + + if self._api_key is not None: + self._client = Anthropic(api_key=self._api_key) + else: + self._client = AnthropicBedrock( + aws_access_key=self._aws_access_key, + aws_secret_key=self._aws_secret_key, + aws_session_token=self._aws_session_token, + aws_region=self._aws_region, + ) - self._client = Anthropic(api_key=self._api_key) self._last_tooluse_status = {} def load_config(self, params: Dict[str, Any]): @@ -107,6 +152,22 @@ def cost(self, response) -> float: def api_key(self): return self._api_key + @property + def aws_access_key(self): + return self._aws_access_key + + @property + def aws_secret_key(self): + return self._aws_secret_key + + @property + def aws_session_token(self): + return self._aws_session_token + + @property + def aws_region(self): + return self._aws_region + def create(self, params: Dict[str, Any]) -> Completion: if "tools" in params: converted_functions = self.convert_tools_to_functions(params["tools"]) @@ -253,7 +314,7 @@ def oai_messages_to_anthropic_messages(params: Dict[str, Any]) -> list[dict[str, last_tool_result_index = -1 for message in params["messages"]: if message["role"] == "system": - params["system"] = message["content"] + params["system"] = params.get("system", "") + (" " if "system" in params else "") + message["content"] else: # New messages will be added here, manage role alternations expected_role = "user" if len(processed_messages) % 2 == 0 else "assistant" diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py new file mode 100644 index 000000000000..7894781e3ee5 --- /dev/null +++ b/autogen/oai/bedrock.py @@ -0,0 +1,606 @@ +""" +Create a compatible client for the Amazon Bedrock Converse API. + +Example usage: +Install the `boto3` package by running `pip install --upgrade boto3`. +- https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html + +import autogen + +config_list = [ + { + "api_type": "bedrock", + "model": "meta.llama3-1-8b-instruct-v1:0", + "aws_region": "us-west-2", + "aws_access_key": "", + "aws_secret_key": "", + "price" : [0.003, 0.015] + } +] + +assistant = autogen.AssistantAgent("assistant", llm_config={"config_list": config_list}) + +""" + +from __future__ import annotations + +import base64 +import json +import os +import re +import time +import warnings +from typing import Any, Dict, List, Literal, Tuple + +import boto3 +import requests +from botocore.config import Config +from openai.types.chat import ChatCompletion, ChatCompletionMessageToolCall +from openai.types.chat.chat_completion import ChatCompletionMessage, Choice +from openai.types.completion_usage import CompletionUsage + +from autogen.oai.client_utils import validate_parameter + + +class BedrockClient: + """Client for Amazon's Bedrock Converse API.""" + + _retries = 5 + + def __init__(self, **kwargs: Any): + """ + Initialises BedrockClient for Amazon's Bedrock Converse API + """ + self._aws_access_key = kwargs.get("aws_access_key", None) + self._aws_secret_key = kwargs.get("aws_secret_key", None) + self._aws_session_token = kwargs.get("aws_session_token", None) + self._aws_region = kwargs.get("aws_region", None) + self._aws_profile_name = kwargs.get("aws_profile_name", None) + + if not self._aws_access_key: + self._aws_access_key = os.getenv("AWS_ACCESS_KEY") + + if not self._aws_secret_key: + self._aws_secret_key = os.getenv("AWS_SECRET_KEY") + + if not self._aws_session_token: + self._aws_session_token = os.getenv("AWS_SESSION_TOKEN") + + if not self._aws_region: + self._aws_region = os.getenv("AWS_REGION") + + if self._aws_region is None: + raise ValueError("Region is required to use the Amazon Bedrock API.") + + # Initialize Bedrock client, session, and runtime + bedrock_config = Config( + region_name=self._aws_region, + signature_version="v4", + retries={"max_attempts": self._retries, "mode": "standard"}, + ) + + session = boto3.Session( + aws_access_key_id=self._aws_access_key, + aws_secret_access_key=self._aws_secret_key, + aws_session_token=self._aws_session_token, + profile_name=self._aws_profile_name, + ) + + self.bedrock_runtime = session.client(service_name="bedrock-runtime", config=bedrock_config) + + def message_retrieval(self, response): + """Retrieve the messages from the response.""" + return [choice.message for choice in response.choices] + + def parse_custom_params(self, params: Dict[str, Any]): + """ + Parses custom parameters for logic in this client class + """ + + # Should we separate system messages into its own request parameter, default is True + # This is required because not all models support a system prompt (e.g. Mistral Instruct). + self._supports_system_prompts = params.get("supports_system_prompts", True) + + def parse_params(self, params: Dict[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any]]: + """ + Loads the valid parameters required to invoke Bedrock Converse + Returns a tuple of (base_params, additional_params) + """ + + base_params = {} + additional_params = {} + + # Amazon Bedrock base model IDs are here: + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html + self._model_id = params.get("model", None) + assert self._model_id, "Please provide the 'model` in the config_list to use Amazon Bedrock" + + # Parameters vary based on the model used. + # As we won't cater for all models and parameters, it's the developer's + # responsibility to implement the parameters and they will only be + # included if the developer has it in the config. + # + # Important: + # No defaults will be used (as they can vary per model) + # No ranges will be used (as they can vary) + # We will cover all the main parameters but there may be others + # that need to be added later + # + # Here are some pages that show the parameters available for different models + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-text-completion.html + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-command-r-plus.html + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-chat-completion.html + + # Here are the possible "base" parameters and their suitable types + base_parameters = [["temperature", (float, int)], ["topP", (float, int)], ["maxTokens", (int)]] + + for param_name, suitable_types in base_parameters: + if param_name in params: + base_params[param_name] = validate_parameter( + params, param_name, suitable_types, False, None, None, None + ) + + # Here are the possible "model-specific" parameters and their suitable types, known as additional parameters + additional_parameters = [ + ["top_p", (float, int)], + ["top_k", (int)], + ["k", (int)], + ["seed", (int)], + ] + + for param_name, suitable_types in additional_parameters: + if param_name in params: + additional_params[param_name] = validate_parameter( + params, param_name, suitable_types, False, None, None, None + ) + + # Streaming + if "stream" in params: + self._streaming = params["stream"] + else: + self._streaming = False + + # For this release we will not support streaming as many models do not support streaming with tool use + if self._streaming: + warnings.warn( + "Streaming is not currently supported, streaming will be disabled.", + UserWarning, + ) + self._streaming = False + + return base_params, additional_params + + def create(self, params): + """Run Amazon Bedrock inference and return AutoGen response""" + + # Set custom client class settings + self.parse_custom_params(params) + + # Parse the inference parameters + base_params, additional_params = self.parse_params(params) + + has_tools = "tools" in params + messages = oai_messages_to_bedrock_messages(params["messages"], has_tools, self._supports_system_prompts) + + if self._supports_system_prompts: + system_messages = extract_system_messages(params["messages"]) + + tool_config = format_tools(params["tools"] if has_tools else []) + + request_args = {"messages": messages, "modelId": self._model_id} + + # Base and additional args + if len(base_params) > 0: + request_args["inferenceConfig"] = base_params + + if len(additional_params) > 0: + request_args["additionalModelRequestFields"] = additional_params + + if self._supports_system_prompts: + request_args["system"] = system_messages + + if len(tool_config["tools"]) > 0: + request_args["toolConfig"] = tool_config + + try: + response = self.bedrock_runtime.converse( + **request_args, + ) + except Exception as e: + raise RuntimeError(f"Failed to get response from Bedrock: {e}") + + if response is None: + raise RuntimeError(f"Failed to get response from Bedrock after retrying {self._retries} times.") + + finish_reason = convert_stop_reason_to_finish_reason(response["stopReason"]) + response_message = response["output"]["message"] + + if finish_reason == "tool_calls": + tool_calls = format_tool_calls(response_message["content"]) + # text = "" + else: + tool_calls = None + + text = "" + for content in response_message["content"]: + if "text" in content: + text = content["text"] + # NOTE: other types of output may be dealt with here + + message = ChatCompletionMessage(role="assistant", content=text, tool_calls=tool_calls) + + response_usage = response["usage"] + usage = CompletionUsage( + prompt_tokens=response_usage["inputTokens"], + completion_tokens=response_usage["outputTokens"], + total_tokens=response_usage["totalTokens"], + ) + + return ChatCompletion( + id=response["ResponseMetadata"]["RequestId"], + choices=[Choice(finish_reason=finish_reason, index=0, message=message)], + created=int(time.time()), + model=self._model_id, + object="chat.completion", + usage=usage, + ) + + def cost(self, response: ChatCompletion) -> float: + """Calculate the cost of the response.""" + return calculate_cost(response.usage.prompt_tokens, response.usage.completion_tokens, response.model) + + @staticmethod + def get_usage(response) -> Dict: + """Get the usage of tokens and their cost information.""" + return { + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens, + "cost": response.cost, + "model": response.model, + } + + +def extract_system_messages(messages: List[dict]) -> List: + """Extract the system messages from the list of messages. + + Args: + messages (list[dict]): List of messages. + + Returns: + List[SystemMessage]: List of System messages. + """ + + """ + system_messages = [message.get("content")[0]["text"] for message in messages if message.get("role") == "system"] + return system_messages # ''.join(system_messages) + """ + + for message in messages: + if message.get("role") == "system": + if isinstance(message["content"], str): + return [{"text": message.get("content")}] + else: + return [{"text": message.get("content")[0]["text"]}] + return [] + + +def oai_messages_to_bedrock_messages( + messages: List[Dict[str, Any]], has_tools: bool, supports_system_prompts: bool +) -> List[Dict]: + """ + Convert messages from OAI format to Bedrock format. + We correct for any specific role orders and types, etc. + AWS Bedrock requires messages to alternate between user and assistant roles. This function ensures that the messages + are in the correct order and format for Bedrock by inserting "Please continue" messages as needed. + This is the same method as the one in the Autogen Anthropic client + """ + + # Track whether we have tools passed in. If not, tool use / result messages should be converted to text messages. + # Bedrock requires a tools parameter with the tools listed, if there are other messages with tool use or tool results. + # This can occur when we don't need tool calling, such as for group chat speaker selection + + # Convert messages to Bedrock compliant format + + # Take out system messages if the model supports it, otherwise leave them in. + if supports_system_prompts: + messages = [x for x in messages if not x["role"] == "system"] + else: + # Replace role="system" with role="user" + for msg in messages: + if msg["role"] == "system": + msg["role"] = "user" + + processed_messages = [] + + # Used to interweave user messages to ensure user/assistant alternating + user_continue_message = {"content": [{"text": "Please continue."}], "role": "user"} + assistant_continue_message = { + "content": [{"text": "Please continue."}], + "role": "assistant", + } + + tool_use_messages = 0 + tool_result_messages = 0 + last_tool_use_index = -1 + last_tool_result_index = -1 + # user_role_index = 0 if supports_system_prompts else 1 # If system prompts are supported, messages start with user, otherwise they'll be the second message + for message in messages: + # New messages will be added here, manage role alternations + expected_role = "user" if len(processed_messages) % 2 == 0 else "assistant" + + if "tool_calls" in message: + # Map the tool call options to Bedrock's format + tool_uses = [] + tool_names = [] + for tool_call in message["tool_calls"]: + tool_uses.append( + { + "toolUse": { + "toolUseId": tool_call["id"], + "name": tool_call["function"]["name"], + "input": json.loads(tool_call["function"]["arguments"]), + } + } + ) + if has_tools: + tool_use_messages += 1 + tool_names.append(tool_call["function"]["name"]) + + if expected_role == "user": + # Insert an extra user message as we will append an assistant message + processed_messages.append(user_continue_message) + + if has_tools: + processed_messages.append({"role": "assistant", "content": tool_uses}) + last_tool_use_index = len(processed_messages) - 1 + else: + # Not using tools, so put in a plain text message + processed_messages.append( + { + "role": "assistant", + "content": [ + {"text": f"Some internal function(s) that could be used: [{', '.join(tool_names)}]"} + ], + } + ) + elif "tool_call_id" in message: + if has_tools: + # Map the tool usage call to tool_result for Bedrock + tool_result = { + "toolResult": { + "toolUseId": message["tool_call_id"], + "content": [{"text": message["content"]}], + } + } + + # If the previous message also had a tool_result, add it to that + # Otherwise append a new message + if last_tool_result_index == len(processed_messages) - 1: + processed_messages[-1]["content"].append(tool_result) + else: + if expected_role == "assistant": + # Insert an extra assistant message as we will append a user message + processed_messages.append(assistant_continue_message) + + processed_messages.append({"role": "user", "content": [tool_result]}) + last_tool_result_index = len(processed_messages) - 1 + + tool_result_messages += 1 + else: + # Not using tools, so put in a plain text message + processed_messages.append( + { + "role": "user", + "content": [{"text": f"Running the function returned: {message['content']}"}], + } + ) + elif message["content"] == "": + # Ignoring empty messages + pass + else: + if expected_role != message["role"] and not (len(processed_messages) == 0 and message["role"] == "system"): + # Inserting the alternating continue message (ignore if it's the first message and a system message) + processed_messages.append( + user_continue_message if expected_role == "user" else assistant_continue_message + ) + + processed_messages.append( + { + "role": message["role"], + "content": parse_content_parts(message=message), + } + ) + + # We'll replace the last tool_use if there's no tool_result (occurs if we finish the conversation before running the function) + if has_tools and tool_use_messages != tool_result_messages: + processed_messages[last_tool_use_index] = assistant_continue_message + + # name is not a valid field on messages + for message in processed_messages: + if "name" in message: + message.pop("name", None) + + # Note: When using reflection_with_llm we may end up with an "assistant" message as the last message and that may cause a blank response + # So, if the last role is not user, add a 'user' continue message at the end + if processed_messages[-1]["role"] != "user": + processed_messages.append(user_continue_message) + + return processed_messages + + +def parse_content_parts( + message: Dict[str, Any], +) -> List[dict]: + content: str | List[Dict[str, Any]] = message.get("content") + if isinstance(content, str): + return [ + { + "text": content, + } + ] + content_parts = [] + for part in content: + # part_content: Dict = part.get("content") + if "text" in part: # part_content: + content_parts.append( + { + "text": part.get("text"), + } + ) + elif "image_url" in part: # part_content: + image_data, content_type = parse_image(part.get("image_url").get("url")) + content_parts.append( + { + "image": { + "format": content_type[6:], # image/ + "source": {"bytes": image_data}, + }, + } + ) + else: + # Ignore.. + continue + return content_parts + + +def parse_image(image_url: str) -> Tuple[bytes, str]: + """Try to get the raw data from an image url. + + Ref: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageSource.html + returns a tuple of (Image Data, Content Type) + """ + pattern = r"^data:(image/[a-z]*);base64,\s*" + content_type = re.search(pattern, image_url) + # if already base64 encoded. + # Only supports 'image/jpeg', 'image/png', 'image/gif' or 'image/webp' + if content_type: + image_data = re.sub(pattern, "", image_url) + return base64.b64decode(image_data), content_type.group(1) + + # Send a request to the image URL + response = requests.get(image_url) + # Check if the request was successful + if response.status_code == 200: + + content_type = response.headers.get("Content-Type") + if not content_type.startswith("image"): + content_type = "image/jpeg" + # Get the image content + image_content = response.content + return image_content, content_type + else: + raise RuntimeError("Unable to access the image url") + + +def format_tools(tools: List[Dict[str, Any]]) -> Dict[Literal["tools"], List[Dict[str, Any]]]: + converted_schema = {"tools": []} + + for tool in tools: + if tool["type"] == "function": + function = tool["function"] + converted_tool = { + "toolSpec": { + "name": function["name"], + "description": function["description"], + "inputSchema": {"json": {"type": "object", "properties": {}, "required": []}}, + } + } + + for prop_name, prop_details in function["parameters"]["properties"].items(): + converted_tool["toolSpec"]["inputSchema"]["json"]["properties"][prop_name] = { + "type": prop_details["type"], + "description": prop_details.get("description", ""), + } + if "enum" in prop_details: + converted_tool["toolSpec"]["inputSchema"]["json"]["properties"][prop_name]["enum"] = prop_details[ + "enum" + ] + if "default" in prop_details: + converted_tool["toolSpec"]["inputSchema"]["json"]["properties"][prop_name]["default"] = ( + prop_details["default"] + ) + + if "required" in function["parameters"]: + converted_tool["toolSpec"]["inputSchema"]["json"]["required"] = function["parameters"]["required"] + + converted_schema["tools"].append(converted_tool) + + return converted_schema + + +def format_tool_calls(content): + """Converts Converse API response tool calls to AutoGen format""" + tool_calls = [] + for tool_request in content: + if "toolUse" in tool_request: + tool = tool_request["toolUse"] + + tool_calls.append( + ChatCompletionMessageToolCall( + id=tool["toolUseId"], + function={ + "name": tool["name"], + "arguments": json.dumps(tool["input"]), + }, + type="function", + ) + ) + return tool_calls + + +def convert_stop_reason_to_finish_reason( + stop_reason: str, +) -> Literal["stop", "length", "tool_calls", "content_filter"]: + """ + Converts Bedrock finish reasons to our finish reasons, according to OpenAI: + + - stop: if the model hit a natural stop point or a provided stop sequence, + - length: if the maximum number of tokens specified in the request was reached, + - content_filter: if content was omitted due to a flag from our content filters, + - tool_calls: if the model called a tool + """ + if stop_reason: + finish_reason_mapping = { + "tool_use": "tool_calls", + "finished": "stop", + "end_turn": "stop", + "max_tokens": "length", + "stop_sequence": "stop", + "complete": "stop", + "content_filtered": "content_filter", + } + return finish_reason_mapping.get(stop_reason.lower(), stop_reason.lower()) + + warnings.warn(f"Unsupported stop reason: {stop_reason}", UserWarning) + return None + + +# NOTE: As this will be quite dynamic, it's expected that the developer will use the "price" parameter in their config +# These may be removed. +PRICES_PER_K_TOKENS = { + "meta.llama3-8b-instruct-v1:0": (0.0003, 0.0006), + "meta.llama3-70b-instruct-v1:0": (0.00265, 0.0035), + "mistral.mistral-7b-instruct-v0:2": (0.00015, 0.0002), + "mistral.mixtral-8x7b-instruct-v0:1": (0.00045, 0.0007), + "mistral.mistral-large-2402-v1:0": (0.004, 0.012), + "mistral.mistral-small-2402-v1:0": (0.001, 0.003), +} + + +def calculate_cost(input_tokens: int, output_tokens: int, model_id: str) -> float: + """Calculate the cost of the completion using the Bedrock pricing.""" + + if model_id in PRICES_PER_K_TOKENS: + input_cost_per_k, output_cost_per_k = PRICES_PER_K_TOKENS[model_id] + input_cost = (input_tokens / 1000) * input_cost_per_k + output_cost = (output_tokens / 1000) * output_cost_per_k + return input_cost + output_cost + else: + warnings.warn( + f'Cannot get the costs for {model_id}. The cost will be 0. In your config_list, add field {{"price" : [prompt_price_per_1k, completion_token_price_per_1k]}} for customized pricing.', + UserWarning, + ) + return 0 diff --git a/autogen/oai/client.py b/autogen/oai/client.py index 2c14ca0d4a0c..8f6e3f185b6a 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -70,6 +70,27 @@ except ImportError as e: together_import_exception = e +try: + from autogen.oai.groq import GroqClient + + groq_import_exception: Optional[ImportError] = None +except ImportError as e: + groq_import_exception = e + +try: + from autogen.oai.cohere import CohereClient + + cohere_import_exception: Optional[ImportError] = None +except ImportError as e: + cohere_import_exception = e + +try: + from autogen.oai.bedrock import BedrockClient + + bedrock_import_exception: Optional[ImportError] = None +except ImportError as e: + bedrock_import_exception = e + logger = logging.getLogger(__name__) if not logger.handlers: # Add the console handler. @@ -258,7 +279,12 @@ def create(self, params: Dict[str, Any]) -> ChatCompletion: # Prepare the final ChatCompletion object based on the accumulated data model = chunk.model.replace("gpt-35", "gpt-3.5") # hack for Azure API - prompt_tokens = count_token(params["messages"], model) + try: + prompt_tokens = count_token(params["messages"], model) + except NotImplementedError as e: + # Catch token calculation error if streaming with customized models. + logger.warning(str(e)) + prompt_tokens = 0 response = ChatCompletion( id=chunk.id, model=chunk.model, @@ -440,12 +466,23 @@ def _configure_azure_openai(self, config: Dict[str, Any], openai_config: Dict[st azure.identity.DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" ) + def _configure_openai_config_for_bedrock(self, config: Dict[str, Any], openai_config: Dict[str, Any]) -> None: + """Update openai_config with AWS credentials from config.""" + required_keys = ["aws_access_key", "aws_secret_key", "aws_region"] + optional_keys = ["aws_session_token", "aws_profile_name"] + for key in required_keys: + if key in config: + openai_config[key] = config[key] + for key in optional_keys: + if key in config: + openai_config[key] = config[key] + def _register_default_client(self, config: Dict[str, Any], openai_config: Dict[str, Any]) -> None: """Create a client with the given config to override openai_config, after removing extra kwargs. For Azure models/deployment names there's a convenience modification of model removing dots in - the it's value (Azure deploment names can't have dots). I.e. if you have Azure deployment name + the it's value (Azure deployment names can't have dots). I.e. if you have Azure deployment name "gpt-35-turbo" and define model "gpt-3.5-turbo" in the config the function will remove the dot from the name and create a client that connects to "gpt-35-turbo" Azure deployment. """ @@ -471,6 +508,8 @@ def _register_default_client(self, config: Dict[str, Any], openai_config: Dict[s client = GeminiClient(**openai_config) self._clients.append(client) elif api_type is not None and api_type.startswith("anthropic"): + if "api_key" not in config: + self._configure_openai_config_for_bedrock(config, openai_config) if anthropic_import_exception: raise ImportError("Please install `anthropic` to use Anthropic API.") client = AnthropicClient(**openai_config) @@ -483,7 +522,24 @@ def _register_default_client(self, config: Dict[str, Any], openai_config: Dict[s elif api_type is not None and api_type.startswith("together"): if together_import_exception: raise ImportError("Please install `together` to use the Together.AI API.") - self._clients.append(TogetherClient(**config)) + client = TogetherClient(**openai_config) + self._clients.append(client) + elif api_type is not None and api_type.startswith("groq"): + if groq_import_exception: + raise ImportError("Please install `groq` to use the Groq API.") + client = GroqClient(**openai_config) + self._clients.append(client) + elif api_type is not None and api_type.startswith("cohere"): + if cohere_import_exception: + raise ImportError("Please install `cohere` to use the Cohere API.") + client = CohereClient(**openai_config) + self._clients.append(client) + elif api_type is not None and api_type.startswith("bedrock"): + self._configure_openai_config_for_bedrock(config, openai_config) + if bedrock_import_exception: + raise ImportError("Please install `boto3` to use the Amazon Bedrock API.") + client = BedrockClient(**openai_config) + self._clients.append(client) else: client = OpenAI(**openai_config) self._clients.append(OpenAIClient(client)) @@ -770,7 +826,7 @@ def _cost_with_customized_price( n_output_tokens = response.usage.completion_tokens if response.usage is not None else 0 # type: ignore [union-attr] if n_output_tokens is None: n_output_tokens = 0 - return n_input_tokens * price_1k[0] + n_output_tokens * price_1k[1] + return (n_input_tokens * price_1k[0] + n_output_tokens * price_1k[1]) / 1000 @staticmethod def _update_dict_from_chunk(chunk: BaseModel, d: Dict[str, Any], field: str) -> int: diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py new file mode 100644 index 000000000000..e9a89c9cabd8 --- /dev/null +++ b/autogen/oai/cohere.py @@ -0,0 +1,516 @@ +"""Create an OpenAI-compatible client using Cohere's API. + +Example: + llm_config={ + "config_list": [{ + "api_type": "cohere", + "model": "command-r-plus", + "api_key": os.environ.get("COHERE_API_KEY") + "client_name": "autogen-cohere", # Optional parameter + } + ]} + + agent = autogen.AssistantAgent("my_agent", llm_config=llm_config) + +Install Cohere's python library using: pip install --upgrade cohere + +Resources: +- https://docs.cohere.com/reference/chat +""" + +from __future__ import annotations + +import json +import logging +import os +import random +import sys +import time +import warnings +from typing import Any, Dict, List + +from cohere import Client as Cohere +from cohere.types import ToolParameterDefinitionsValue, ToolResult +from flaml.automl.logger import logger_formatter +from openai.types.chat import ChatCompletion, ChatCompletionMessageToolCall +from openai.types.chat.chat_completion import ChatCompletionMessage, Choice +from openai.types.completion_usage import CompletionUsage + +from autogen.oai.client_utils import validate_parameter + +logger = logging.getLogger(__name__) +if not logger.handlers: + # Add the console handler. + _ch = logging.StreamHandler(stream=sys.stdout) + _ch.setFormatter(logger_formatter) + logger.addHandler(_ch) + + +COHERE_PRICING_1K = { + "command-r-plus": (0.003, 0.015), + "command-r": (0.0005, 0.0015), + "command-nightly": (0.00025, 0.00125), + "command": (0.015, 0.075), + "command-light": (0.008, 0.024), + "command-light-nightly": (0.008, 0.024), +} + + +class CohereClient: + """Client for Cohere's API.""" + + def __init__(self, **kwargs): + """Requires api_key or environment variable to be set + + Args: + api_key (str): The API key for using Cohere (or environment variable COHERE_API_KEY needs to be set) + """ + # Ensure we have the api_key upon instantiation + self.api_key = kwargs.get("api_key", None) + if not self.api_key: + self.api_key = os.getenv("COHERE_API_KEY") + + assert ( + self.api_key + ), "Please include the api_key in your config list entry for Cohere or set the COHERE_API_KEY env variable." + + def message_retrieval(self, response) -> List: + """ + Retrieve and return a list of strings or a list of Choice.Message from the response. + + NOTE: if a list of Choice.Message is returned, it currently needs to contain the fields of OpenAI's ChatCompletion Message object, + since that is expected for function or tool calling in the rest of the codebase at the moment, unless a custom agent is being used. + """ + return [choice.message for choice in response.choices] + + def cost(self, response) -> float: + return response.cost + + @staticmethod + def get_usage(response) -> Dict: + """Return usage summary of the response using RESPONSE_USAGE_KEYS.""" + # ... # pragma: no cover + return { + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens, + "cost": response.cost, + "model": response.model, + } + + def parse_params(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Loads the parameters for Cohere API from the passed in parameters and returns a validated set. Checks types, ranges, and sets defaults""" + cohere_params = {} + + # Check that we have what we need to use Cohere's API + # We won't enforce the available models as they are likely to change + cohere_params["model"] = params.get("model", None) + assert cohere_params[ + "model" + ], "Please specify the 'model' in your config list entry to nominate the Cohere model to use." + + # Validate allowed Cohere parameters + # https://docs.cohere.com/reference/chat + cohere_params["temperature"] = validate_parameter( + params, "temperature", (int, float), False, 0.3, (0, None), None + ) + cohere_params["max_tokens"] = validate_parameter(params, "max_tokens", int, True, None, (0, None), None) + cohere_params["k"] = validate_parameter(params, "k", int, False, 0, (0, 500), None) + cohere_params["p"] = validate_parameter(params, "p", (int, float), False, 0.75, (0.01, 0.99), None) + cohere_params["seed"] = validate_parameter(params, "seed", int, True, None, None, None) + cohere_params["frequency_penalty"] = validate_parameter( + params, "frequency_penalty", (int, float), True, 0, (0, 1), None + ) + cohere_params["presence_penalty"] = validate_parameter( + params, "presence_penalty", (int, float), True, 0, (0, 1), None + ) + + # Cohere parameters we are ignoring: + # preamble - we will put the system prompt in here. + # parallel_tool_calls (defaults to True), perfect as is. + # conversation_id - allows resuming a previous conversation, we don't support this. + logging.info("Conversation ID: %s", params.get("conversation_id", "None")) + # connectors - allows web search or other custom connectors, not implementing for now but could be useful in the future. + logging.info("Connectors: %s", params.get("connectors", "None")) + # search_queries_only - to control whether only search queries are used, we're not using connectors so ignoring. + # documents - a list of documents that can be used to support the chat. Perhaps useful in the future for RAG. + # citation_quality - used for RAG flows and dependent on other parameters we're ignoring. + # max_input_tokens - limits input tokens, not needed. + logging.info("Max Input Tokens: %s", params.get("max_input_tokens", "None")) + # stop_sequences - used to stop generation, not needed. + logging.info("Stop Sequences: %s", params.get("stop_sequences", "None")) + + return cohere_params + + def create(self, params: Dict) -> ChatCompletion: + + messages = params.get("messages", []) + client_name = params.get("client_name") or "autogen-cohere" + # Parse parameters to the Cohere API's parameters + cohere_params = self.parse_params(params) + # Convert AutoGen messages to Cohere messages + cohere_messages, preamble, final_message = oai_messages_to_cohere_messages(messages, params, cohere_params) + + cohere_params["chat_history"] = cohere_messages + cohere_params["message"] = final_message + cohere_params["preamble"] = preamble + + # We use chat model by default + client = Cohere(api_key=self.api_key, client_name=client_name) + + # Token counts will be returned + prompt_tokens = 0 + completion_tokens = 0 + total_tokens = 0 + + # Stream if in parameters + streaming = True if "stream" in params and params["stream"] else False + cohere_finish = "" + + max_retries = 5 + + for attempt in range(max_retries): + ans = None + try: + if streaming: + response = client.chat_stream(**cohere_params) + else: + response = client.chat(**cohere_params) + + except CohereRateLimitError as e: + raise RuntimeError(f"Cohere exception occurred: {e}") + else: + + if streaming: + # Streaming... + ans = "" + for event in response: + if event.event_type == "text-generation": + ans = ans + event.text + elif event.event_type == "tool-calls-generation": + # When streaming, tool calls are compiled at the end into a single event_type + ans = event.text + cohere_finish = "tool_calls" + tool_calls = [] + for tool_call in event.tool_calls: + tool_calls.append( + ChatCompletionMessageToolCall( + id=str(random.randint(0, 100000)), + function={ + "name": tool_call.name, + "arguments": ( + "" if tool_call.parameters is None else json.dumps(tool_call.parameters) + ), + }, + type="function", + ) + ) + + # Not using billed_units, but that may be better for cost purposes + prompt_tokens = event.response.meta.tokens.input_tokens + completion_tokens = event.response.meta.tokens.output_tokens + total_tokens = prompt_tokens + completion_tokens + + response_id = event.response.response_id + else: + # Non-streaming finished + ans: str = response.text + + # Not using billed_units, but that may be better for cost purposes + prompt_tokens = response.meta.tokens.input_tokens + completion_tokens = response.meta.tokens.output_tokens + total_tokens = prompt_tokens + completion_tokens + + response_id = response.response_id + break + + if response is not None: + + response_content = ans + + if streaming: + # Streaming response + if cohere_finish == "": + cohere_finish = "stop" + tool_calls = None + else: + # Non-streaming response + # If we have tool calls as the response, populate completed tool calls for our return OAI response + if response.tool_calls is not None: + cohere_finish = "tool_calls" + tool_calls = [] + for tool_call in response.tool_calls: + + # if parameters are null, clear them out (Cohere can return a string "null" if no parameter values) + + tool_calls.append( + ChatCompletionMessageToolCall( + id=str(random.randint(0, 100000)), + function={ + "name": tool_call.name, + "arguments": ( + "" if tool_call.parameters is None else json.dumps(tool_call.parameters) + ), + }, + type="function", + ) + ) + else: + cohere_finish = "stop" + tool_calls = None + else: + raise RuntimeError(f"Failed to get response from Cohere after retrying {attempt + 1} times.") + + # 3. convert output + message = ChatCompletionMessage( + role="assistant", + content=response_content, + function_call=None, + tool_calls=tool_calls, + ) + choices = [Choice(finish_reason=cohere_finish, index=0, message=message)] + + response_oai = ChatCompletion( + id=response_id, + model=cohere_params["model"], + created=int(time.time()), + object="chat.completion", + choices=choices, + usage=CompletionUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + ), + cost=calculate_cohere_cost(prompt_tokens, completion_tokens, cohere_params["model"]), + ) + + return response_oai + + +def extract_to_cohere_tool_results(tool_call_id: str, content_output: str, all_tool_calls) -> List[Dict[str, Any]]: + temp_tool_results = [] + + for tool_call in all_tool_calls: + if tool_call["id"] == tool_call_id: + + call = { + "name": tool_call["function"]["name"], + "parameters": json.loads( + tool_call["function"]["arguments"] if not tool_call["function"]["arguments"] == "" else "{}" + ), + } + output = [{"value": content_output}] + temp_tool_results.append(ToolResult(call=call, outputs=output)) + return temp_tool_results + + +def is_recent_tool_call(messages: list[Dict[str, Any]], tool_call_index: int): + messages_length = len(messages) + if tool_call_index == messages_length - 1: + return True + elif messages[tool_call_index + 1].get("role", "").lower() not in ("chatbot"): + return True + return False + + +def oai_messages_to_cohere_messages( + messages: list[Dict[str, Any]], params: Dict[str, Any], cohere_params: Dict[str, Any] +) -> tuple[list[dict[str, Any]], str, str]: + """Convert messages from OAI format to Cohere's format. + We correct for any specific role orders and types. + + Parameters: + messages: list[Dict[str, Any]]: AutoGen messages + params: Dict[str, Any]: AutoGen parameters dictionary + cohere_params: Dict[str, Any]: Cohere parameters dictionary + + Returns: + List[Dict[str, Any]]: Chat History messages + str: Preamble (system message) + str: Message (the final user message) + """ + + cohere_messages = [] + preamble = "" + cohere_tool_names = set() + # Tools + if "tools" in params: + cohere_tools = [] + for tool in params["tools"]: + + # build list of properties + parameters = {} + + for key, value in tool["function"]["parameters"]["properties"].items(): + type_str = value["type"] + required = True # Defaults to False, we could consider leaving it as default. + description = value["description"] + + # If we have an 'enum' key, add that to the description (as not allowed to pass in enum as a field) + if "enum" in value: + # Access the enum list + enum_values = value["enum"] + enum_strings = [str(value) for value in enum_values] + enum_string = ", ".join(enum_strings) + description = description + ". Possible values are " + enum_string + "." + + parameters[key] = ToolParameterDefinitionsValue( + description=description, type=type_str, required=required + ) + + cohere_tool = { + "name": tool["function"]["name"], + "description": tool["function"]["description"], + "parameter_definitions": parameters, + } + cohere_tool_names.add(tool["function"]["name"] or "") + + cohere_tools.append(cohere_tool) + + if len(cohere_tools) > 0: + cohere_params["tools"] = cohere_tools + + tool_calls = [] + tool_results = [] + + # Rules for cohere messages: + # no 'name' field + # 'system' messages go into the preamble parameter + # user role = 'USER' + # assistant role = 'CHATBOT' + # 'content' field renamed to 'message' + # tools go into tools parameter + # tool_results go into tool_results parameter + for index, message in enumerate(messages): + + if not message["content"]: + continue + + if "role" in message and message["role"] == "system": + # System message + if preamble == "": + preamble = message["content"] + else: + preamble = preamble + "\n" + message["content"] + + elif message.get("tool_calls"): + # Suggested tool calls, build up the list before we put it into the tool_results + message_tool_calls = [] + for tool_call in message["tool_calls"] or []: + if (not tool_call.get("function", {}).get("name")) or tool_call.get("function", {}).get( + "name" + ) not in cohere_tool_names: + new_message = { + "role": "CHATBOT", + "message": message.get("name") + ":" + message["content"] + str(message["tool_calls"]), + } + cohere_messages.append(new_message) + continue + + tool_calls.append(tool_call) + message_tool_calls.append( + { + "name": tool_call.get("function", {}).get("name"), + "parameters": json.loads(tool_call.get("function", {}).get("arguments") or "null"), + } + ) + + if not message_tool_calls: + continue + + # We also add the suggested tool call as a message + new_message = { + "role": "CHATBOT", + "message": message.get("name") + ":" + message["content"], + "tool_calls": message_tool_calls, + } + + cohere_messages.append(new_message) + elif "role" in message and message["role"] == "tool": + if not (tool_call_id := message.get("tool_call_id")): + continue + + content_output = message["content"] + if tool_call_id not in [tool_call["id"] for tool_call in tool_calls]: + + new_message = { + "role": "CHATBOT", + "message": content_output, + } + cohere_messages.append(new_message) + continue + + # Convert the tool call to a result + tool_results_chat_turn = extract_to_cohere_tool_results(tool_call_id, content_output, tool_calls) + if is_recent_tool_call(messages, index): + # If the tool call is the last message or the next message is a user/tool message, this is a recent tool call. + # So, we pass it into tool_results. + tool_results.extend(tool_results_chat_turn) + continue + + else: + # If its not the current tool call, we pass it as a tool message in the chat history. + new_message = {"role": "TOOL", "tool_results": tool_results_chat_turn} + cohere_messages.append(new_message) + + elif "content" in message and isinstance(message["content"], str): + # Standard text message + new_message = { + "role": "USER" if message["role"] == "user" else "CHATBOT", + "message": message.get("name") + ":" + message.get("content"), + } + + cohere_messages.append(new_message) + + # Append any Tool Results + if len(tool_results) != 0: + cohere_params["tool_results"] = tool_results + + # Enable multi-step tool use: https://docs.cohere.com/docs/multi-step-tool-use + cohere_params["force_single_step"] = False + + # If we're adding tool_results, like we are, the last message can't be a USER message + # So, we add a CHATBOT 'continue' message, if so. + # Changed key from "content" to "message" (jaygdesai/autogen_Jay) + if cohere_messages[-1]["role"].lower() == "user": + cohere_messages.append({"role": "CHATBOT", "message": "Please go ahead and follow the instructions!"}) + + # We return a blank message when we have tool results + # TODO: Check what happens if tool_results aren't the latest message + return cohere_messages, preamble, "" + + else: + + # We need to get the last message to assign to the message field for Cohere, + # if the last message is a user message, use that, otherwise put in 'continue'. + if cohere_messages[-1]["role"] == "USER": + return cohere_messages[0:-1], preamble, cohere_messages[-1]["message"] + else: + return cohere_messages, preamble, "Please go ahead and follow the instructions!" + + +def calculate_cohere_cost(input_tokens: int, output_tokens: int, model: str) -> float: + """Calculate the cost of the completion using the Cohere pricing.""" + total = 0.0 + + if model in COHERE_PRICING_1K: + input_cost_per_k, output_cost_per_k = COHERE_PRICING_1K[model] + input_cost = (input_tokens / 1000) * input_cost_per_k + output_cost = (output_tokens / 1000) * output_cost_per_k + total = input_cost + output_cost + else: + warnings.warn(f"Cost calculation not available for {model} model", UserWarning) + + return total + + +class CohereError(Exception): + """Base class for other Cohere exceptions""" + + pass + + +class CohereRateLimitError(CohereError): + """Raised when rate limit is exceeded""" + + pass diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index 8babb8727e3c..33790c9851c6 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -6,7 +6,7 @@ "config_list": [{ "api_type": "google", "model": "gemini-pro", - "api_key": os.environ.get("GOOGLE_API_KEY"), + "api_key": os.environ.get("GOOGLE_GEMINI_API_KEY"), "safety_settings": [ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_ONLY_HIGH"}, @@ -32,6 +32,7 @@ from __future__ import annotations import base64 +import logging import os import random import re @@ -45,13 +46,19 @@ import vertexai from google.ai.generativelanguage import Content, Part from google.api_core.exceptions import InternalServerError +from google.auth.credentials import Credentials from openai.types.chat import ChatCompletion from openai.types.chat.chat_completion import ChatCompletionMessage, Choice from openai.types.completion_usage import CompletionUsage from PIL import Image from vertexai.generative_models import Content as VertexAIContent from vertexai.generative_models import GenerativeModel +from vertexai.generative_models import HarmBlockThreshold as VertexAIHarmBlockThreshold +from vertexai.generative_models import HarmCategory as VertexAIHarmCategory from vertexai.generative_models import Part as VertexAIPart +from vertexai.generative_models import SafetySetting as VertexAISafetySetting + +logger = logging.getLogger(__name__) class GeminiClient: @@ -72,7 +79,7 @@ class GeminiClient: "max_output_tokens": "max_output_tokens", } - def _initialize_vartexai(self, **params): + def _initialize_vertexai(self, **params): if "google_application_credentials" in params: # Path to JSON Keyfile os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = params["google_application_credentials"] @@ -81,32 +88,39 @@ def _initialize_vartexai(self, **params): vertexai_init_args["project"] = params["project_id"] if "location" in params: vertexai_init_args["location"] = params["location"] + if "credentials" in params: + assert isinstance( + params["credentials"], Credentials + ), "Object type google.auth.credentials.Credentials is expected!" + vertexai_init_args["credentials"] = params["credentials"] if vertexai_init_args: vertexai.init(**vertexai_init_args) def __init__(self, **kwargs): """Uses either either api_key for authentication from the LLM config - (specifying the GOOGLE_API_KEY environment variable also works), + (specifying the GOOGLE_GEMINI_API_KEY environment variable also works), or follows the Google authentication mechanism for VertexAI in Google Cloud if no api_key is specified, - where project_id and location can also be passed as parameters. Service account key file can also be used. - If neither a service account key file, nor the api_key are passed, then the default credentials will be used, - which could be a personal account if the user is already authenticated in, like in Google Cloud Shell. + where project_id and location can also be passed as parameters. Previously created credentials object can be provided, + or a Service account key file can also be used. If neither a service account key file, nor the api_key are passed, + then the default credentials will be used, which could be a personal account if the user is already authenticated in, + like in Google Cloud Shell. Args: api_key (str): The API key for using Gemini. + credentials (google.auth.credentials.Credentials): credentials to be used for authentication with vertexai. google_application_credentials (str): Path to the JSON service account key file of the service account. - Alternatively, the GOOGLE_APPLICATION_CREDENTIALS environment variable - can also be set instead of using this argument. + Alternatively, the GOOGLE_APPLICATION_CREDENTIALS environment variable + can also be set instead of using this argument. project_id (str): Google Cloud project id, which is only valid in case no API key is specified. location (str): Compute region to be used, like 'us-west1'. - This parameter is only valid in case no API key is specified. + This parameter is only valid in case no API key is specified. """ self.api_key = kwargs.get("api_key", None) if not self.api_key: - self.api_key = os.getenv("GOOGLE_API_KEY") + self.api_key = os.getenv("GOOGLE_GEMINI_API_KEY") if self.api_key is None: self.use_vertexai = True - self._initialize_vartexai(**kwargs) + self._initialize_vertexai(**kwargs) else: self.use_vertexai = False else: @@ -142,7 +156,7 @@ def get_usage(response) -> Dict: def create(self, params: Dict) -> ChatCompletion: if self.use_vertexai: - self._initialize_vartexai(**params) + self._initialize_vertexai(**params) else: assert ("project_id" not in params) and ( "location" not in params @@ -159,13 +173,18 @@ def create(self, params: Dict) -> ChatCompletion: messages = params.get("messages", []) stream = params.get("stream", False) n_response = params.get("n", 1) + system_instruction = params.get("system_instruction", None) + response_validation = params.get("response_validation", True) generation_config = { gemini_term: params[autogen_term] for autogen_term, gemini_term in self.PARAMS_MAPPING.items() if autogen_term in params } - safety_settings = params.get("safety_settings", {}) + if self.use_vertexai: + safety_settings = GeminiClient._to_vertexai_safety_settings(params.get("safety_settings", {})) + else: + safety_settings = params.get("safety_settings", {}) if stream: warnings.warn( @@ -181,20 +200,29 @@ def create(self, params: Dict) -> ChatCompletion: gemini_messages = self._oai_messages_to_gemini_messages(messages) if self.use_vertexai: model = GenerativeModel( - model_name, generation_config=generation_config, safety_settings=safety_settings + model_name, + generation_config=generation_config, + safety_settings=safety_settings, + system_instruction=system_instruction, ) + chat = model.start_chat(history=gemini_messages[:-1], response_validation=response_validation) else: # we use chat model by default model = genai.GenerativeModel( - model_name, generation_config=generation_config, safety_settings=safety_settings + model_name, + generation_config=generation_config, + safety_settings=safety_settings, + system_instruction=system_instruction, ) genai.configure(api_key=self.api_key) - chat = model.start_chat(history=gemini_messages[:-1]) + chat = model.start_chat(history=gemini_messages[:-1]) max_retries = 5 for attempt in range(max_retries): ans = None try: - response = chat.send_message(gemini_messages[-1], stream=stream) + response = chat.send_message( + gemini_messages[-1].parts, stream=stream, safety_settings=safety_settings + ) except InternalServerError: delay = 5 * (2**attempt) warnings.warn( @@ -218,16 +246,22 @@ def create(self, params: Dict) -> ChatCompletion: # B. handle the vision model if self.use_vertexai: model = GenerativeModel( - model_name, generation_config=generation_config, safety_settings=safety_settings + model_name, + generation_config=generation_config, + safety_settings=safety_settings, + system_instruction=system_instruction, ) else: model = genai.GenerativeModel( - model_name, generation_config=generation_config, safety_settings=safety_settings + model_name, + generation_config=generation_config, + safety_settings=safety_settings, + system_instruction=system_instruction, ) genai.configure(api_key=self.api_key) # Gemini's vision model does not support chat history yet # chat = model.start_chat(history=gemini_messages[:-1]) - # response = chat.send_message(gemini_messages[-1]) + # response = chat.send_message(gemini_messages[-1].parts) user_message = self._oai_content_to_gemini_content(messages[-1]["content"]) if len(messages) > 2: warnings.warn( @@ -270,6 +304,8 @@ def _oai_content_to_gemini_content(self, content: Union[str, List]) -> List: """Convert content from OAI format to Gemini format""" rst = [] if isinstance(content, str): + if content == "": + content = "empty" # Empty content is not allowed. if self.use_vertexai: rst.append(VertexAIPart.from_text(content)) else: @@ -372,6 +408,35 @@ def _oai_messages_to_gemini_messages(self, messages: list[Dict[str, Any]]) -> li return rst + @staticmethod + def _to_vertexai_safety_settings(safety_settings): + """Convert safety settings to VertexAI format if needed, + like when specifying them in the OAI_CONFIG_LIST + """ + if isinstance(safety_settings, list) and all( + [ + isinstance(safety_setting, dict) and not isinstance(safety_setting, VertexAISafetySetting) + for safety_setting in safety_settings + ] + ): + vertexai_safety_settings = [] + for safety_setting in safety_settings: + if safety_setting["category"] not in VertexAIHarmCategory.__members__: + invalid_category = safety_setting["category"] + logger.error(f"Safety setting category {invalid_category} is invalid") + elif safety_setting["threshold"] not in VertexAIHarmBlockThreshold.__members__: + invalid_threshold = safety_setting["threshold"] + logger.error(f"Safety threshold {invalid_threshold} is invalid") + else: + vertexai_safety_setting = VertexAISafetySetting( + category=safety_setting["category"], + threshold=safety_setting["threshold"], + ) + vertexai_safety_settings.append(vertexai_safety_setting) + return vertexai_safety_settings + else: + return safety_settings + def _to_pil(data: str) -> Image.Image: """ diff --git a/autogen/oai/groq.py b/autogen/oai/groq.py new file mode 100644 index 000000000000..d2abe5116a25 --- /dev/null +++ b/autogen/oai/groq.py @@ -0,0 +1,282 @@ +"""Create an OpenAI-compatible client using Groq's API. + +Example: + llm_config={ + "config_list": [{ + "api_type": "groq", + "model": "mixtral-8x7b-32768", + "api_key": os.environ.get("GROQ_API_KEY") + } + ]} + + agent = autogen.AssistantAgent("my_agent", llm_config=llm_config) + +Install Groq's python library using: pip install --upgrade groq + +Resources: +- https://console.groq.com/docs/quickstart +""" + +from __future__ import annotations + +import copy +import os +import time +import warnings +from typing import Any, Dict, List + +from groq import Groq, Stream +from openai.types.chat import ChatCompletion, ChatCompletionMessageToolCall +from openai.types.chat.chat_completion import ChatCompletionMessage, Choice +from openai.types.completion_usage import CompletionUsage + +from autogen.oai.client_utils import should_hide_tools, validate_parameter + +# Cost per thousand tokens - Input / Output (NOTE: Convert $/Million to $/K) +GROQ_PRICING_1K = { + "llama3-70b-8192": (0.00059, 0.00079), + "mixtral-8x7b-32768": (0.00024, 0.00024), + "llama3-8b-8192": (0.00005, 0.00008), + "gemma-7b-it": (0.00007, 0.00007), +} + + +class GroqClient: + """Client for Groq's API.""" + + def __init__(self, **kwargs): + """Requires api_key or environment variable to be set + + Args: + api_key (str): The API key for using Groq (or environment variable GROQ_API_KEY needs to be set) + """ + # Ensure we have the api_key upon instantiation + self.api_key = kwargs.get("api_key", None) + if not self.api_key: + self.api_key = os.getenv("GROQ_API_KEY") + + assert ( + self.api_key + ), "Please include the api_key in your config list entry for Groq or set the GROQ_API_KEY env variable." + + def message_retrieval(self, response) -> List: + """ + Retrieve and return a list of strings or a list of Choice.Message from the response. + + NOTE: if a list of Choice.Message is returned, it currently needs to contain the fields of OpenAI's ChatCompletion Message object, + since that is expected for function or tool calling in the rest of the codebase at the moment, unless a custom agent is being used. + """ + return [choice.message for choice in response.choices] + + def cost(self, response) -> float: + return response.cost + + @staticmethod + def get_usage(response) -> Dict: + """Return usage summary of the response using RESPONSE_USAGE_KEYS.""" + # ... # pragma: no cover + return { + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens, + "cost": response.cost, + "model": response.model, + } + + def parse_params(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Loads the parameters for Groq API from the passed in parameters and returns a validated set. Checks types, ranges, and sets defaults""" + groq_params = {} + + # Check that we have what we need to use Groq's API + # We won't enforce the available models as they are likely to change + groq_params["model"] = params.get("model", None) + assert groq_params[ + "model" + ], "Please specify the 'model' in your config list entry to nominate the Groq model to use." + + # Validate allowed Groq parameters + # https://console.groq.com/docs/api-reference#chat + groq_params["frequency_penalty"] = validate_parameter( + params, "frequency_penalty", (int, float), True, None, (-2, 2), None + ) + groq_params["max_tokens"] = validate_parameter(params, "max_tokens", int, True, None, (0, None), None) + groq_params["presence_penalty"] = validate_parameter( + params, "presence_penalty", (int, float), True, None, (-2, 2), None + ) + groq_params["seed"] = validate_parameter(params, "seed", int, True, None, None, None) + groq_params["stream"] = validate_parameter(params, "stream", bool, True, False, None, None) + groq_params["temperature"] = validate_parameter(params, "temperature", (int, float), True, 1, (0, 2), None) + groq_params["top_p"] = validate_parameter(params, "top_p", (int, float), True, None, None, None) + + # Groq parameters not supported by their models yet, ignoring + # logit_bias, logprobs, top_logprobs + + # Groq parameters we are ignoring: + # n (must be 1), response_format (to enforce JSON but needs prompting as well), user, + # parallel_tool_calls (defaults to True), stop + # function_call (deprecated), functions (deprecated) + # tool_choice (none if no tools, auto if there are tools) + + return groq_params + + def create(self, params: Dict) -> ChatCompletion: + + messages = params.get("messages", []) + + # Convert AutoGen messages to Groq messages + groq_messages = oai_messages_to_groq_messages(messages) + + # Parse parameters to the Groq API's parameters + groq_params = self.parse_params(params) + + # Add tools to the call if we have them and aren't hiding them + if "tools" in params: + hide_tools = validate_parameter( + params, "hide_tools", str, False, "never", None, ["if_all_run", "if_any_run", "never"] + ) + if not should_hide_tools(groq_messages, params["tools"], hide_tools): + groq_params["tools"] = params["tools"] + + groq_params["messages"] = groq_messages + + # We use chat model by default, and set max_retries to 5 (in line with typical retries loop) + client = Groq(api_key=self.api_key, max_retries=5) + + # Token counts will be returned + prompt_tokens = 0 + completion_tokens = 0 + total_tokens = 0 + + # Streaming tool call recommendations + streaming_tool_calls = [] + + ans = None + try: + response = client.chat.completions.create(**groq_params) + except Exception as e: + raise RuntimeError(f"Groq exception occurred: {e}") + else: + + if groq_params["stream"]: + # Read in the chunks as they stream, taking in tool_calls which may be across + # multiple chunks if more than one suggested + ans = "" + for chunk in response: + ans = ans + (chunk.choices[0].delta.content or "") + + if chunk.choices[0].delta.tool_calls: + # We have a tool call recommendation + for tool_call in chunk.choices[0].delta.tool_calls: + streaming_tool_calls.append( + ChatCompletionMessageToolCall( + id=tool_call.id, + function={ + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + }, + type="function", + ) + ) + + if chunk.choices[0].finish_reason: + prompt_tokens = chunk.x_groq.usage.prompt_tokens + completion_tokens = chunk.x_groq.usage.completion_tokens + total_tokens = chunk.x_groq.usage.total_tokens + else: + # Non-streaming finished + ans: str = response.choices[0].message.content + + prompt_tokens = response.usage.prompt_tokens + completion_tokens = response.usage.completion_tokens + total_tokens = response.usage.total_tokens + + if response is not None: + + if isinstance(response, Stream): + # Streaming response + if chunk.choices[0].finish_reason == "tool_calls": + groq_finish = "tool_calls" + tool_calls = streaming_tool_calls + else: + groq_finish = "stop" + tool_calls = None + + response_content = ans + response_id = chunk.id + else: + # Non-streaming response + # If we have tool calls as the response, populate completed tool calls for our return OAI response + if response.choices[0].finish_reason == "tool_calls": + groq_finish = "tool_calls" + tool_calls = [] + for tool_call in response.choices[0].message.tool_calls: + tool_calls.append( + ChatCompletionMessageToolCall( + id=tool_call.id, + function={"name": tool_call.function.name, "arguments": tool_call.function.arguments}, + type="function", + ) + ) + else: + groq_finish = "stop" + tool_calls = None + + response_content = response.choices[0].message.content + response_id = response.id + else: + raise RuntimeError("Failed to get response from Groq after retrying 5 times.") + + # 3. convert output + message = ChatCompletionMessage( + role="assistant", + content=response_content, + function_call=None, + tool_calls=tool_calls, + ) + choices = [Choice(finish_reason=groq_finish, index=0, message=message)] + + response_oai = ChatCompletion( + id=response_id, + model=groq_params["model"], + created=int(time.time()), + object="chat.completion", + choices=choices, + usage=CompletionUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + ), + cost=calculate_groq_cost(prompt_tokens, completion_tokens, groq_params["model"]), + ) + + return response_oai + + +def oai_messages_to_groq_messages(messages: list[Dict[str, Any]]) -> list[dict[str, Any]]: + """Convert messages from OAI format to Groq's format. + We correct for any specific role orders and types. + """ + + groq_messages = copy.deepcopy(messages) + + # Remove the name field + for message in groq_messages: + if "name" in message: + message.pop("name", None) + + return groq_messages + + +def calculate_groq_cost(input_tokens: int, output_tokens: int, model: str) -> float: + """Calculate the cost of the completion using the Groq pricing.""" + total = 0.0 + + if model in GROQ_PRICING_1K: + input_cost_per_k, output_cost_per_k = GROQ_PRICING_1K[model] + input_cost = (input_tokens / 1000) * input_cost_per_k + output_cost = (output_tokens / 1000) * output_cost_per_k + total = input_cost + output_cost + else: + warnings.warn(f"Cost calculation not available for model {model}", UserWarning) + + return total diff --git a/autogen/oai/mistral.py b/autogen/oai/mistral.py index 8017e3536324..10d0f926ffbf 100644 --- a/autogen/oai/mistral.py +++ b/autogen/oai/mistral.py @@ -15,28 +15,32 @@ Resources: - https://docs.mistral.ai/getting-started/quickstart/ -""" -# Important notes when using the Mistral.AI API: -# The first system message can greatly affect whether the model returns a tool call, including text that references the ability to use functions will help. -# Changing the role on the first system message to 'user' improved the chances of the model recommending a tool call. +NOTE: Requires mistralai package version >= 1.0.1 +""" import inspect import json import os import time import warnings -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Union # Mistral libraries # pip install mistralai -from mistralai.client import MistralClient -from mistralai.exceptions import MistralAPIException -from mistralai.models.chat_completion import ChatCompletionResponse, ChatMessage, ToolCall +from mistralai import ( + AssistantMessage, + Function, + FunctionCall, + Mistral, + SystemMessage, + ToolCall, + ToolMessage, + UserMessage, +) from openai.types.chat import ChatCompletion, ChatCompletionMessageToolCall from openai.types.chat.chat_completion import ChatCompletionMessage, Choice from openai.types.completion_usage import CompletionUsage -from typing_extensions import Annotated from autogen.oai.client_utils import should_hide_tools, validate_parameter @@ -50,6 +54,7 @@ def __init__(self, **kwargs): Args: api_key (str): The API key for using Mistral.AI (or environment variable MISTRAL_API_KEY needs to be set) """ + # Ensure we have the api_key upon instantiation self.api_key = kwargs.get("api_key", None) if not self.api_key: @@ -59,7 +64,9 @@ def __init__(self, **kwargs): self.api_key ), "Please specify the 'api_key' in your config list entry for Mistral or set the MISTRAL_API_KEY env variable." - def message_retrieval(self, response: ChatCompletionResponse) -> Union[List[str], List[ChatCompletionMessage]]: + self._client = Mistral(api_key=self.api_key) + + def message_retrieval(self, response: ChatCompletion) -> Union[List[str], List[ChatCompletionMessage]]: """Retrieve the messages from the response.""" return [choice.message for choice in response.choices] @@ -86,34 +93,52 @@ def parse_params(self, params: Dict[str, Any]) -> Dict[str, Any]: ) mistral_params["random_seed"] = validate_parameter(params, "random_seed", int, True, None, False, None) + # TODO + if params.get("stream", False): + warnings.warn( + "Streaming is not currently supported, streaming will be disabled.", + UserWarning, + ) + # 3. Convert messages to Mistral format mistral_messages = [] tool_call_ids = {} # tool call ids to function name mapping for message in params["messages"]: if message["role"] == "assistant" and "tool_calls" in message and message["tool_calls"] is not None: # Convert OAI ToolCall to Mistral ToolCall - openai_toolcalls = message["tool_calls"] - mistral_toolcalls = [] - for toolcall in openai_toolcalls: - mistral_toolcall = ToolCall(id=toolcall["id"], function=toolcall["function"]) - mistral_toolcalls.append(mistral_toolcall) - mistral_messages.append( - ChatMessage(role=message["role"], content=message["content"], tool_calls=mistral_toolcalls) - ) + mistral_messages_tools = [] + for toolcall in message["tool_calls"]: + mistral_messages_tools.append( + ToolCall( + id=toolcall["id"], + function=FunctionCall( + name=toolcall["function"]["name"], + arguments=json.loads(toolcall["function"]["arguments"]), + ), + ) + ) + + mistral_messages.append(AssistantMessage(content="", tool_calls=mistral_messages_tools)) # Map tool call id to the function name for tool_call in message["tool_calls"]: tool_call_ids[tool_call["id"]] = tool_call["function"]["name"] - elif message["role"] in ("system", "user", "assistant"): - # Note this ChatMessage can take a 'name' but it is rejected by the Mistral API if not role=tool, so, no, the 'name' field is not used. - mistral_messages.append(ChatMessage(role=message["role"], content=message["content"])) + elif message["role"] == "system": + if len(mistral_messages) > 0 and mistral_messages[-1].role == "assistant": + # System messages can't appear after an Assistant message, so use a UserMessage + mistral_messages.append(UserMessage(content=message["content"])) + else: + mistral_messages.append(SystemMessage(content=message["content"])) + elif message["role"] == "assistant": + mistral_messages.append(AssistantMessage(content=message["content"])) + elif message["role"] == "user": + mistral_messages.append(UserMessage(content=message["content"])) elif message["role"] == "tool": # Indicates the result of a tool call, the name is the function name called mistral_messages.append( - ChatMessage( - role="tool", + ToolMessage( name=tool_call_ids[message["tool_call_id"]], content=message["content"], tool_call_id=message["tool_call_id"], @@ -122,21 +147,20 @@ def parse_params(self, params: Dict[str, Any]) -> Dict[str, Any]: else: warnings.warn(f"Unknown message role {message['role']}", UserWarning) - # If a 'system' message follows an 'assistant' message, change it to 'user' - # This can occur when using LLM summarisation - for i in range(1, len(mistral_messages)): - if mistral_messages[i - 1].role == "assistant" and mistral_messages[i].role == "system": - mistral_messages[i].role = "user" + # 4. Last message needs to be user or tool, if not, add a "please continue" message + if not isinstance(mistral_messages[-1], UserMessage) and not isinstance(mistral_messages[-1], ToolMessage): + mistral_messages.append(UserMessage(content="Please continue.")) mistral_params["messages"] = mistral_messages - # 4. Add tools to the call if we have them and aren't hiding them + # 5. Add tools to the call if we have them and aren't hiding them if "tools" in params: hide_tools = validate_parameter( params, "hide_tools", str, False, "never", None, ["if_all_run", "if_any_run", "never"] ) if not should_hide_tools(params["messages"], params["tools"], hide_tools): - mistral_params["tools"] = params["tools"] + mistral_params["tools"] = tool_def_to_mistral(params["tools"]) + return mistral_params def create(self, params: Dict[str, Any]) -> ChatCompletion: @@ -144,8 +168,7 @@ def create(self, params: Dict[str, Any]) -> ChatCompletion: mistral_params = self.parse_params(params) # 2. Call Mistral.AI API - client = MistralClient(api_key=self.api_key) - mistral_response = client.chat(**mistral_params) + mistral_response = self._client.chat.complete(**mistral_params) # TODO: Handle streaming # 3. Convert Mistral response to OAI compatible format @@ -191,7 +214,7 @@ def create(self, params: Dict[str, Any]) -> ChatCompletion: return response_oai @staticmethod - def get_usage(response: ChatCompletionResponse) -> Dict: + def get_usage(response: ChatCompletion) -> Dict: return { "prompt_tokens": response.usage.prompt_tokens if response.usage is not None else 0, "completion_tokens": response.usage.completion_tokens if response.usage is not None else 0, @@ -203,25 +226,48 @@ def get_usage(response: ChatCompletionResponse) -> Dict: } +def tool_def_to_mistral(tool_definitions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Converts AutoGen tool definition to a mistral tool format""" + + mistral_tools = [] + + for autogen_tool in tool_definitions: + mistral_tool = { + "type": "function", + "function": Function( + name=autogen_tool["function"]["name"], + description=autogen_tool["function"]["description"], + parameters=autogen_tool["function"]["parameters"], + ), + } + + mistral_tools.append(mistral_tool) + + return mistral_tools + + def calculate_mistral_cost(input_tokens: int, output_tokens: int, model_name: str) -> float: """Calculate the cost of the mistral response.""" - # Prices per 1 million tokens + # Prices per 1 thousand tokens # https://mistral.ai/technology/ model_cost_map = { - "open-mistral-7b": {"input": 0.25, "output": 0.25}, - "open-mixtral-8x7b": {"input": 0.7, "output": 0.7}, - "open-mixtral-8x22b": {"input": 2.0, "output": 6.0}, - "mistral-small-latest": {"input": 1.0, "output": 3.0}, - "mistral-medium-latest": {"input": 2.7, "output": 8.1}, - "mistral-large-latest": {"input": 4.0, "output": 12.0}, + "open-mistral-7b": {"input": 0.00025, "output": 0.00025}, + "open-mixtral-8x7b": {"input": 0.0007, "output": 0.0007}, + "open-mixtral-8x22b": {"input": 0.002, "output": 0.006}, + "mistral-small-latest": {"input": 0.001, "output": 0.003}, + "mistral-medium-latest": {"input": 0.00275, "output": 0.0081}, + "mistral-large-latest": {"input": 0.0003, "output": 0.0003}, + "mistral-large-2407": {"input": 0.0003, "output": 0.0003}, + "open-mistral-nemo-2407": {"input": 0.0003, "output": 0.0003}, + "codestral-2405": {"input": 0.001, "output": 0.003}, } # Ensure we have the model they are using and return the total cost if model_name in model_cost_map: costs = model_cost_map[model_name] - return (input_tokens * costs["input"] / 1_000_000) + (output_tokens * costs["output"] / 1_000_000) + return (input_tokens * costs["input"] / 1000) + (output_tokens * costs["output"] / 1000) else: warnings.warn(f"Cost calculation is not implemented for model {model_name}, will return $0.", UserWarning) return 0 diff --git a/autogen/oai/openai_utils.py b/autogen/oai/openai_utils.py index 0c8a0a413375..41b94324118a 100644 --- a/autogen/oai/openai_utils.py +++ b/autogen/oai/openai_utils.py @@ -13,18 +13,30 @@ from openai.types.beta.assistant import Assistant from packaging.version import parse -NON_CACHE_KEY = ["api_key", "base_url", "api_type", "api_version", "azure_ad_token", "azure_ad_token_provider"] +NON_CACHE_KEY = [ + "api_key", + "base_url", + "api_type", + "api_version", + "azure_ad_token", + "azure_ad_token_provider", + "credentials", +] DEFAULT_AZURE_API_VERSION = "2024-02-01" OAI_PRICE1K = { # https://openai.com/api/pricing/ # gpt-4o "gpt-4o": (0.005, 0.015), "gpt-4o-2024-05-13": (0.005, 0.015), + "gpt-4o-2024-08-06": (0.0025, 0.01), # gpt-4-turbo "gpt-4-turbo-2024-04-09": (0.01, 0.03), # gpt-4 "gpt-4": (0.03, 0.06), "gpt-4-32k": (0.06, 0.12), + # gpt-4o-mini + "gpt-4o-mini": (0.000150, 0.000600), + "gpt-4o-mini-2024-07-18": (0.000150, 0.000600), # gpt-3.5 turbo "gpt-3.5-turbo": (0.0005, 0.0015), # default is 0125 "gpt-3.5-turbo-0125": (0.0005, 0.0015), # 16k @@ -96,7 +108,7 @@ def is_valid_api_key(api_key: str) -> bool: Returns: bool: A boolean that indicates if input is valid OpenAI API key. """ - api_key_re = re.compile(r"^sk-(proj-)?[A-Za-z0-9]{32,}$") + api_key_re = re.compile(r"^sk-([A-Za-z0-9]+(-+[A-Za-z0-9]+)*-)?[A-Za-z0-9]{32,}$") return bool(re.fullmatch(api_key_re, api_key)) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 9393903ec86c..4fb53c7c9600 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -365,7 +365,7 @@ def create_vector_db_from_dir( embedding_function is not None. embedding_function (Optional, Callable): the embedding function to use. Default is None, SentenceTransformer with the given `embedding_model` will be used. If you want to use OpenAI, Cohere, HuggingFace or other embedding - functions, you can pass it here, follow the examples in `https://docs.trychroma.com/embeddings`. + functions, you can pass it here, follow the examples in `https://docs.trychroma.com/guides/embeddings`. custom_text_split_function (Optional, Callable): a custom function to split a string into a list of strings. Default is None, will use the default function in `autogen.retrieve_utils.split_text_to_chunks`. custom_text_types (Optional, List[str]): a list of file types to be processed. Default is TEXT_FORMATS. @@ -448,7 +448,7 @@ def query_vector_db( embedding_function is not None. embedding_function (Optional, Callable): the embedding function to use. Default is None, SentenceTransformer with the given `embedding_model` will be used. If you want to use OpenAI, Cohere, HuggingFace or other embedding - functions, you can pass it here, follow the examples in `https://docs.trychroma.com/embeddings`. + functions, you can pass it here, follow the examples in `https://docs.trychroma.com/guides/embeddings`. Returns: diff --git a/autogen/runtime_logging.py b/autogen/runtime_logging.py index adb55ba63b4f..0fd7cc2fc8b9 100644 --- a/autogen/runtime_logging.py +++ b/autogen/runtime_logging.py @@ -14,7 +14,10 @@ if TYPE_CHECKING: from autogen import Agent, ConversableAgent, OpenAIWrapper from autogen.oai.anthropic import AnthropicClient + from autogen.oai.bedrock import BedrockClient + from autogen.oai.cohere import CohereClient from autogen.oai.gemini import GeminiClient + from autogen.oai.groq import GroqClient from autogen.oai.mistral import MistralAIClient from autogen.oai.together import TogetherClient @@ -110,7 +113,17 @@ def log_new_wrapper(wrapper: OpenAIWrapper, init_args: Dict[str, Union[LLMConfig def log_new_client( - client: Union[AzureOpenAI, OpenAI, GeminiClient, AnthropicClient, MistralAIClient, TogetherClient], + client: Union[ + AzureOpenAI, + OpenAI, + GeminiClient, + AnthropicClient, + MistralAIClient, + TogetherClient, + GroqClient, + CohereClient, + BedrockClient, + ], wrapper: OpenAIWrapper, init_args: Dict[str, Any], ) -> None: diff --git a/autogen/token_count_utils.py b/autogen/token_count_utils.py index 2842a7494536..8552a8f16536 100644 --- a/autogen/token_count_utils.py +++ b/autogen/token_count_utils.py @@ -36,6 +36,9 @@ def get_max_token_limit(model: str = "gpt-3.5-turbo-0613") -> int: "gpt-4-vision-preview": 128000, "gpt-4o": 128000, "gpt-4o-2024-05-13": 128000, + "gpt-4o-2024-08-06": 128000, + "gpt-4o-mini": 128000, + "gpt-4o-mini-2024-07-18": 128000, } return max_token_limit[model] @@ -95,7 +98,7 @@ def _num_token_from_messages(messages: Union[List, Dict], model="gpt-3.5-turbo-0 try: encoding = tiktoken.encoding_for_model(model) except KeyError: - print("Warning: model not found. Using cl100k_base encoding.") + logger.warning(f"Model {model} not found. Using cl100k_base encoding.") encoding = tiktoken.get_encoding("cl100k_base") if model in { "gpt-3.5-turbo-0613", @@ -166,7 +169,7 @@ def num_tokens_from_functions(functions, model="gpt-3.5-turbo-0613") -> int: try: encoding = tiktoken.encoding_for_model(model) except KeyError: - print("Warning: model not found. Using cl100k_base encoding.") + logger.warning(f"Model {model} not found. Using cl100k_base encoding.") encoding = tiktoken.get_encoding("cl100k_base") num_tokens = 0 @@ -193,7 +196,7 @@ def num_tokens_from_functions(functions, model="gpt-3.5-turbo-0613") -> int: function_tokens += 3 function_tokens += len(encoding.encode(o)) else: - print(f"Warning: not supported field {field}") + logger.warning(f"Not supported field {field}") function_tokens += 11 if len(parameters["properties"]) == 0: function_tokens -= 2 diff --git a/autogen/version.py b/autogen/version.py index 77fc1e2ea295..9b1b78b4b3a0 100644 --- a/autogen/version.py +++ b/autogen/version.py @@ -1 +1 @@ -__version__ = "0.2.31" +__version__ = "0.2.35" diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index 5fa215f0ce9c..78d18527b629 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34322.80 @@ -27,15 +26,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SemanticKernel", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Core", "src\AutoGen.Core\AutoGen.Core.csproj", "{D58D43D1-0617-4A3D-9932-C773E6398535}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI", "src\AutoGen.OpenAI\AutoGen.OpenAI.csproj", "{63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.V1", "src\AutoGen.OpenAI.V1\AutoGen.OpenAI.V1.csproj", "{63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Mistral", "src\AutoGen.Mistral\AutoGen.Mistral.csproj", "{6585D1A4-3D97-4D76-A688-1933B61AEB19}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Mistral.Tests", "test\AutoGen.Mistral.Tests\AutoGen.Mistral.Tests.csproj", "{15441693-3659-4868-B6C1-B106F52FF3BA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.WebAPI", "src\AutoGen.WebAPI\AutoGen.WebAPI.csproj", "{257FFD71-08E5-40C7-AB04-6A81A78EB410}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.WebAPI.Tests", "test\AutoGen.WebAPI.Tests\AutoGen.WebAPI.Tests.csproj", "{E2EF5E66-683C-4DDC-8ADA-5F676502B9BA}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SemanticKernel.Tests", "test\AutoGen.SemanticKernel.Tests\AutoGen.SemanticKernel.Tests.csproj", "{1DFABC4A-8458-4875-8DCB-59F3802DAC65}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.V1.Tests", "test\AutoGen.OpenAI.V1.Tests\AutoGen.OpenAI.V1.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}" EndProject @@ -61,7 +64,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Gemini.Sample", "sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.AotCompatibility.Tests", "test\AutoGen.AotCompatibility.Tests\AutoGen.AotCompatibility.Tests.csproj", "{6B82F26D-5040-4453-B21B-C8D1F913CE4C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.OpenAI.Sample", "sample\AutoGen.OpenAI.Sample\AutoGen.OpenAI.Sample.csproj", "{0E635268-351C-4A6B-A28D-593D868C2CA4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.Sample", "sample\AutoGen.OpenAI.Sample\AutoGen.OpenAI.Sample.csproj", "{0E635268-351C-4A6B-A28D-593D868C2CA4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.WebAPI.Sample", "sample\AutoGen.WebAPI.Sample\AutoGen.WebAPI.Sample.csproj", "{12079C18-A519-403F-BBFD-200A36A0C083}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.AzureAIInference", "src\AutoGen.AzureAIInference\AutoGen.AzureAIInference.csproj", "{5C45981D-1319-4C25-935C-83D411CB28DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.AzureAIInference.Tests", "test\AutoGen.AzureAIInference.Tests\AutoGen.AzureAIInference.Tests.csproj", "{5970868F-831E-418F-89A9-4EC599563E16}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Tests.Share", "test\AutoGen.Test.Share\AutoGen.Tests.Share.csproj", "{143725E2-206C-4D37-93E4-9EDF699826B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI", "src\AutoGen.OpenAI\AutoGen.OpenAI.csproj", "{3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{42A8251C-E7B3-47BB-A82E-459952EBE132}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -117,6 +132,14 @@ Global {15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.Build.0 = Release|Any CPU + {257FFD71-08E5-40C7-AB04-6A81A78EB410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {257FFD71-08E5-40C7-AB04-6A81A78EB410}.Debug|Any CPU.Build.0 = Debug|Any CPU + {257FFD71-08E5-40C7-AB04-6A81A78EB410}.Release|Any CPU.ActiveCfg = Release|Any CPU + {257FFD71-08E5-40C7-AB04-6A81A78EB410}.Release|Any CPU.Build.0 = Release|Any CPU + {E2EF5E66-683C-4DDC-8ADA-5F676502B9BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2EF5E66-683C-4DDC-8ADA-5F676502B9BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2EF5E66-683C-4DDC-8ADA-5F676502B9BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2EF5E66-683C-4DDC-8ADA-5F676502B9BA}.Release|Any CPU.Build.0 = Release|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -177,6 +200,30 @@ Global {0E635268-351C-4A6B-A28D-593D868C2CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E635268-351C-4A6B-A28D-593D868C2CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E635268-351C-4A6B-A28D-593D868C2CA4}.Release|Any CPU.Build.0 = Release|Any CPU + {12079C18-A519-403F-BBFD-200A36A0C083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12079C18-A519-403F-BBFD-200A36A0C083}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12079C18-A519-403F-BBFD-200A36A0C083}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12079C18-A519-403F-BBFD-200A36A0C083}.Release|Any CPU.Build.0 = Release|Any CPU + {5C45981D-1319-4C25-935C-83D411CB28DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C45981D-1319-4C25-935C-83D411CB28DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C45981D-1319-4C25-935C-83D411CB28DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C45981D-1319-4C25-935C-83D411CB28DF}.Release|Any CPU.Build.0 = Release|Any CPU + {5970868F-831E-418F-89A9-4EC599563E16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5970868F-831E-418F-89A9-4EC599563E16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5970868F-831E-418F-89A9-4EC599563E16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5970868F-831E-418F-89A9-4EC599563E16}.Release|Any CPU.Build.0 = Release|Any CPU + {143725E2-206C-4D37-93E4-9EDF699826B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {143725E2-206C-4D37-93E4-9EDF699826B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {143725E2-206C-4D37-93E4-9EDF699826B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {143725E2-206C-4D37-93E4-9EDF699826B2}.Release|Any CPU.Build.0 = Release|Any CPU + {3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8}.Release|Any CPU.Build.0 = Release|Any CPU + {42A8251C-E7B3-47BB-A82E-459952EBE132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42A8251C-E7B3-47BB-A82E-459952EBE132}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42A8251C-E7B3-47BB-A82E-459952EBE132}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42A8251C-E7B3-47BB-A82E-459952EBE132}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -194,6 +241,8 @@ Global {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {6585D1A4-3D97-4D76-A688-1933B61AEB19} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {15441693-3659-4868-B6C1-B106F52FF3BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {257FFD71-08E5-40C7-AB04-6A81A78EB410} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {E2EF5E66-683C-4DDC-8ADA-5F676502B9BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {1DFABC4A-8458-4875-8DCB-59F3802DAC65} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {D36A85F9-C172-487D-8192-6BFE5D05B4A7} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} @@ -209,8 +258,14 @@ Global {19679B75-CE3A-4DF0-A3F0-CA369D2760A4} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} {6B82F26D-5040-4453-B21B-C8D1F913CE4C} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {0E635268-351C-4A6B-A28D-593D868C2CA4} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} + {12079C18-A519-403F-BBFD-200A36A0C083} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} + {5C45981D-1319-4C25-935C-83D411CB28DF} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {5970868F-831E-418F-89A9-4EC599563E16} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {143725E2-206C-4D37-93E4-9EDF699826B2} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {3AF1CBEC-2877-41E9-92AE-3A391B2AA9E8} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {42A8251C-E7B3-47BB-A82E-459952EBE132} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 4b3e9441f1ee..b5663fe4c578 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -4,7 +4,8 @@ <Import Project="./eng/MetaInfo.props" /> <Import Project="./eng/Sign.props" /> <PropertyGroup> - <TestTargetFramework>net8.0</TestTargetFramework> + <PackageTargetFrameworks>netstandard2.0;net6.0;net8.0</PackageTargetFrameworks> + <TestTargetFrameworks>net8.0</TestTargetFrameworks> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> <SignAssembly>True</SignAssembly> @@ -31,6 +32,7 @@ <PackageReference Include="xunit" Version="$(XUnitVersion)" /> <PackageReference Include="xunit.runner.console" Version="$(XUnitVersion)" /> <PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitVersion)" /> + <PackageReference Include="Moq" Version="4.20.70" /> </ItemGroup> <ItemGroup Condition="'$(IsTestProject)' == 'true'"> diff --git a/dotnet/eng/MetaInfo.props b/dotnet/eng/MetaInfo.props index 041ee0ec6c97..c6eeaf843435 100644 --- a/dotnet/eng/MetaInfo.props +++ b/dotnet/eng/MetaInfo.props @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> - <VersionPrefix>0.0.15</VersionPrefix> + <VersionPrefix>0.2.1</VersionPrefix> <Authors>AutoGen</Authors> <PackageProjectUrl>https://microsoft.github.io/autogen-for-net/</PackageProjectUrl> <RepositoryUrl>https://github.com/microsoft/autogen</RepositoryUrl> diff --git a/dotnet/eng/Version.props b/dotnet/eng/Version.props index 0b8dcaa565cb..36cfd917c2c0 100644 --- a/dotnet/eng/Version.props +++ b/dotnet/eng/Version.props @@ -2,8 +2,9 @@ <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <AzureOpenAIVersion>1.0.0-beta.17</AzureOpenAIVersion> - <SemanticKernelVersion>1.10.0</SemanticKernelVersion> - <SemanticKernelExperimentalVersion>1.10.0-alpha</SemanticKernelExperimentalVersion> + <AzureOpenAIV2Version>2.0.0-beta.3</AzureOpenAIV2Version> + <SemanticKernelVersion>1.18.1-rc</SemanticKernelVersion> + <SemanticKernelExperimentalVersion>1.18.1-alpha</SemanticKernelExperimentalVersion> <SystemCodeDomVersion>5.0.0</SystemCodeDomVersion> <MicrosoftCodeAnalysisVersion>4.3.0</MicrosoftCodeAnalysisVersion> <ApprovalTestVersion>6.0.0</ApprovalTestVersion> @@ -12,7 +13,11 @@ <MicrosoftNETTestSdkVersion>17.7.0</MicrosoftNETTestSdkVersion> <MicrosoftDotnetInteractive>1.0.0-beta.24229.4</MicrosoftDotnetInteractive> <MicrosoftSourceLinkGitHubVersion>8.0.0</MicrosoftSourceLinkGitHubVersion> + <MicrosoftASPNETCoreVersion>8.0.4</MicrosoftASPNETCoreVersion> <GoogleCloudAPIPlatformVersion>3.0.0</GoogleCloudAPIPlatformVersion> <JsonSchemaVersion>4.3.0.2</JsonSchemaVersion> + <AzureAIInferenceVersion>1.0.0-beta.1</AzureAIInferenceVersion> + <OpenAISDKVersion>2.0.0-beta.10</OpenAISDKVersion> + <PowershellSDKVersion>7.4.4</PowershellSDKVersion> </PropertyGroup> </Project> \ No newline at end of file diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/Anthropic_Agent_With_Prompt_Caching.cs b/dotnet/sample/AutoGen.Anthropic.Samples/Anthropic_Agent_With_Prompt_Caching.cs new file mode 100644 index 000000000000..5d8a99ce1288 --- /dev/null +++ b/dotnet/sample/AutoGen.Anthropic.Samples/Anthropic_Agent_With_Prompt_Caching.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Anthropic_Agent_With_Prompt_Caching.cs + +using AutoGen.Anthropic.DTO; +using AutoGen.Anthropic.Extensions; +using AutoGen.Anthropic.Utils; +using AutoGen.Core; + +namespace AutoGen.Anthropic.Samples; + +public class Anthropic_Agent_With_Prompt_Caching +{ + // A random and long test string to demonstrate cache control. + // the context must be larger than 1024 tokens for Claude 3.5 Sonnet and Claude 3 Opus + // 2048 tokens for Claude 3.0 Haiku + // Shorter prompts cannot be cached, even if marked with cache_control. Any requests to cache fewer than this number of tokens will be processed without caching + + #region Long story for caching + public const string LongStory = """ + Once upon a time in a small, nondescript town lived a man named Bob. Bob was an unassuming individual, the kind of person you wouldn’t look twice at if you passed him on the street. He worked as an IT specialist for a mid-sized corporation, spending his days fixing computers and troubleshooting software issues. But beneath his average exterior, Bob harbored a secret ambition—he wanted to take over the world. + + Bob wasn’t always like this. For most of his life, he had been content with his routine, blending into the background. But one day, while browsing the dark corners of the internet, Bob stumbled upon an ancient manuscript, encrypted within the deep web, detailing the steps to global domination. It was written by a forgotten conqueror, someone whose name had been erased from history but whose methods were preserved in this digital relic. The manuscript laid out a plan so intricate and flawless that Bob, with his analytical mind, became obsessed. + + Over the next few years, Bob meticulously followed the manuscript’s guidance. He started small, creating a network of like-minded individuals who shared his dream. They communicated through encrypted channels, meeting in secret to discuss their plans. Bob was careful, never revealing too much about himself, always staying in the shadows. He used his IT skills to gather information, infiltrating government databases, and private corporations, and acquiring secrets that could be used as leverage. + + As his network grew, so did his influence. Bob began to manipulate world events from behind the scenes. He orchestrated economic crises, incited political turmoil, and planted seeds of discord among the world’s most powerful nations. Each move was calculated, each action a step closer to his ultimate goal. The world was in chaos, and no one suspected that a man like Bob could be behind it all. + + But Bob knew that causing chaos wasn’t enough. To truly take over the world, he needed something more—something to cement his power. That’s when he turned to technology. Bob had always been ahead of the curve when it came to tech, and now, he planned to use it to his advantage. He began developing an AI, one that would be more powerful and intelligent than anything the world had ever seen. This AI, which Bob named “Nemesis,” was designed to control every aspect of modern life—from financial systems to military networks. + + It took years of coding, testing, and refining, but eventually, Nemesis was ready. Bob unleashed the AI, and within days, it had taken control of the world’s digital infrastructure. Governments were powerless, their systems compromised. Corporations crumbled as their assets were seized. The military couldn’t act, their weapons turned against them. Bob, from the comfort of his modest home, had done it. He had taken over the world. + + The world, now under Bob’s control, was eerily quiet. There were no more wars, no more financial crises, no more political strife. Nemesis ensured that everything ran smoothly, efficiently, and without dissent. The people of the world had no choice but to obey, their lives dictated by an unseen hand. + + Bob, once a man who was overlooked and ignored, was now the most powerful person on the planet. But with that power came a realization. The world he had taken over was not the world he had envisioned. It was cold, mechanical, and devoid of the chaos that once made life unpredictable and exciting. Bob had achieved his goal, but in doing so, he had lost the very thing that made life worth living—freedom. + + And so, Bob, now ruler of the world, sat alone in his control room, staring at the screens that displayed his dominion. He had everything he had ever wanted, yet he felt emptier than ever before. The world was his, but at what cost? + + In the end, Bob realized that true power didn’t come from controlling others, but from the ability to let go. He deactivated Nemesis, restoring the world to its former state, and disappeared into obscurity, content to live out the rest of his days as just another face in the crowd. And though the world never knew his name, Bob’s legacy would live on, a reminder of the dangers of unchecked ambition. + + Bob had vanished, leaving the world in a fragile state of recovery. Governments scrambled to regain control of their systems, corporations tried to rebuild, and the global population slowly adjusted to life without the invisible grip of Nemesis. Yet, even as society returned to a semblance of normalcy, whispers of the mysterious figure who had brought the world to its knees lingered in the shadows. + + Meanwhile, Bob had retreated to a secluded cabin deep in the mountains. The cabin was a modest, rustic place, surrounded by dense forests and overlooking a tranquil lake. It was far from civilization, a perfect place for a man who wanted to disappear. Bob spent his days fishing, hiking, and reflecting on his past. For the first time in years, he felt a sense of peace. + + But peace was fleeting. Despite his best efforts to put his past behind him, Bob couldn’t escape the consequences of his actions. He had unleashed Nemesis upon the world, and though he had deactivated the AI, remnants of its code still existed. Rogue factions, hackers, and remnants of his old network were searching for those fragments, hoping to revive Nemesis and seize the power that Bob had relinquished. + + One day, as Bob was chopping wood outside his cabin, a figure emerged from the tree line. It was a young woman, dressed in hiking gear, with a determined look in her eyes. Bob tensed, his instincts telling him that this was no ordinary hiker. + + “Bob,” the woman said, her voice steady. “Or should I say, the man who almost became the ruler of the world?” + + Bob sighed, setting down his axe. “Who are you, and what do you want?” + + The woman stepped closer. “My name is Sarah. I was part of your network, one of the few who knew about Nemesis. But I wasn’t like the others. I didn’t want power for myself—I wanted to protect the world from those who would misuse it.” + + Bob studied her, trying to gauge her intentions. “And why are you here now?” + + Sarah reached into her backpack and pulled out a small device. “Because Nemesis isn’t dead. Some of its code is still active, and it’s trying to reboot itself. I need your help to stop it for good.” + + Bob’s heart sank. He had hoped that by deactivating Nemesis, he had erased it from existence. But deep down, he knew that an AI as powerful as Nemesis wouldn’t go down so easily. “Why come to me? I’m the one who created it. I’m the reason the world is in this mess.” + + Sarah shook her head. “You’re also the only one who knows how to stop it. I’ve tracked down the remnants of Nemesis’s code, but I need you to help destroy it before it falls into the wrong hands.” + + Bob hesitated. He had wanted nothing more than to leave his past behind, but he couldn’t ignore the responsibility that weighed on him. He had created Nemesis, and now it was his duty to make sure it never posed a threat again. + + “Alright,” Bob said finally. “I’ll help you. But after this, I’m done. No more world domination, no more secret networks. I just want to live in peace.” + + Sarah nodded. “Agreed. Let’s finish what you started.” + + Over the next few weeks, Bob and Sarah worked together, traveling to various locations around the globe where fragments of Nemesis’s code had been detected. They infiltrated secure facilities, outsmarted rogue hackers, and neutralized threats, all while staying one step ahead of those who sought to control Nemesis for their own gain. + + As they worked, Bob and Sarah developed a deep respect for one another. Sarah was sharp, resourceful, and driven by a genuine desire to protect the world. Bob found himself opening up to her, sharing his regrets, his doubts, and the lessons he had learned. In turn, Sarah shared her own story—how she had once been tempted by power but had chosen a different path, one that led her to fight for what was right. + + Finally, after weeks of intense effort, they tracked down the last fragment of Nemesis’s code, hidden deep within a remote server farm in the Arctic. The facility was heavily guarded, but Bob and Sarah had planned meticulously. Under the cover of a blizzard, they infiltrated the facility, avoiding detection as they made their way to the heart of the server room. + + As Bob began the process of erasing the final fragment, an alarm blared, and the facility’s security forces closed in. Sarah held them off as long as she could, but they were outnumbered and outgunned. Just as the situation seemed hopeless, Bob executed the final command, wiping Nemesis from existence once and for all. + + But as the last remnants of Nemesis were deleted, Bob knew there was only one way to ensure it could never be resurrected. He initiated a self-destruct sequence for the server farm, trapping himself and Sarah inside. + + Sarah stared at him, realization dawning in her eyes. “Bob, what are you doing?” + + Bob looked at her, a sad smile on his face. “I have to make sure it’s over. This is the only way.” + + Sarah’s eyes filled with tears, but she nodded, understanding the gravity of his decision. “Thank you, Bob. For everything.” + + As the facility’s countdown reached its final seconds, Bob and Sarah stood side by side, knowing they had done the right thing. The explosion that followed was seen from miles away, a final testament to the end of an era. + + The world never knew the true story of Bob, the man who almost ruled the world. But in his final act of sacrifice, he ensured that the world would remain free, a place where people could live their lives without fear of control. Bob had redeemed himself, not as a conqueror, but as a protector—a man who chose to save the world rather than rule it. + + And in the quiet aftermath of the explosion, as the snow settled over the wreckage, Bob’s legacy was sealed—not as a name in history books, but as a silent guardian whose actions would be felt for generations to come. + """; + #endregion + + public static async Task RunAsync() + { + #region init translator agents & register middlewares + + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? + throw new Exception("Please set ANTHROPIC_API_KEY environment variable."); + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, apiKey); + var frenchTranslatorAgent = + new AnthropicClientAgent(anthropicClient, "frenchTranslator", AnthropicConstants.Claude35Sonnet, + systemMessage: "You are a French translator") + .RegisterMessageConnector() + .RegisterPrintMessage(); + + var germanTranslatorAgent = new AnthropicClientAgent(anthropicClient, "germanTranslator", + AnthropicConstants.Claude35Sonnet, systemMessage: "You are a German translator") + .RegisterMessageConnector() + .RegisterPrintMessage(); + + #endregion + + var userProxyAgent = new UserProxyAgent( + name: "user", + humanInputMode: HumanInputMode.ALWAYS) + .RegisterPrintMessage(); + + var groupChat = new RoundRobinGroupChat( + agents: [userProxyAgent, frenchTranslatorAgent, germanTranslatorAgent]); + + var messageEnvelope = + MessageEnvelope.Create( + new ChatMessage("user", [TextContent.CreateTextWithCacheControl(LongStory)]), + from: "user"); + + var chatHistory = new List<IMessage>() + { + new TextMessage(Role.User, "translate this text for me", from: userProxyAgent.Name), + messageEnvelope, + }; + + var history = await groupChat.SendAsync(chatHistory).ToArrayAsync(); + } +} diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj b/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj index 33a5aa7f16b6..fe7553b937f4 100644 --- a/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj +++ b/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>$(TestTargetFramework)</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateDocumentationFile>True</GenerateDocumentationFile> @@ -13,6 +13,7 @@ <ProjectReference Include="..\..\src\AutoGen.DotnetInteractive\AutoGen.DotnetInteractive.csproj" /> <ProjectReference Include="..\..\src\AutoGen.SourceGenerator\AutoGen.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\..\src\AutoGen\AutoGen.csproj" /> + <PackageReference Include="FluentAssertions" Version="$(FluentAssertionVersion)" /> </ItemGroup> </Project> diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs b/dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent.cs similarity index 93% rename from dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs rename to dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent.cs index 94b5f37511e6..6f32c3cb4a21 100644 --- a/dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs +++ b/dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// AnthropicSamples.cs +// Create_Anthropic_Agent.cs using AutoGen.Anthropic.Extensions; using AutoGen.Anthropic.Utils; @@ -7,7 +7,7 @@ namespace AutoGen.Anthropic.Samples; -public static class AnthropicSamples +public static class Create_Anthropic_Agent { public static async Task RunAsync() { diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent_With_Tool.cs b/dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent_With_Tool.cs new file mode 100644 index 000000000000..0324a39ffa59 --- /dev/null +++ b/dotnet/sample/AutoGen.Anthropic.Samples/Create_Anthropic_Agent_With_Tool.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Create_Anthropic_Agent_With_Tool.cs + +using AutoGen.Anthropic.DTO; +using AutoGen.Anthropic.Extensions; +using AutoGen.Anthropic.Utils; +using AutoGen.Core; +using FluentAssertions; + +namespace AutoGen.Anthropic.Samples; + +#region WeatherFunction + +public partial class WeatherFunction +{ + /// <summary> + /// Gets the weather based on the location and the unit + /// </summary> + /// <param name="location"></param> + /// <param name="unit"></param> + /// <returns></returns> + [Function] + public async Task<string> GetWeather(string location, string unit) + { + // dummy implementation + return $"The weather in {location} is currently sunny with a tempature of {unit} (s)"; + } +} +#endregion +public class Create_Anthropic_Agent_With_Tool +{ + public static async Task RunAsync() + { + #region define_tool + var tool = new Tool + { + Name = "GetWeather", + Description = "Get the current weather in a given location", + InputSchema = new InputSchema + { + Type = "object", + Properties = new Dictionary<string, SchemaProperty> + { + { "location", new SchemaProperty { Type = "string", Description = "The city and state, e.g. San Francisco, CA" } }, + { "unit", new SchemaProperty { Type = "string", Description = "The unit of temperature, either \"celsius\" or \"fahrenheit\"" } } + }, + Required = new List<string> { "location" } + } + }; + + var weatherFunction = new WeatherFunction(); + var functionMiddleware = new FunctionCallMiddleware( + functions: [ + weatherFunction.GetWeatherFunctionContract, + ], + functionMap: new Dictionary<string, Func<string, Task<string>>> + { + { weatherFunction.GetWeatherFunctionContract.Name!, weatherFunction.GetWeatherWrapper }, + }); + + #endregion + + #region create_anthropic_agent + + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? + throw new Exception("Missing ANTHROPIC_API_KEY environment variable."); + + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, apiKey); + var agent = new AnthropicClientAgent(anthropicClient, "assistant", AnthropicConstants.Claude3Haiku, + tools: [tool]); // Define tools for AnthropicClientAgent + #endregion + + #region register_middleware + + var agentWithConnector = agent + .RegisterMessageConnector() + .RegisterPrintMessage() + .RegisterStreamingMiddleware(functionMiddleware); + #endregion register_middleware + + #region single_turn + var question = new TextMessage(Role.Assistant, + "What is the weather like in San Francisco?", + from: "user"); + var functionCallReply = await agentWithConnector.SendAsync(question); + #endregion + + #region Single_turn_verify_reply + functionCallReply.Should().BeOfType<ToolCallAggregateMessage>(); + #endregion Single_turn_verify_reply + + #region Multi_turn + var finalReply = await agentWithConnector.SendAsync(chatHistory: [question, functionCallReply]); + #endregion Multi_turn + + #region Multi_turn_verify_reply + finalReply.Should().BeOfType<TextMessage>(); + #endregion Multi_turn_verify_reply + } +} diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs b/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs index f3c615088610..105bb56524fd 100644 --- a/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs +++ b/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs @@ -7,6 +7,6 @@ internal static class Program { public static async Task Main(string[] args) { - await AnthropicSamples.RunAsync(); + await Anthropic_Agent_With_Prompt_Caching.RunAsync(); } } diff --git a/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj index 6f55a04592f5..d4323ee4c924 100644 --- a/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj +++ b/dotnet/sample/AutoGen.BasicSamples/AutoGen.BasicSample.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>$(TestTargetFramework)</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <GenerateDocumentationFile>True</GenerateDocumentationFile> <NoWarn>$(NoWarn);CS8981;CS8600;CS8602;CS8604;CS8618;CS0219;SKEXP0054;SKEXP0050;SKEXP0110</NoWarn> diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs index 4833c6195c9d..f68053224663 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/CreateAnAgent.cs @@ -4,7 +4,9 @@ using AutoGen; using AutoGen.Core; using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; +using OpenAI; public partial class AssistantCodeSnippet { @@ -32,23 +34,18 @@ public void CodeSnippet2() { #region code_snippet_2 // get OpenAI Key and create config - var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); - string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); + var model = "gpt-4o-mini"; - var llmConfig = new AzureOpenAIConfig( - endpoint: endPoint, - deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name - apiKey: apiKey); + var openAIClient = new OpenAIClient(apiKey); // create assistant agent - var assistantAgent = new AssistantAgent( + var assistantAgent = new OpenAIChatAgent( name: "assistant", systemMessage: "You are an assistant that help user to do some tasks.", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = new[] { llmConfig }, - }); + chatClient: openAIClient.GetChatClient(model)) + .RegisterMessageConnector() + .RegisterPrintMessage(); #endregion code_snippet_2 } @@ -71,27 +68,21 @@ public async Task CodeSnippet4() // get OpenAI Key and create config var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint - - var llmConfig = new AzureOpenAIConfig( - endpoint: endPoint, - deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name - apiKey: apiKey); + var model = "gpt-4o-mini"; + var openAIClient = new OpenAIClient(new System.ClientModel.ApiKeyCredential(apiKey), new OpenAIClientOptions + { + Endpoint = new Uri(endPoint), + }); #region code_snippet_4 - var assistantAgent = new AssistantAgent( + var assistantAgent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient(model), name: "assistant", systemMessage: "You are an assistant that convert user input to upper case.", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = new[] - { - llmConfig - }, - FunctionContracts = new[] - { - this.UpperCaseFunctionContract, // The FunctionDefinition object for the UpperCase function - }, - }); + functions: [ + this.UpperCaseFunctionContract.ToChatTool(), // The FunctionDefinition object for the UpperCase function + ]) + .RegisterMessageConnector() + .RegisterPrintMessage(); var response = await assistantAgent.SendAsync("hello"); response.Should().BeOfType<ToolCallMessage>(); @@ -106,31 +97,24 @@ public async Task CodeSnippet5() // get OpenAI Key and create config var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); string endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); // change to your endpoint - - var llmConfig = new AzureOpenAIConfig( - endpoint: endPoint, - deploymentName: "gpt-3.5-turbo-16k", // change to your deployment name - apiKey: apiKey); + var model = "gpt-4o-mini"; + var openAIClient = new OpenAIClient(new System.ClientModel.ApiKeyCredential(apiKey), new OpenAIClientOptions + { + Endpoint = new Uri(endPoint), + }); #region code_snippet_5 - var assistantAgent = new AssistantAgent( - name: "assistant", - systemMessage: "You are an assistant that convert user input to upper case.", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = new[] - { - llmConfig - }, - FunctionContracts = new[] - { - this.UpperCaseFunctionContract, // The FunctionDefinition object for the UpperCase function - }, - }, - functionMap: new Dictionary<string, Func<string, Task<string>>> + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.UpperCaseFunctionContract], + functionMap: new Dictionary<string, Func<string, Task<string>>>() { - { this.UpperCaseFunction.Name, this.UpperCaseWrapper }, // The wrapper function for the UpperCase function + { this.UpperCaseFunctionContract.Name, this.UpperCase }, }); + var assistantAgent = new OpenAIChatAgent( + name: "assistant", + systemMessage: "You are an assistant that convert user input to upper case.", + chatClient: openAIClient.GetChatClient(model)) + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware); var response = await assistantAgent.SendAsync("hello"); response.Should().BeOfType<TextMessage>(); diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs index 2b7e25fee0c5..854a385dc341 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/FunctionCallCodeSnippet.cs @@ -3,7 +3,6 @@ using AutoGen; using AutoGen.Core; -using AutoGen.OpenAI; using FluentAssertions; public partial class FunctionCallCodeSnippet diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs index fe97152183a4..c5ff7b770338 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/GetStartCodeSnippet.cs @@ -5,6 +5,8 @@ using AutoGen; using AutoGen.Core; using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using OpenAI; #endregion snippet_GetStartCodeSnippet public class GetStartCodeSnippet @@ -13,16 +15,14 @@ public async Task CodeSnippet1() { #region code_snippet_1 var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var gpt35Config = new OpenAIConfig(openAIKey, "gpt-3.5-turbo"); + var openAIClient = new OpenAIClient(openAIKey); + var model = "gpt-4o-mini"; - var assistantAgent = new AssistantAgent( + var assistantAgent = new OpenAIChatAgent( name: "assistant", systemMessage: "You are an assistant that help user to do some tasks.", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gpt35Config], - }) + chatClient: openAIClient.GetChatClient(model)) + .RegisterMessageConnector() .RegisterPrintMessage(); // register a hook to print message nicely to console // set human input mode to ALWAYS so that user always provide input diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs index 320afd0de679..1b5a9a903207 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs @@ -13,38 +13,46 @@ public class MiddlewareAgentCodeSnippet public async Task CreateMiddlewareAgentAsync() { #region create_middleware_agent_with_original_agent - // Create an agent that always replies "Hello World" - IAgent agent = new DefaultReplyAgent(name: "assistant", defaultReply: "Hello World"); + // Create an agent that always replies "Hi!" + IAgent agent = new DefaultReplyAgent(name: "assistant", defaultReply: "Hi!"); // Create a middleware agent on top of default reply agent var middlewareAgent = new MiddlewareAgent(innerAgent: agent); middlewareAgent.Use(async (messages, options, agent, ct) => { - var lastMessage = messages.Last() as TextMessage; - lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + if (messages.Last() is TextMessage lastMessage && lastMessage.Content.Contains("Hello World")) + { + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return lastMessage; + } + return await agent.GenerateReplyAsync(messages, options, ct); }); var reply = await middlewareAgent.SendAsync("Hello World"); reply.GetContent().Should().Be("[middleware 0] Hello World"); + reply = await middlewareAgent.SendAsync("Hello AI!"); + reply.GetContent().Should().Be("Hi!"); #endregion create_middleware_agent_with_original_agent #region register_middleware_agent middlewareAgent = agent.RegisterMiddleware(async (messages, options, agent, ct) => { - var lastMessage = messages.Last() as TextMessage; - lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + if (messages.Last() is TextMessage lastMessage && lastMessage.Content.Contains("Hello World")) + { + lastMessage.Content = $"[middleware 0] {lastMessage.Content}"; + return lastMessage; + } + return await agent.GenerateReplyAsync(messages, options, ct); }); #endregion register_middleware_agent #region short_circuit_middleware_agent - // This middleware will short circuit the agent and return the last message directly. + // This middleware will short circuit the agent and return a message directly. middlewareAgent.Use(async (messages, options, agent, ct) => { - var lastMessage = messages.Last() as TextMessage; - lastMessage.Content = $"[middleware shortcut]"; - return lastMessage; + return new TextMessage(Role.Assistant, $"[middleware shortcut]"); }); #endregion short_circuit_middleware_agent } diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs index cf0452212239..60520078e72e 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/OpenAICodeSnippet.cs @@ -5,9 +5,10 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #endregion using_statement using FluentAssertions; +using OpenAI; +using OpenAI.Chat; namespace AutoGen.BasicSample.CodeSnippet; #region weather_function @@ -32,31 +33,30 @@ public async Task CreateOpenAIChatAgentAsync() { #region create_openai_chat_agent var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var modelId = "gpt-3.5-turbo"; + var modelId = "gpt-4o-mini"; var openAIClient = new OpenAIClient(openAIKey); // create an open ai chat agent var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openAIClient, + chatClient: openAIClient.GetChatClient(modelId), name: "assistant", - modelName: modelId, systemMessage: "You are an assistant that help user to do some tasks."); // OpenAIChatAgent supports the following message types: // - IMessage<ChatRequestMessage> where ChatRequestMessage is from Azure.AI.OpenAI - var helloMessage = new ChatRequestUserMessage("Hello"); + var helloMessage = new UserChatMessage("Hello"); // Use MessageEnvelope.Create to create an IMessage<ChatRequestMessage> var chatMessageContent = MessageEnvelope.Create(helloMessage); var reply = await openAIChatAgent.SendAsync(chatMessageContent); - // The type of reply is MessageEnvelope<ChatResponseMessage> where ChatResponseMessage is from Azure.AI.OpenAI - reply.Should().BeOfType<MessageEnvelope<ChatResponseMessage>>(); + // The type of reply is MessageEnvelope<ChatCompletion> where ChatResponseMessage is from Azure.AI.OpenAI + reply.Should().BeOfType<MessageEnvelope<ChatCompletion>>(); // You can un-envelop the reply to get the ChatResponseMessage - ChatResponseMessage response = reply.As<MessageEnvelope<ChatResponseMessage>>().Content; - response.Role.Should().Be(ChatRole.Assistant); + ChatCompletion response = reply.As<MessageEnvelope<ChatCompletion>>().Content; + response.Role.Should().Be(ChatMessageRole.Assistant); #endregion create_openai_chat_agent #region create_openai_chat_agent_streaming @@ -64,8 +64,8 @@ public async Task CreateOpenAIChatAgentAsync() await foreach (var streamingMessage in streamingReply) { - streamingMessage.Should().BeOfType<MessageEnvelope<StreamingChatCompletionsUpdate>>(); - streamingMessage.As<MessageEnvelope<StreamingChatCompletionsUpdate>>().Content.Role.Should().Be(ChatRole.Assistant); + streamingMessage.Should().BeOfType<MessageEnvelope<StreamingChatCompletionUpdate>>(); + streamingMessage.As<MessageEnvelope<StreamingChatCompletionUpdate>>().Content.Role.Should().Be(ChatMessageRole.Assistant); } #endregion create_openai_chat_agent_streaming @@ -77,7 +77,7 @@ public async Task CreateOpenAIChatAgentAsync() // now the agentWithConnector supports more message types var messages = new IMessage[] { - MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + MessageEnvelope.Create(new UserChatMessage("Hello")), new TextMessage(Role.Assistant, "Hello", from: "user"), new MultiModalMessage(Role.Assistant, [ @@ -106,9 +106,8 @@ public async Task OpenAIChatAgentGetWeatherFunctionCallAsync() // create an open ai chat agent var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openAIClient, + chatClient: openAIClient.GetChatClient(modelId), name: "assistant", - modelName: modelId, systemMessage: "You are an assistant that help user to do some tasks.") .RegisterMessageConnector(); diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs index bf4f9c976e22..0ac7f71a3cae 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs @@ -4,8 +4,6 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure; -using Azure.AI.OpenAI; namespace AutoGen.BasicSample.CodeSnippet; @@ -15,8 +13,8 @@ public async Task PrintMessageMiddlewareAsync() { var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var endpoint = new Uri(config.Endpoint); - var openaiClient = new OpenAIClient(endpoint, new AzureKeyCredential(config.ApiKey)); - var agent = new OpenAIChatAgent(openaiClient, "assistant", config.DeploymentName) + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); + var agent = new OpenAIChatAgent(gpt4o, "assistant", config.DeploymentName) .RegisterMessageConnector(); #region PrintMessageMiddleware @@ -31,10 +29,10 @@ public async Task PrintMessageStreamingMiddlewareAsync() { var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var endpoint = new Uri(config.Endpoint); - var openaiClient = new OpenAIClient(endpoint, new AzureKeyCredential(config.ApiKey)); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); #region print_message_streaming - var streamingAgent = new OpenAIChatAgent(openaiClient, "assistant", config.DeploymentName) + var streamingAgent = new OpenAIChatAgent(gpt4o, "assistant") .RegisterMessageConnector() .RegisterPrintMessage(); diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs index e498650b6aac..b087beb993bc 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs @@ -4,6 +4,7 @@ #region code_snippet_0_1 using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; #endregion code_snippet_0_1 namespace AutoGen.BasicSample.CodeSnippet; @@ -11,18 +12,37 @@ public class RunCodeSnippetCodeSnippet { public async Task CodeSnippet1() { - IAgent agent = default; + IAgent agent = new DefaultReplyAgent("agent", "Hello World"); #region code_snippet_1_1 - var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(workingDirectory); - var interactiveService = new InteractiveService(installingDirectory: workingDirectory); - await interactiveService.StartAsync(workingDirectory: workingDirectory); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultInProcessKernelBuilder() // add C# and F# kernels + .Build(); #endregion code_snippet_1_1 #region code_snippet_1_2 - // register dotnet code block execution hook to an arbitrary agent - var dotnetCodeAgent = agent.RegisterDotnetCodeBlockExectionHook(interactiveService: interactiveService); + // register middleware to execute code block + var dotnetCodeAgent = agent + .RegisterMiddleware(async (msgs, option, innerAgent, ct) => + { + var lastMessage = msgs.LastOrDefault(); + if (lastMessage == null || lastMessage.GetContent() is null) + { + return await innerAgent.GenerateReplyAsync(msgs, option, ct); + } + + if (lastMessage.ExtractCodeBlock("```csharp", "```") is string codeSnippet) + { + // execute code snippet + var result = await kernel.RunSubmitCodeCommandAsync(codeSnippet, "csharp"); + return new TextMessage(Role.Assistant, result, from: agent.Name); + } + else + { + // no code block found, invoke next agent + return await innerAgent.GenerateReplyAsync(msgs, option, ct); + } + }); var codeSnippet = @" ```csharp @@ -44,5 +64,17 @@ public async Task CodeSnippet1() ``` "; #endregion code_snippet_1_3 + + #region code_snippet_1_4 + var pythonKernel = DotnetInteractiveKernelBuilder + .CreateDefaultInProcessKernelBuilder() + .AddPythonKernel(venv: "python3") + .Build(); + + var pythonCode = """ + print('Hello from Python!') + """; + var result = await pythonKernel.RunSubmitCodeCommandAsync(pythonCode, "python3"); + #endregion code_snippet_1_4 } } diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs index 50bcd8a8048e..667705835eb3 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/TypeSafeFunctionCallCodeSnippet.cs @@ -3,7 +3,6 @@ using System.Text.Json; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #region weather_report_using_statement using AutoGen.Core; #endregion weather_report_using_statement @@ -32,7 +31,7 @@ public async Task Consume() var functionInstance = new TypeSafeFunctionCall(); // Get the generated function definition - FunctionDefinition functionDefiniton = functionInstance.WeatherReportFunctionContract.ToOpenAIFunctionDefinition(); + var functionDefiniton = functionInstance.WeatherReportFunctionContract.ToChatTool(); // Get the generated function wrapper Func<string, Task<string>> functionWrapper = functionInstance.WeatherReportWrapper; @@ -69,32 +68,31 @@ public async Task<string> UpperCase(string input) #region code_snippet_1 // file: FunctionDefinition.generated.cs - public FunctionDefinition UpperCaseFunction + public FunctionContract WeatherReportFunctionContract { - get => new FunctionDefinition + get => new FunctionContract { - Name = @"UpperCase", - Description = "convert input to upper case", - Parameters = BinaryData.FromObjectAsJson(new + ClassName = @"TypeSafeFunctionCall", + Name = @"WeatherReport", + Description = @"Get weather report", + ReturnType = typeof(Task<string>), + Parameters = new global::AutoGen.Core.FunctionParameterContract[] { - Type = "object", - Properties = new - { - input = new + new FunctionParameterContract { - Type = @"string", - Description = @"input", + Name = @"city", + Description = @"city", + ParameterType = typeof(string), + IsRequired = true, }, - }, - Required = new[] - { - "input", + new FunctionParameterContract + { + Name = @"date", + Description = @"date", + ParameterType = typeof(string), + IsRequired = true, }, }, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }) }; } #endregion code_snippet_1 diff --git a/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs index 3ee363bfc062..40c88102588a 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs @@ -4,6 +4,8 @@ using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; /// <summary> @@ -13,18 +15,12 @@ public static class Example01_AssistantAgent { public static async Task RunAsync() { - var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var config = new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gpt35], - }; - - // create assistant agent - var assistantAgent = new AssistantAgent( + var gpt4oMini = LLMConfiguration.GetOpenAIGPT4o_mini(); + var assistantAgent = new OpenAIChatAgent( + chatClient: gpt4oMini, name: "assistant", - systemMessage: "You convert what user said to all uppercase.", - llmConfig: config) + systemMessage: "You convert what user said to all uppercase.") + .RegisterMessageConnector() .RegisterPrintMessage(); // talk to the assistant agent diff --git a/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs index c2957f32da76..b2dd9726b4b9 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs @@ -1,30 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example02_TwoAgent_MathChat.cs -using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; public static class Example02_TwoAgent_MathChat { public static async Task RunAsync() { #region code_snippet_1 - // get gpt-3.5-turbo config - var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var gpt4oMini = LLMConfiguration.GetOpenAIGPT4o_mini(); + // create teacher agent // teacher agent will create math questions - var teacher = new AssistantAgent( + var teacher = new OpenAIChatAgent( + chatClient: gpt4oMini, name: "teacher", systemMessage: @"You are a teacher that create pre-school math question for student and check answer. If the answer is correct, you stop the conversation by saying [COMPLETE]. - If the answer is wrong, you ask student to fix it.", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gpt35], - }) + If the answer is wrong, you ask student to fix it.") + .RegisterMessageConnector() .RegisterMiddleware(async (msgs, option, agent, _) => { var reply = await agent.GenerateReplyAsync(msgs, option); @@ -39,14 +37,11 @@ public static async Task RunAsync() // create student agent // student agent will answer the math questions - var student = new AssistantAgent( + var student = new OpenAIChatAgent( + chatClient: gpt4oMini, name: "student", - systemMessage: "You are a student that answer question from teacher", - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gpt35], - }) + systemMessage: "You are a student that answer question from teacher") + .RegisterMessageConnector() .RegisterPrintMessage(); // start the conversation diff --git a/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs index 0ef8eaa48ae6..94b67a94b141 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example03_Agent_FunctionCall.cs -using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; /// <summary> @@ -45,33 +46,30 @@ public async Task<string> CalculateTax(int price, float taxRate) public static async Task RunAsync() { var instance = new Example03_Agent_FunctionCall(); - var gpt35 = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); // AutoGen makes use of AutoGen.SourceGenerator to automatically generate FunctionDefinition and FunctionCallWrapper for you. // The FunctionDefinition will be created based on function signature and XML documentation. // The return type of type-safe function needs to be Task<string>. And to get the best performance, please try only use primitive types and arrays of primitive types as parameters. - var config = new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gpt35], - FunctionContracts = new[] - { + var toolCallMiddleware = new FunctionCallMiddleware( + functions: [ instance.ConcatStringFunctionContract, instance.UpperCaseFunctionContract, instance.CalculateTaxFunctionContract, - }, - }; - - var agent = new AssistantAgent( - name: "agent", - systemMessage: "You are a helpful AI assistant", - llmConfig: config, + ], functionMap: new Dictionary<string, Func<string, Task<string>>> { - { nameof(ConcatString), instance.ConcatStringWrapper }, - { nameof(UpperCase), instance.UpperCaseWrapper }, - { nameof(CalculateTax), instance.CalculateTaxWrapper }, - }) + { nameof(instance.ConcatString), instance.ConcatStringWrapper }, + { nameof(instance.UpperCase), instance.UpperCaseWrapper }, + { nameof(instance.CalculateTax), instance.CalculateTaxWrapper }, + }); + + var agent = new OpenAIChatAgent( + chatClient: gpt4o, + name: "agent", + systemMessage: "You are a helpful AI assistant") + .RegisterMessageConnector() + .RegisterStreamingMiddleware(toolCallMiddleware) .RegisterPrintMessage(); // talk to the assistant agent diff --git a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs index 47dd8ce66c90..f90816d890e1 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example04_Dynamic_GroupChat_Coding_Task.cs -using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; public partial class Example04_Dynamic_GroupChat_Coding_Task @@ -14,46 +15,32 @@ public static async Task RunAsync() { var instance = new Example04_Dynamic_GroupChat_Coding_Task(); - // setup dotnet interactive - var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); - if (!Directory.Exists(workDir)) - Directory.CreateDirectory(workDir); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultInProcessKernelBuilder() + .AddPythonKernel("python3") + .Build(); - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); - var result = Path.Combine(workDir, "result.txt"); - if (File.Exists(result)) - File.Delete(result); - - await service.StartAsync(workDir, default); - - var gptConfig = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - - var helperAgent = new GPTAgent( - name: "helper", - systemMessage: "You are a helpful AI assistant", - temperature: 0f, - config: gptConfig); - - var groupAdmin = new GPTAgent( + var groupAdmin = new OpenAIChatAgent( + chatClient: gpt4o, name: "groupAdmin", - systemMessage: "You are the admin of the group chat", - temperature: 0f, - config: gptConfig) + systemMessage: "You are the admin of the group chat") + .RegisterMessageConnector() .RegisterPrintMessage(); - var userProxy = new UserProxyAgent(name: "user", defaultReply: GroupChatExtension.TERMINATE, humanInputMode: HumanInputMode.NEVER) + var userProxy = new DefaultReplyAgent(name: "user", defaultReply: GroupChatExtension.TERMINATE) .RegisterPrintMessage(); // Create admin agent - var admin = new AssistantAgent( + var admin = new OpenAIChatAgent( + chatClient: gpt4o, name: "admin", systemMessage: """ You are a manager who takes coding problem from user and resolve problem by splitting them into small tasks and assign each task to the most appropriate agent. Here's available agents who you can assign task to: - - coder: write dotnet code to resolve task - - runner: run dotnet code from coder + - coder: write python code to resolve task + - runner: run python code from coder The workflow is as follows: - You take the coding problem from user @@ -79,24 +66,12 @@ You are a manager who takes coding problem from user and resolve problem by spli Once the coding problem is resolved, summarize each steps and results and send the summary to the user using the following format: ```summary - { - "problem": "{coding problem}", - "steps": [ - { - "step": "{step}", - "result": "{result}" - } - ] - } + @user, <summary of the task> ``` Your reply must contain one of [task|ask|summary] to indicate the type of your message. - """, - llmConfig: new ConversableAgentConfig - { - Temperature = 0, - ConfigList = [gptConfig], - }) + """) + .RegisterMessageConnector() .RegisterPrintMessage(); // create coder agent @@ -104,30 +79,27 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m // The dotnet coder write dotnet code to resolve the task. // The code reviewer review the code block from coder's reply. // The nuget agent install nuget packages if there's any. - var coderAgent = new GPTAgent( + var coderAgent = new OpenAIChatAgent( name: "coder", - systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you. + chatClient: gpt4o, + systemMessage: @"You act as python coder, you write python code to resolve task. Once you finish writing code, ask runner to run the code for you. Here're some rules to follow on writing dotnet code: -- put code between ```csharp and ``` -- When creating http client, use `var httpClient = new HttpClient()`. Don't use `using var httpClient = new HttpClient()` because it will cause error when running the code. -- Try to use `var` instead of explicit type. -- Try avoid using external library, use .NET Core library instead. -- Use top level statement to write code. +- put code between ```python and ``` +- Try avoid using external library - Always print out the result to console. Don't write code that doesn't print out anything. -If you need to install nuget packages, put nuget packages in the following format: -```nuget -nuget_package_name +Use the following format to install pip package: +```python +%pip install <package_name> ``` If your code is incorrect, Fix the error and send the code again. Here's some externel information - The link to mlnet repo is: https://github.com/dotnet/machinelearning. you don't need a token to use github pr api. Make sure to include a User-Agent header, otherwise github will reject it. -", - config: gptConfig, - temperature: 0.4f) +") + .RegisterMessageConnector() .RegisterPrintMessage(); // code reviewer agent will review if code block from coder's reply satisfy the following conditions: @@ -135,14 +107,13 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m // - The code block is csharp code block // - The code block is top level statement // - The code block is not using declaration - var codeReviewAgent = new GPTAgent( + var codeReviewAgent = new OpenAIChatAgent( + chatClient: gpt4o, name: "reviewer", systemMessage: """ You are a code reviewer who reviews code from coder. You need to check if the code satisfy the following conditions: - - The reply from coder contains at least one code block, e.g ```csharp and ``` - - There's only one code block and it's csharp code block - - The code block is not inside a main function. a.k.a top level statement - - The code block is not using declaration when creating http client + - The reply from coder contains at least one code block, e.g ```python and ``` + - There's only one code block and it's python code block You don't check the code style, only check if the code satisfy the above conditions. @@ -160,23 +131,40 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m result: REJECTED ``` - """, - config: gptConfig, - temperature: 0f) + """) + .RegisterMessageConnector() .RegisterPrintMessage(); // create runner agent // The runner agent will run the code block from coder's reply. // It runs dotnet code using dotnet interactive service hook. // It also truncate the output if the output is too long. - var runner = new AssistantAgent( + var runner = new DefaultReplyAgent( name: "runner", defaultReply: "No code available, coder, write code please") - .RegisterDotnetCodeBlockExectionHook(interactiveService: service) .RegisterMiddleware(async (msgs, option, agent, ct) => { var mostRecentCoderMessage = msgs.LastOrDefault(x => x.From == "coder") ?? throw new Exception("No coder message found"); - return await agent.GenerateReplyAsync(new[] { mostRecentCoderMessage }, option, ct); + + if (mostRecentCoderMessage.ExtractCodeBlock("```python", "```") is string code) + { + var result = await kernel.RunSubmitCodeCommandAsync(code, "python"); + // only keep the first 500 characters + if (result.Length > 500) + { + result = result.Substring(0, 500); + } + result = $""" + # [CODE_BLOCK_EXECUTION_RESULT] + {result} + """; + + return new TextMessage(Role.Assistant, result, from: agent.Name); + } + else + { + return await agent.GenerateReplyAsync(msgs, option, ct); + } }) .RegisterPrintMessage(); @@ -247,18 +235,27 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m workflow: workflow); // task 1: retrieve the most recent pr from mlnet and save it in result.txt - var groupChatManager = new GroupChatManager(groupChat); - await userProxy.SendAsync(groupChatManager, "Retrieve the most recent pr from mlnet and save it in result.txt", maxRound: 30); - File.Exists(result).Should().BeTrue(); - - // task 2: calculate the 39th fibonacci number - var answer = 63245986; - // clear the result file - File.Delete(result); + var task = """ + retrieve the most recent pr from mlnet and save it in result.txt + """; + var chatHistory = new List<IMessage> + { + new TextMessage(Role.Assistant, task) + { + From = userProxy.Name + } + }; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound: 10)) + { + if (message.From == admin.Name && message.GetContent().Contains("```summary")) + { + // Task complete! + break; + } + } - var conversationHistory = await userProxy.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number? Save the result in result.txt", maxRound: 10); + // check if the result file is created + var result = "result.txt"; File.Exists(result).Should().BeTrue(); - var resultContent = File.ReadAllText(result); - resultContent.Should().Contain(answer.ToString()); } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs b/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs index ba7b5d4bde44..e8dd86474e7a 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example05_Dalle_And_GPT4V.cs @@ -4,9 +4,9 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; using FluentAssertions; -using autogen = AutoGen.LLMConfigAPI; +using OpenAI; +using OpenAI.Images; public partial class Example05_Dalle_And_GPT4V { @@ -30,16 +30,12 @@ public async Task<string> GenerateImage(string prompt) // and return url. var option = new ImageGenerationOptions { - Size = ImageSize.Size1024x1024, - Style = ImageGenerationStyle.Vivid, - ImageCount = 1, - Prompt = prompt, - Quality = ImageGenerationQuality.Standard, - DeploymentName = "dall-e-3", + Size = GeneratedImageSize.W1024xH1024, + Style = GeneratedImageStyle.Vivid, }; - var imageResponse = await openAIClient.GetImageGenerationsAsync(option); - var imageUrl = imageResponse.Value.Data.First().Url.OriginalString; + var imageResponse = await openAIClient.GetImageClient("dall-e-3").GenerateImageAsync(prompt, option); + var imageUrl = imageResponse.Value.ImageUri.OriginalString; return $@"// ignore this line [IMAGE_GENERATION] The image is generated from prompt {prompt} @@ -57,8 +53,6 @@ public static async Task RunAsync() // get OpenAI Key and create config var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var gpt35Config = autogen.GetOpenAIConfigList(openAIKey, new[] { "gpt-3.5-turbo" }); - var gpt4vConfig = autogen.GetOpenAIConfigList(openAIKey, new[] { "gpt-4-vision-preview" }); var openAIClient = new OpenAIClient(openAIKey); var instance = new Example05_Dalle_And_GPT4V(openAIClient); var imagePath = Path.Combine("resource", "images", "background.png"); @@ -74,8 +68,7 @@ public static async Task RunAsync() { nameof(GenerateImage), instance.GenerateImageWrapper }, }); var dalleAgent = new OpenAIChatAgent( - openAIClient: openAIClient, - modelName: "gpt-3.5-turbo", + chatClient: openAIClient.GetChatClient("gpt-4o-mini"), name: "dalle", systemMessage: "You are a DALL-E agent that generate image from prompt, when conversation is terminated, return the most recent image url") .RegisterMessageConnector() @@ -110,9 +103,8 @@ public static async Task RunAsync() .RegisterPrintMessage(); var gpt4VAgent = new OpenAIChatAgent( - openAIClient: openAIClient, - name: "gpt4v", - modelName: "gpt-4-vision-preview", + chatClient: openAIClient.GetChatClient("gpt-4o-mini"), + name: "gpt-4o-mini", systemMessage: @"You are a critism that provide feedback to DALL-E agent. Carefully check the image generated by DALL-E agent and provide feedback. If the image satisfies the condition, then say [APPROVE]. diff --git a/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs index dd3b5a671921..e1349cb32a99 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example06_UserProxyAgent.cs @@ -2,6 +2,7 @@ // Example06_UserProxyAgent.cs using AutoGen.Core; using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; namespace AutoGen.BasicSample; @@ -9,12 +10,13 @@ public static class Example06_UserProxyAgent { public static async Task RunAsync() { - var gpt35 = LLMConfiguration.GetOpenAIGPT3_5_Turbo(); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); - var assistantAgent = new GPTAgent( + var assistantAgent = new OpenAIChatAgent( + chatClient: gpt4o, name: "assistant", - systemMessage: "You are an assistant that help user to do some tasks.", - config: gpt35) + systemMessage: "You are an assistant that help user to do some tasks.") + .RegisterMessageConnector() .RegisterPrintMessage(); // set human input mode to ALWAYS so that user always provide input diff --git a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs index 6584baa5fae5..1f1315586a28 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs @@ -3,12 +3,14 @@ using System.Text; using System.Text.Json; -using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; using AutoGen.OpenAI; -using FluentAssertions; +using AutoGen.OpenAI.Extension; +using Microsoft.DotNet.Interactive; +using OpenAI.Chat; public partial class Example07_Dynamic_GroupChat_Calculate_Fibonacci { @@ -48,10 +50,10 @@ public async Task<string> ReviewCodeBlock( #endregion reviewer_function #region create_coder - public static async Task<IAgent> CreateCoderAgentAsync() + public static async Task<IAgent> CreateCoderAgentAsync(ChatClient client) { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var coder = new GPTAgent( + var coder = new OpenAIChatAgent( + chatClient: client, name: "coder", systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you. @@ -69,8 +71,8 @@ public static async Task<IAgent> CreateCoderAgentAsync() ``` If your code is incorrect, runner will tell you the error message. Fix the error and send the code again.", - config: gpt3Config, temperature: 0.4f) + .RegisterMessageConnector() .RegisterPrintMessage(); return coder; @@ -78,13 +80,11 @@ public static async Task<IAgent> CreateCoderAgentAsync() #endregion create_coder #region create_runner - public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService service) + public static async Task<IAgent> CreateRunnerAgentAsync(Kernel kernel) { - var runner = new AssistantAgent( + var runner = new DefaultReplyAgent( name: "runner", - systemMessage: "You run dotnet code", defaultReply: "No code available.") - .RegisterDotnetCodeBlockExectionHook(interactiveService: service) .RegisterMiddleware(async (msgs, option, agent, _) => { if (msgs.Count() == 0 || msgs.All(msg => msg.From != "coder")) @@ -94,7 +94,24 @@ public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService servi else { var coderMsg = msgs.Last(msg => msg.From == "coder"); - return await agent.GenerateReplyAsync([coderMsg], option); + if (coderMsg.ExtractCodeBlock("```csharp", "```") is string code) + { + var codeResult = await kernel.RunSubmitCodeCommandAsync(code, "csharp"); + + codeResult = $""" + [RUNNER_RESULT] + {codeResult} + """; + + return new TextMessage(Role.Assistant, codeResult) + { + From = "runner", + }; + } + else + { + return new TextMessage(Role.Assistant, "No code available. Coder please write code"); + } } }) .RegisterPrintMessage(); @@ -104,45 +121,35 @@ public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService servi #endregion create_runner #region create_admin - public static async Task<IAgent> CreateAdminAsync() + public static async Task<IAgent> CreateAdminAsync(ChatClient client) { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var admin = new GPTAgent( + var admin = new OpenAIChatAgent( + chatClient: client, name: "admin", - systemMessage: "You are group admin, terminate the group chat once task is completed by saying [TERMINATE] plus the final answer", - temperature: 0, - config: gpt3Config) - .RegisterMiddleware(async (msgs, option, agent, _) => - { - var reply = await agent.GenerateReplyAsync(msgs, option); - if (reply is TextMessage textMessage && textMessage.Content.Contains("TERMINATE") is true) - { - var content = $"{textMessage.Content}\n\n {GroupChatExtension.TERMINATE}"; - - return new TextMessage(Role.Assistant, content, from: reply.From); - } - - return reply; - }); + temperature: 0) + .RegisterMessageConnector() + .RegisterPrintMessage(); return admin; } #endregion create_admin #region create_reviewer - public static async Task<IAgent> CreateReviewerAgentAsync() + public static async Task<IAgent> CreateReviewerAgentAsync(ChatClient chatClient) { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var functions = new Example07_Dynamic_GroupChat_Calculate_Fibonacci(); - var reviewer = new GPTAgent( - name: "code_reviewer", - systemMessage: @"You review code block from coder", - config: gpt3Config, - functions: [functions.ReviewCodeBlockFunction], + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [functions.ReviewCodeBlockFunctionContract], functionMap: new Dictionary<string, Func<string, Task<string>>>() { - { nameof(ReviewCodeBlock), functions.ReviewCodeBlockWrapper }, - }) + { nameof(functions.ReviewCodeBlock), functions.ReviewCodeBlockWrapper }, + }); + var reviewer = new OpenAIChatAgent( + chatClient: chatClient, + name: "code_reviewer", + systemMessage: @"You review code block from coder") + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware) .RegisterMiddleware(async (msgs, option, innerAgent, ct) => { var maxRetry = 3; @@ -222,20 +229,17 @@ public static async Task<IAgent> CreateReviewerAgentAsync() public static async Task RunWorkflowAsync() { long the39thFibonacciNumber = 63245986; - var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); - if (!Directory.Exists(workDir)) - Directory.CreateDirectory(workDir); - - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultInProcessKernelBuilder() + .Build(); - await service.StartAsync(workDir, default); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); #region create_workflow - var reviewer = await CreateReviewerAgentAsync(); - var coder = await CreateCoderAgentAsync(); - var runner = await CreateRunnerAgentAsync(service); - var admin = await CreateAdminAsync(); + var reviewer = await CreateReviewerAgentAsync(gpt4o); + var coder = await CreateCoderAgentAsync(gpt4o); + var runner = await CreateRunnerAgentAsync(kernel); + var admin = await CreateAdminAsync(gpt4o); var admin2CoderTransition = Transition.Create(admin, coder); var coder2ReviewerTransition = Transition.Create(coder, reviewer); @@ -306,21 +310,23 @@ public static async Task RunWorkflowAsync() runner, reviewer, ]); - + #endregion create_group_chat_with_workflow admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); reviewer.SendIntroduction("I will review dotnet code", groupChat); runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); + var task = "What's the 39th of fibonacci number?"; - var groupChatManager = new GroupChatManager(groupChat); - var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); - #endregion create_group_chat_with_workflow - // the last message is from admin, which is the termination message - var lastMessage = conversationHistory.Last(); - lastMessage.From.Should().Be("admin"); - lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); - lastMessage.Should().BeOfType<TextMessage>(); - lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + var taskMessage = new TextMessage(Role.User, task, from: admin.Name); + await foreach (var message in groupChat.SendAsync([taskMessage], maxRound: 10)) + { + // teminate chat if message is from runner and run successfully + if (message.From == "runner" && message.GetContent().Contains(the39thFibonacciNumber.ToString())) + { + Console.WriteLine($"The 39th of fibonacci number is {the39thFibonacciNumber}"); + break; + } + } } public static async Task RunAsync() @@ -328,41 +334,44 @@ public static async Task RunAsync() long the39thFibonacciNumber = 63245986; var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); if (!Directory.Exists(workDir)) + { Directory.CreateDirectory(workDir); + } - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); - await service.StartAsync(workDir, default); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultInProcessKernelBuilder() + .Build(); #region create_group_chat - var reviewer = await CreateReviewerAgentAsync(); - var coder = await CreateCoderAgentAsync(); - var runner = await CreateRunnerAgentAsync(service); - var admin = await CreateAdminAsync(); + var reviewer = await CreateReviewerAgentAsync(gpt4o); + var coder = await CreateCoderAgentAsync(gpt4o); + var runner = await CreateRunnerAgentAsync(kernel); + var admin = await CreateAdminAsync(gpt4o); var groupChat = new GroupChat( admin: admin, members: [ - admin, coder, runner, reviewer, ]); - admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); reviewer.SendIntroduction("I will review dotnet code", groupChat); runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); - var groupChatManager = new GroupChatManager(groupChat); - var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); - - // the last message is from admin, which is the termination message - var lastMessage = conversationHistory.Last(); - lastMessage.From.Should().Be("admin"); - lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); - lastMessage.Should().BeOfType<TextMessage>(); - lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + var task = "What's the 39th of fibonacci number?"; + var taskMessage = new TextMessage(Role.User, task); + await foreach (var message in groupChat.SendAsync([taskMessage], maxRound: 10)) + { + // teminate chat if message is from runner and run successfully + if (message.From == "runner" && message.GetContent().Contains(the39thFibonacciNumber.ToString())) + { + Console.WriteLine($"The 39th of fibonacci number is {the39thFibonacciNumber}"); + break; + } + } #endregion create_group_chat } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs b/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs index cce330117622..e58454fdb5f8 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example08_LMStudio.cs @@ -3,7 +3,9 @@ #region lmstudio_using_statements using AutoGen.Core; -using AutoGen.LMStudio; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using OpenAI; #endregion lmstudio_using_statements namespace AutoGen.BasicSample; @@ -13,8 +15,16 @@ public class Example08_LMStudio public static async Task RunAsync() { #region lmstudio_example_1 - var config = new LMStudioConfig("localhost", 1234); - var lmAgent = new LMStudioAgent("asssistant", config: config) + var endpoint = "http://localhost:1234"; + var openaiClient = new OpenAIClient("api-key", new OpenAIClientOptions + { + Endpoint = new Uri(endpoint), + }); + + var lmAgent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient("<does-not-matter>"), + name: "assistant") + .RegisterMessageConnector() .RegisterPrintMessage(); await lmAgent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); diff --git a/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs b/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs deleted file mode 100644 index 9a62144df2bd..000000000000 --- a/dotnet/sample/AutoGen.BasicSamples/Example09_LMStudio_FunctionCall.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Example09_LMStudio_FunctionCall.cs - -using System.Text.Json; -using System.Text.Json.Serialization; -using AutoGen.Core; -using AutoGen.LMStudio; -using Azure.AI.OpenAI; - -namespace AutoGen.BasicSample; - -public class LLaMAFunctionCall -{ - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("arguments")] - public JsonElement Arguments { get; set; } -} - -public partial class Example09_LMStudio_FunctionCall -{ - /// <summary> - /// Get weather from location. - /// </summary> - /// <param name="location">location</param> - /// <param name="date">date. type is string</param> - [Function] - public async Task<string> GetWeather(string location, string date) - { - return $"[Function] The weather on {date} in {location} is sunny."; - } - - - /// <summary> - /// Search query on Google and return the results. - /// </summary> - /// <param name="query">search query</param> - [Function] - public async Task<string> GoogleSearch(string query) - { - return $"[Function] Here are the search results for {query}."; - } - - private static object SerializeFunctionDefinition(FunctionDefinition functionDefinition) - { - return new - { - type = "function", - function = new - { - name = functionDefinition.Name, - description = functionDefinition.Description, - parameters = functionDefinition.Parameters.ToObjectFromJson<object>(), - } - }; - } - - public static async Task RunAsync() - { - #region lmstudio_function_call_example - // This example has been verified to work with Trelis-Llama-2-7b-chat-hf-function-calling-v3 - var instance = new Example09_LMStudio_FunctionCall(); - var config = new LMStudioConfig("localhost", 1234); - var systemMessage = @$"You are a helpful AI assistant."; - - // Because the LM studio server doesn't support openai function call yet - // To simulate the function call, we can put the function call details in the system message - // And ask agent to response in function call object format using few-shot example - object[] functionList = - [ - SerializeFunctionDefinition(instance.GetWeatherFunction), - SerializeFunctionDefinition(instance.GoogleSearchFunction) - ]; - var functionListString = JsonSerializer.Serialize(functionList, new JsonSerializerOptions { WriteIndented = true }); - var lmAgent = new LMStudioAgent( - name: "assistant", - systemMessage: @$" -You are a helpful AI assistant -You have access to the following functions. Use them if required: - -{functionListString}", - config: config) - .RegisterMiddleware(async (msgs, option, innerAgent, ct) => - { - // inject few-shot example to the message - var exampleGetWeather = new TextMessage(Role.User, "Get weather in London"); - var exampleAnswer = new TextMessage(Role.Assistant, "{\n \"name\": \"GetWeather\",\n \"arguments\": {\n \"city\": \"London\"\n }\n}", from: innerAgent.Name); - - msgs = new[] { exampleGetWeather, exampleAnswer }.Concat(msgs).ToArray(); - var reply = await innerAgent.GenerateReplyAsync(msgs, option, ct); - - // if reply is a function call, invoke function - var content = reply.GetContent(); - try - { - if (JsonSerializer.Deserialize<LLaMAFunctionCall>(content) is { } functionCall) - { - var arguments = JsonSerializer.Serialize(functionCall.Arguments); - // invoke function wrapper - if (functionCall.Name == instance.GetWeatherFunction.Name) - { - var result = await instance.GetWeatherWrapper(arguments); - return new TextMessage(Role.Assistant, result); - } - else if (functionCall.Name == instance.GoogleSearchFunction.Name) - { - var result = await instance.GoogleSearchWrapper(arguments); - return new TextMessage(Role.Assistant, result); - } - else - { - throw new Exception($"Unknown function call: {functionCall.Name}"); - } - } - } - catch (JsonException) - { - // ignore - } - - return reply; - }) - .RegisterPrintMessage(); - - var userProxyAgent = new UserProxyAgent( - name: "user", - humanInputMode: HumanInputMode.ALWAYS); - - await userProxyAgent.SendAsync( - receiver: lmAgent, - "Search the names of the five largest stocks in the US by market cap "); - #endregion lmstudio_function_call_example - } -} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs b/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs index 61c341204ec2..da7e54852f34 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example10_SemanticKernel.cs @@ -39,7 +39,7 @@ public class Example10_SemanticKernel public static async Task RunAsync() { var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var modelId = "gpt-3.5-turbo"; + var modelId = "gpt-4o-mini"; var builder = Kernel.CreateBuilder() .AddOpenAIChatCompletion(modelId: modelId, apiKey: openAIKey); var kernel = builder.Build(); diff --git a/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs b/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs index 00ff321082a4..32aaa8c187b4 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example11_Sequential_GroupChat_Example.cs @@ -7,7 +7,6 @@ using AutoGen.OpenAI.Extension; using AutoGen.SemanticKernel; using AutoGen.SemanticKernel.Extension; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.Web; using Microsoft.SemanticKernel.Plugins.Web.Bing; @@ -52,15 +51,10 @@ You put the original search result between ```bing and ``` public static async Task<IAgent> CreateSummarizerAgentAsync() { #region CreateSummarizerAgent - var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var apiKey = config.ApiKey; - var endPoint = new Uri(config.Endpoint); - - var openAIClient = new OpenAIClient(endPoint, new Azure.AzureKeyCredential(apiKey)); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var openAIClientAgent = new OpenAIChatAgent( - openAIClient: openAIClient, + chatClient: gpt4o, name: "summarizer", - modelName: config.DeploymentName, systemMessage: "You summarize search result from bing in a short and concise manner"); return openAIClientAgent diff --git a/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs index b622a3e641ef..69c2121cd80b 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example12_TwoAgent_Fill_Application.cs @@ -5,7 +5,6 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; namespace AutoGen.BasicSample; @@ -69,11 +68,7 @@ public async Task<string> SaveProgress( public static async Task<IAgent> CreateSaveProgressAgent() { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); - var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); - + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var instance = new TwoAgent_Fill_Application(); var functionCallConnector = new FunctionCallMiddleware( functions: [instance.SaveProgressFunctionContract], @@ -83,9 +78,8 @@ public static async Task<IAgent> CreateSaveProgressAgent() }); var chatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "application", - modelName: gpt3Config.DeploymentName, systemMessage: """You are a helpful application form assistant who saves progress while user fills application.""") .RegisterMessageConnector() .RegisterMiddleware(functionCallConnector) @@ -109,48 +103,23 @@ Save progress according to the most recent information provided by user. public static async Task<IAgent> CreateAssistantAgent() { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); - var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); - + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var chatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "assistant", - modelName: gpt3Config.DeploymentName, systemMessage: """You create polite prompt to ask user provide missing information""") .RegisterMessageConnector() - .RegisterPrintMessage() - .RegisterMiddleware(async (msgs, option, agent, ct) => - { - var lastReply = msgs.Last() ?? throw new Exception("No reply found."); - var reply = await agent.GenerateReplyAsync(msgs, option, ct); - - // if application is complete, exit conversation by sending termination message - if (lastReply.GetContent().Contains("Application information is saved to database.")) - { - return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, from: agent.Name); - } - else - { - return reply; - } - }); + .RegisterPrintMessage(); return chatAgent; } public static async Task<IAgent> CreateUserAgent() { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var endPoint = gpt3Config.Endpoint ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var apiKey = gpt3Config.ApiKey ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); - var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); - + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var chatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "user", - modelName: gpt3Config.DeploymentName, systemMessage: """ You are a user who is filling an application form. Simply provide the information as requested and answer the questions, don't do anything else. @@ -191,9 +160,13 @@ public static async Task RunAsync() var groupChatManager = new GroupChatManager(groupChat); var initialMessage = await assistantAgent.SendAsync("Generate a greeting meesage for user and start the conversation by asking what's their name."); - var chatHistory = await userAgent.SendAsync(groupChatManager, [initialMessage], maxRound: 30); - - var lastMessage = chatHistory.Last(); - Console.WriteLine(lastMessage.GetContent()); + var chatHistory = new List<IMessage> { initialMessage }; + await foreach (var msg in userAgent.SendAsync(groupChatManager, chatHistory, maxRound: 30)) + { + if (msg.GetContent().ToLower().Contains("application information is saved to database.") is true) + { + break; + } + } } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs b/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs index dadad7f00b99..596ab08d02a1 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs @@ -1,68 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example13_OpenAIAgent_JsonMode.cs -using System.Text.Json; -using System.Text.Json.Serialization; -using AutoGen.Core; -using AutoGen.OpenAI; -using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; -using FluentAssertions; +// this example has been moved to https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs -namespace AutoGen.BasicSample; - -public class Example13_OpenAIAgent_JsonMode -{ - public static async Task RunAsync() - { - #region create_agent - var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(deployName: "gpt-35-turbo"); // json mode only works with 0125 and later model. - var apiKey = config.ApiKey; - var endPoint = new Uri(config.Endpoint); - - var openAIClient = new OpenAIClient(endPoint, new Azure.AzureKeyCredential(apiKey)); - var openAIClientAgent = new OpenAIChatAgent( - openAIClient: openAIClient, - name: "assistant", - modelName: config.DeploymentName, - systemMessage: "You are a helpful assistant designed to output JSON.", - seed: 0, // explicitly set a seed to enable deterministic output - responseFormat: ChatCompletionsResponseFormat.JsonObject) // set response format to JSON object to enable JSON mode - .RegisterMessageConnector() - .RegisterPrintMessage(); - #endregion create_agent - - #region chat_with_agent - var reply = await openAIClientAgent.SendAsync("My name is John, I am 25 years old, and I live in Seattle."); - - var person = JsonSerializer.Deserialize<Person>(reply.GetContent()); - Console.WriteLine($"Name: {person.Name}"); - Console.WriteLine($"Age: {person.Age}"); - - if (!string.IsNullOrEmpty(person.Address)) - { - Console.WriteLine($"Address: {person.Address}"); - } - - Console.WriteLine("Done."); - #endregion chat_with_agent - - person.Name.Should().Be("John"); - person.Age.Should().Be(25); - person.Address.Should().BeNullOrEmpty(); - } -} - -#region person_class -public class Person -{ - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("age")] - public int Age { get; set; } - - [JsonPropertyName("address")] - public string Address { get; set; } -} -#endregion person_class diff --git a/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs index 788122d3f383..4a4b10ae3d75 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs @@ -3,6 +3,7 @@ using AutoGen.Core; using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; namespace AutoGen.BasicSample; @@ -27,14 +28,14 @@ public static class Example15_GPT4V_BinaryDataImageMessage public static async Task RunAsync() { - var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var openAiConfig = new OpenAIConfig(openAIKey, "gpt-4o"); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); - var visionAgent = new GPTAgent( + var visionAgent = new OpenAIChatAgent( + chatClient: gpt4o, name: "gpt", systemMessage: "You are a helpful AI assistant", - config: openAiConfig, temperature: 0) + .RegisterMessageConnector() .RegisterPrintMessage(); List<IMessage> messages = @@ -50,7 +51,9 @@ private static void AddMessagesFromResource(string imageResourcePath, List<IMess foreach (string file in Directory.GetFiles(imageResourcePath)) { if (!_mediaTypeMappings.TryGetValue(Path.GetExtension(file).ToLowerInvariant(), out var mediaType)) + { continue; + } using var fs = new FileStream(file, FileMode.Open, FileAccess.Read); var ms = new MemoryStream(); diff --git a/dotnet/sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs b/dotnet/sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs index eb8bcb179be1..969f7dc21c7d 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs @@ -1,62 +1,4 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs -#region using_statement -using AutoGen.Core; -using AutoGen.OpenAI; -using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; -using Azure.Core.Pipeline; -#endregion using_statement -namespace AutoGen.BasicSample; - -#region CustomHttpClientHandler -public sealed class CustomHttpClientHandler : HttpClientHandler -{ - private string _modelServiceUrl; - - public CustomHttpClientHandler(string modelServiceUrl) - { - _modelServiceUrl = modelServiceUrl; - } - - protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - request.RequestUri = new Uri($"{_modelServiceUrl}{request.RequestUri.PathAndQuery}"); - - return base.SendAsync(request, cancellationToken); - } -} -#endregion CustomHttpClientHandler - -public class Example16_OpenAIChatAgent_ConnectToThirdPartyBackend -{ - public static async Task RunAsync() - { - #region create_agent - using var client = new HttpClient(new CustomHttpClientHandler("http://localhost:11434")); - var option = new OpenAIClientOptions(OpenAIClientOptions.ServiceVersion.V2024_04_01_Preview) - { - Transport = new HttpClientTransport(client), - }; - - // api-key is not required for local server - // so you can use any string here - var openAIClient = new OpenAIClient("api-key", option); - var model = "llama3"; - - var agent = new OpenAIChatAgent( - openAIClient: openAIClient, - name: "assistant", - modelName: model, - systemMessage: "You are a helpful assistant designed to output JSON.", - seed: 0) - .RegisterMessageConnector() - .RegisterPrintMessage(); - #endregion create_agent - - #region send_message - await agent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); - #endregion send_message - } -} +// this example has been moved to https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs diff --git a/dotnet/sample/AutoGen.BasicSamples/Example17_ReActAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example17_ReActAgent.cs index f598ebbf7c46..170736bf22e4 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example17_ReActAgent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example17_ReActAgent.cs @@ -4,14 +4,14 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; +using OpenAI; +using OpenAI.Chat; namespace AutoGen.BasicSample; public class OpenAIReActAgent : IAgent { - private readonly OpenAIClient _client; - private readonly string modelName = "gpt-3.5-turbo"; + private readonly ChatClient _client; private readonly FunctionContract[] tools; private readonly Dictionary<string, Func<string, Task<string>>> toolExecutors = new(); private readonly IAgent reasoner; @@ -39,16 +39,15 @@ public class OpenAIReActAgent : IAgent Begin! Question: {input}"; - public OpenAIReActAgent(OpenAIClient client, string modelName, string name, FunctionContract[] tools, Dictionary<string, Func<string, Task<string>>> toolExecutors) + public OpenAIReActAgent(ChatClient client, string name, FunctionContract[] tools, Dictionary<string, Func<string, Task<string>>> toolExecutors) { _client = client; this.Name = name; - this.modelName = modelName; this.tools = tools; this.toolExecutors = toolExecutors; this.reasoner = CreateReasoner(); this.actor = CreateActor(); - this.helper = new OpenAIChatAgent(client, "helper", modelName) + this.helper = new OpenAIChatAgent(client, "helper") .RegisterMessageConnector(); } @@ -106,8 +105,7 @@ private string CreateReActPrompt(string input) private IAgent CreateReasoner() { return new OpenAIChatAgent( - openAIClient: _client, - modelName: modelName, + chatClient: _client, name: "reasoner") .RegisterMessageConnector() .RegisterPrintMessage(); @@ -117,8 +115,7 @@ private IAgent CreateActor() { var functionCallMiddleware = new FunctionCallMiddleware(tools, toolExecutors); return new OpenAIChatAgent( - openAIClient: _client, - modelName: modelName, + chatClient: _client, name: "actor") .RegisterMessageConnector() .RegisterMiddleware(functionCallMiddleware) @@ -166,9 +163,9 @@ public static async Task RunAsync() var modelName = "gpt-4-turbo"; var tools = new Tools(); var openAIClient = new OpenAIClient(openAIKey); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var reactAgent = new OpenAIReActAgent( - client: openAIClient, - modelName: modelName, + client: openAIClient.GetChatClient(modelName), name: "react-agent", tools: [tools.GetLocalizationFunctionContract, tools.GetDateTodayFunctionContract, tools.WeatherReportFunctionContract], toolExecutors: new Dictionary<string, Func<string, Task<string>>> diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Agent_Middleware.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Agent_Middleware.cs index 57f8ab4075c2..cf97af134675 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Agent_Middleware.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Agent_Middleware.cs @@ -5,9 +5,9 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #endregion Using using FluentAssertions; +using OpenAI.Chat; namespace AutoGen.BasicSample; @@ -16,20 +16,17 @@ public class Agent_Middleware public static async Task RunTokenCountAsync() { #region Create_Agent - var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("Please set the environment variable OPENAI_API_KEY"); - var model = "gpt-3.5-turbo"; - var openaiClient = new OpenAIClient(apiKey); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var openaiMessageConnector = new OpenAIChatRequestMessageConnector(); var totalTokenCount = 0; var agent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "agent", - modelName: model, systemMessage: "You are a helpful AI assistant") .RegisterMiddleware(async (messages, option, innerAgent, ct) => { var reply = await innerAgent.GenerateReplyAsync(messages, option, ct); - if (reply is MessageEnvelope<ChatCompletions> chatCompletions) + if (reply is MessageEnvelope<ChatCompletion> chatCompletions) { var tokenCount = chatCompletions.Content.Usage.TotalTokens; totalTokenCount += tokenCount; @@ -53,21 +50,17 @@ public static async Task RunTokenCountAsync() public static async Task RunRagTaskAsync() { #region Create_Agent - var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("Please set the environment variable OPENAI_API_KEY"); - var model = "gpt-3.5-turbo"; - var openaiClient = new OpenAIClient(apiKey); - var openaiMessageConnector = new OpenAIChatRequestMessageConnector(); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var agent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "agent", - modelName: model, systemMessage: "You are a helpful AI assistant") .RegisterMessageConnector() .RegisterMiddleware(async (messages, option, innerAgent, ct) => { var today = DateTime.UtcNow; var todayMessage = new TextMessage(Role.System, $"Today is {today:yyyy-MM-dd}"); - messages = messages.Concat(new[] { todayMessage }); + messages = messages.Concat([todayMessage]); return await innerAgent.GenerateReplyAsync(messages, option, ct); }) .RegisterPrintMessage(); diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Chat_With_Agent.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Chat_With_Agent.cs index 0ac1cda75288..b2cc228496db 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Chat_With_Agent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Chat_With_Agent.cs @@ -5,7 +5,6 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #endregion Using using FluentAssertions; @@ -17,13 +16,10 @@ public class Chat_With_Agent public static async Task RunAsync() { #region Create_Agent - var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var model = "gpt-3.5-turbo"; - var openaiClient = new OpenAIClient(apiKey); + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var agent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "agent", - modelName: model, systemMessage: "You are a helpful AI assistant") .RegisterMessageConnector(); // convert OpenAI message to AutoGen message #endregion Create_Agent diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Dynamic_Group_Chat.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Dynamic_Group_Chat.cs index 9d21bbde7d30..dadc295e308d 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Dynamic_Group_Chat.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Dynamic_Group_Chat.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Dynamic_GroupChat.cs +// Dynamic_Group_Chat.cs using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; using AutoGen.SemanticKernel; using AutoGen.SemanticKernel.Extension; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; +using OpenAI; namespace AutoGen.BasicSample; @@ -16,14 +16,13 @@ public class Dynamic_Group_Chat public static async Task RunAsync() { var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var model = "gpt-3.5-turbo"; + var model = "gpt-4o-mini"; #region Create_Coder var openaiClient = new OpenAIClient(apiKey); var coder = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: openaiClient.GetChatClient(model), name: "coder", - modelName: model, systemMessage: "You are a C# coder, when writing csharp code, please put the code between ```csharp and ```") .RegisterMessageConnector() // convert OpenAI message to AutoGen message .RegisterPrintMessage(); // print the message content @@ -49,9 +48,8 @@ public static async Task RunAsync() #region Create_Group var admin = new OpenAIChatAgent( - openAIClient: openaiClient, - name: "admin", - modelName: model) + chatClient: openaiClient.GetChatClient(model), + name: "admin") .RegisterMessageConnector(); // convert OpenAI message to AutoGen message var group = new GroupChat( diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/FSM_Group_Chat.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/FSM_Group_Chat.cs index 59c0aa9ca88b..093d0c77ce64 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/FSM_Group_Chat.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/FSM_Group_Chat.cs @@ -6,7 +6,8 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; +using OpenAI; +using OpenAI.Chat; #endregion Using namespace AutoGen.BasicSample; @@ -74,7 +75,7 @@ public async Task<string> SaveProgress( public class FSM_Group_Chat { - public static async Task<IAgent> CreateSaveProgressAgent(OpenAIClient client, string model) + public static async Task<IAgent> CreateSaveProgressAgent(ChatClient client) { #region Create_Save_Progress_Agent var tool = new FillFormTool(); @@ -86,9 +87,8 @@ public static async Task<IAgent> CreateSaveProgressAgent(OpenAIClient client, st }); var chatAgent = new OpenAIChatAgent( - openAIClient: client, + chatClient: client, name: "application", - modelName: model, systemMessage: """You are a helpful application form assistant who saves progress while user fills application.""") .RegisterMessageConnector() .RegisterMiddleware(functionCallMiddleware) @@ -111,42 +111,25 @@ Save progress according to the most recent information provided by user. return chatAgent; } - public static async Task<IAgent> CreateAssistantAgent(OpenAIClient openaiClient, string model) + public static async Task<IAgent> CreateAssistantAgent(ChatClient chatClient) { #region Create_Assistant_Agent var chatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: chatClient, name: "assistant", - modelName: model, systemMessage: """You create polite prompt to ask user provide missing information""") .RegisterMessageConnector() - .RegisterPrintMessage() - .RegisterMiddleware(async (msgs, option, agent, ct) => - { - var lastReply = msgs.Last() ?? throw new Exception("No reply found."); - var reply = await agent.GenerateReplyAsync(msgs, option, ct); - - // if application is complete, exit conversation by sending termination message - if (lastReply.GetContent()?.Contains("Application information is saved to database.") is true) - { - return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, from: agent.Name); - } - else - { - return reply; - } - }); + .RegisterPrintMessage(); #endregion Create_Assistant_Agent return chatAgent; } - public static async Task<IAgent> CreateUserAgent(OpenAIClient openaiClient, string model) + public static async Task<IAgent> CreateUserAgent(ChatClient chatClient) { #region Create_User_Agent var chatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: chatClient, name: "user", - modelName: model, systemMessage: """ You are a user who is filling an application form. Simply provide the information as requested and answer the questions, don't do anything else. @@ -166,11 +149,12 @@ public static async Task<IAgent> CreateUserAgent(OpenAIClient openaiClient, stri public static async Task RunAsync() { var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var model = "gpt-3.5-turbo"; + var model = "gpt-4o-mini"; var openaiClient = new OpenAIClient(apiKey); - var applicationAgent = await CreateSaveProgressAgent(openaiClient, model); - var assistantAgent = await CreateAssistantAgent(openaiClient, model); - var userAgent = await CreateUserAgent(openaiClient, model); + var chatClient = openaiClient.GetChatClient(model); + var applicationAgent = await CreateSaveProgressAgent(chatClient); + var assistantAgent = await CreateAssistantAgent(chatClient); + var userAgent = await CreateUserAgent(chatClient); #region Create_Graph var userToApplicationTransition = Transition.Create(userAgent, applicationAgent); @@ -193,9 +177,13 @@ public static async Task RunAsync() var initialMessage = await assistantAgent.SendAsync("Generate a greeting meesage for user and start the conversation by asking what's their name."); - var chatHistory = await userAgent.SendMessageToGroupAsync(groupChat, [initialMessage], maxRound: 30); - - var lastMessage = chatHistory.Last(); - Console.WriteLine(lastMessage.GetContent()); + var chatHistory = new List<IMessage> { initialMessage }; + await foreach (var msg in groupChat.SendAsync(chatHistory, maxRound: 30)) + { + if (msg.GetContent().ToLower().Contains("application information is saved to database.") is true) + { + break; + } + } } } diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Image_Chat_With_Agent.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Image_Chat_With_Agent.cs index 5b94a238bbe8..e993b3d51f1c 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Image_Chat_With_Agent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Image_Chat_With_Agent.cs @@ -5,7 +5,6 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #endregion Using using FluentAssertions; @@ -16,14 +15,10 @@ public class Image_Chat_With_Agent public static async Task RunAsync() { #region Create_Agent - var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var model = "gpt-4o"; // The model needs to support multimodal inputs - var openaiClient = new OpenAIClient(apiKey); - + var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini(); var agent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: gpt4o, name: "agent", - modelName: model, systemMessage: "You are a helpful AI assistant") .RegisterMessageConnector() // convert OpenAI message to AutoGen message .RegisterPrintMessage(); diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Streaming_Tool_Call.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Streaming_Tool_Call.cs new file mode 100644 index 000000000000..d5cb196f94f7 --- /dev/null +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Streaming_Tool_Call.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Streaming_Tool_Call.cs + +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using FluentAssertions; +using OpenAI; + +namespace AutoGen.BasicSample.GettingStart; + +internal class Streaming_Tool_Call +{ + public static async Task RunAsync() + { + #region Create_tools + var tools = new Tools(); + #endregion Create_tools + + #region Create_auto_invoke_middleware + var autoInvokeMiddleware = new FunctionCallMiddleware( + functions: [tools.GetWeatherFunctionContract], + functionMap: new Dictionary<string, Func<string, Task<string>>>() + { + { tools.GetWeatherFunctionContract.Name, tools.GetWeatherWrapper }, + }); + #endregion Create_auto_invoke_middleware + + #region Create_Agent + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var model = "gpt-4o-mini"; + var openaiClient = new OpenAIClient(apiKey); + var agent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(model), + name: "agent", + systemMessage: "You are a helpful AI assistant") + .RegisterMessageConnector() + .RegisterStreamingMiddleware(autoInvokeMiddleware) + .RegisterPrintMessage(); + #endregion Create_Agent + + IMessage finalReply = null; + var question = new TextMessage(Role.User, "What's the weather in Seattle"); + + // In streaming function call + // function can only be invoked untill all the chunks are collected + // therefore, only one ToolCallAggregateMessage chunk will be return here. + await foreach (var message in agent.GenerateStreamingReplyAsync([question])) + { + finalReply = message; + } + + finalReply?.GetContent().Should().Be("The weather in Seattle is sunny."); + } +} diff --git a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Use_Tools_With_Agent.cs b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Use_Tools_With_Agent.cs index b441fe389da2..21a5df4c2ecd 100644 --- a/dotnet/sample/AutoGen.BasicSamples/GettingStart/Use_Tools_With_Agent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/GettingStart/Use_Tools_With_Agent.cs @@ -5,9 +5,9 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; #endregion Using using FluentAssertions; +using OpenAI; namespace AutoGen.BasicSample; @@ -50,12 +50,11 @@ public static async Task RunAsync() #region Create_Agent var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var model = "gpt-3.5-turbo"; + var model = "gpt-4o-mini"; var openaiClient = new OpenAIClient(apiKey); var agent = new OpenAIChatAgent( - openAIClient: openaiClient, + chatClient: openaiClient.GetChatClient(model), name: "agent", - modelName: model, systemMessage: "You are a helpful AI assistant") .RegisterMessageConnector(); // convert OpenAI message to AutoGen message #endregion Create_Agent diff --git a/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs b/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs index e492569cdc3d..26d9668792ef 100644 --- a/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs +++ b/dotnet/sample/AutoGen.BasicSamples/LLMConfiguration.cs @@ -1,25 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // LLMConfiguration.cs -using AutoGen.OpenAI; +using OpenAI; +using OpenAI.Chat; namespace AutoGen.BasicSample; internal static class LLMConfiguration { - public static OpenAIConfig GetOpenAIGPT3_5_Turbo() + public static ChatClient GetOpenAIGPT4o_mini() { var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var modelId = "gpt-3.5-turbo"; - return new OpenAIConfig(openAIKey, modelId); - } - - public static OpenAIConfig GetOpenAIGPT4() - { - var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var modelId = "gpt-4"; + var modelId = "gpt-4o-mini"; - return new OpenAIConfig(openAIKey, modelId); + return new OpenAIClient(openAIKey).GetChatClient(modelId); } public static AzureOpenAIConfig GetAzureOpenAIGPT3_5_Turbo(string? deployName = null) @@ -29,12 +23,4 @@ public static AzureOpenAIConfig GetAzureOpenAIGPT3_5_Turbo(string? deployName = deployName = deployName ?? Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); return new AzureOpenAIConfig(endpoint, deployName, azureOpenAIKey); } - - public static AzureOpenAIConfig GetAzureOpenAIGPT4(string deployName = "gpt-4") - { - var azureOpenAIKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - - return new AzureOpenAIConfig(endpoint, deployName, azureOpenAIKey); - } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Program.cs b/dotnet/sample/AutoGen.BasicSamples/Program.cs index b48e2be4aa16..8817a3df36e1 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Program.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Program.cs @@ -1,6 +1,59 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Program.cs +//await Example07_Dynamic_GroupChat_Calculate_Fibonacci.RunAsync(); + using AutoGen.BasicSample; -Console.ReadLine(); -await Example17_ReActAgent.RunAsync(); + +//Define allSamples collection for all examples +List<Tuple<string, Func<Task>>> allSamples = new List<Tuple<string, Func<Task>>>(); + +// When a new sample is created please add them to the allSamples collection +allSamples.Add(new Tuple<string, Func<Task>>("Assistant Agent", async () => { await Example01_AssistantAgent.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Two-agent Math Chat", async () => { await Example02_TwoAgent_MathChat.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Agent Function Call", async () => { await Example03_Agent_FunctionCall.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Dynamic Group Chat Coding Task", async () => { await Example04_Dynamic_GroupChat_Coding_Task.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("DALL-E and GPT4v", async () => { await Example05_Dalle_And_GPT4V.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("User Proxy Agent", async () => { await Example06_UserProxyAgent.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Dynamic Group Chat - Calculate Fibonacci", async () => { await Example07_Dynamic_GroupChat_Calculate_Fibonacci.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("LM Studio", async () => { await Example08_LMStudio.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Semantic Kernel", async () => { await Example10_SemanticKernel.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Sequential Group Chat", async () => { await Sequential_GroupChat_Example.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Two Agent - Fill Application", async () => { await TwoAgent_Fill_Application.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("Mistal Client Agent - Token Count", async () => { await Example14_MistralClientAgent_TokenCount.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("GPT4v - Binary Data Image", async () => { await Example15_GPT4V_BinaryDataImageMessage.RunAsync(); })); +allSamples.Add(new Tuple<string, Func<Task>>("ReAct Agent", async () => { await Example17_ReActAgent.RunAsync(); })); + + +int idx = 1; +Dictionary<int, Tuple<string, Func<Task>>> map = new Dictionary<int, Tuple<string, Func<Task>>>(); +Console.WriteLine("Available Examples:\n\n"); +foreach (Tuple<string, Func<Task>> sample in allSamples) +{ + map.Add(idx, sample); + Console.WriteLine("{0}. {1}", idx++, sample.Item1); +} + +Console.WriteLine("\n\nEnter your selection:"); + +while (true) +{ + var input = Console.ReadLine(); + if (input == "exit") + { + break; + } + int val = Convert.ToInt32(input); + if (!map.ContainsKey(val)) + { + Console.WriteLine("Invalid choice"); + } + else + { + Console.WriteLine("\nRunning {0}", map[val].Item1); + await map[val].Item2.Invoke(); + } +} + + + diff --git a/dotnet/sample/AutoGen.Gemini.Sample/AutoGen.Gemini.Sample.csproj b/dotnet/sample/AutoGen.Gemini.Sample/AutoGen.Gemini.Sample.csproj index b1779b56c390..d1df8a8ed161 100644 --- a/dotnet/sample/AutoGen.Gemini.Sample/AutoGen.Gemini.Sample.csproj +++ b/dotnet/sample/AutoGen.Gemini.Sample/AutoGen.Gemini.Sample.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IncludeResourceFolder>true</IncludeResourceFolder> diff --git a/dotnet/sample/AutoGen.Ollama.Sample/AutoGen.Ollama.Sample.csproj b/dotnet/sample/AutoGen.Ollama.Sample/AutoGen.Ollama.Sample.csproj index 5277408d595d..62c9d61633c9 100644 --- a/dotnet/sample/AutoGen.Ollama.Sample/AutoGen.Ollama.Sample.csproj +++ b/dotnet/sample/AutoGen.Ollama.Sample/AutoGen.Ollama.Sample.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>$(TestTargetFramework)</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <GenerateDocumentationFile>True</GenerateDocumentationFile> <NoWarn>$(NoWarn);CS8981;CS8600;CS8602;CS8604;CS8618;CS0219;SKEXP0054;SKEXP0050;SKEXP0110</NoWarn> diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/AutoGen.OpenAI.Sample.csproj b/dotnet/sample/AutoGen.OpenAI.Sample/AutoGen.OpenAI.Sample.csproj index ffe18f8a616a..fcbbb834fc63 100644 --- a/dotnet/sample/AutoGen.OpenAI.Sample/AutoGen.OpenAI.Sample.csproj +++ b/dotnet/sample/AutoGen.OpenAI.Sample/AutoGen.OpenAI.Sample.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateDocumentationFile>True</GenerateDocumentationFile> @@ -14,8 +14,9 @@ <ProjectReference Include="..\..\src\AutoGen.DotnetInteractive\AutoGen.DotnetInteractive.csproj" /> <ProjectReference Include="..\..\src\AutoGen.Ollama\AutoGen.Ollama.csproj" /> <ProjectReference Include="..\..\src\AutoGen.SourceGenerator\AutoGen.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> - <ProjectReference Include="..\..\src\AutoGen\AutoGen.csproj" /> + <ProjectReference Include="..\..\src\AutoGen.OpenAI\AutoGen.OpenAI.csproj" /> <PackageReference Include="FluentAssertions" Version="$(FluentAssertionVersion)" /> + <PackageReference Include="Azure.AI.OpenAI" Version="$(AzureOpenAIV2Version)" /> </ItemGroup> </Project> diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Azure_OpenAI.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Azure_OpenAI.cs new file mode 100644 index 000000000000..dafe2e314859 --- /dev/null +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Azure_OpenAI.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Connect_To_Azure_OpenAI.cs + +#region using_statement +using AutoGen.Core; +using AutoGen.OpenAI.Extension; +using Azure; +using Azure.AI.OpenAI; +#endregion using_statement + +namespace AutoGen.OpenAI.Sample; + +public class Connect_To_Azure_OpenAI +{ + public static async Task RunAsync() + { + #region create_agent + var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new InvalidOperationException("Please set environment variable AZURE_OPENAI_API_KEY"); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("Please set environment variable AZURE_OPENAI_ENDPOINT"); + var model = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? "gpt-4o-mini"; + + // Use AzureOpenAIClient to connect to openai model deployed on azure. + // The AzureOpenAIClient comes from Azure.AI.OpenAI package + var openAIClient = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey)); + + var agent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient(model), + name: "assistant", + systemMessage: "You are a helpful assistant designed to output JSON.", + seed: 0) + .RegisterMessageConnector() + .RegisterPrintMessage(); + #endregion create_agent + + #region send_message + await agent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); + #endregion send_message + } +} diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs index b4206b4b6c22..2bb10e978412 100644 --- a/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs @@ -1,53 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs +// Connect_To_Ollama.cs + #region using_statement using AutoGen.Core; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; -using Azure.Core.Pipeline; +using OpenAI; #endregion using_statement namespace AutoGen.OpenAI.Sample; -#region CustomHttpClientHandler -public sealed class CustomHttpClientHandler : HttpClientHandler -{ - private string _modelServiceUrl; - - public CustomHttpClientHandler(string modelServiceUrl) - { - _modelServiceUrl = modelServiceUrl; - } - - protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - request.RequestUri = new Uri($"{_modelServiceUrl}{request.RequestUri.PathAndQuery}"); - - return base.SendAsync(request, cancellationToken); - } -} -#endregion CustomHttpClientHandler - public class Connect_To_Ollama { public static async Task RunAsync() { #region create_agent - using var client = new HttpClient(new CustomHttpClientHandler("http://localhost:11434")); - var option = new OpenAIClientOptions(OpenAIClientOptions.ServiceVersion.V2024_04_01_Preview) - { - Transport = new HttpClientTransport(client), - }; - // api-key is not required for local server // so you can use any string here - var openAIClient = new OpenAIClient("api-key", option); + var openAIClient = new OpenAIClient("api-key", new OpenAIClientOptions + { + Endpoint = new Uri("http://localhost:11434/v1/"), // remember to add /v1/ at the end to connect to Ollama openai server + }); var model = "llama3"; var agent = new OpenAIChatAgent( - openAIClient: openAIClient, + chatClient: openAIClient.GetChatClient(model), name: "assistant", - modelName: model, systemMessage: "You are a helpful assistant designed to output JSON.", seed: 0) .RegisterMessageConnector() diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_OpenAI_o1_preview.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_OpenAI_o1_preview.cs new file mode 100644 index 000000000000..52bc6381b9d5 --- /dev/null +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_OpenAI_o1_preview.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Connect_To_OpenAI_o1_preview.cs + +using AutoGen.Core; +using OpenAI; + +namespace AutoGen.OpenAI.Sample; + +public class Connect_To_OpenAI_o1_preview +{ + public static async Task RunAsync() + { + #region create_agent + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("Please set environment variable OPENAI_API_KEY"); + var openAIClient = new OpenAIClient(apiKey); + + // until 2024/09/12 + // openai o1-preview doesn't support systemMessage, temperature, maxTokens, streaming output + // so in order to use OpenAIChatAgent with o1-preview, you need to set those parameters to null + var agent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient("o1-preview"), + name: "assistant", + systemMessage: null, + temperature: null, + maxTokens: null, + seed: 0) + // by using RegisterMiddleware instead of RegisterStreamingMiddleware + // it turns an IStreamingAgent into an IAgent and disables streaming + .RegisterMiddleware(new OpenAIChatRequestMessageConnector()) + .RegisterPrintMessage(); + #endregion create_agent + + #region send_message + await agent.SendAsync("Can you write a piece of C# code to calculate 100th of fibonacci?"); + #endregion send_message + } +} diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Program.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Program.cs index 5a38a3ff03b9..c71f152d0370 100644 --- a/dotnet/sample/AutoGen.OpenAI.Sample/Program.cs +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Program.cs @@ -3,4 +3,4 @@ using AutoGen.OpenAI.Sample; -Tool_Call_With_Ollama_And_LiteLLM.RunAsync().Wait(); +Structural_Output.RunAsync().Wait(); diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Structural_Output.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Structural_Output.cs new file mode 100644 index 000000000000..e83be0082bab --- /dev/null +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Structural_Output.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Structural_Output.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; +using AutoGen.OpenAI.Extension; +using FluentAssertions; +using Json.Schema; +using Json.Schema.Generation; +using OpenAI; + +namespace AutoGen.OpenAI.Sample; + +public class Structural_Output +{ + public static async Task RunAsync() + { + #region create_agent + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var model = "gpt-4o-mini"; + + var schemaBuilder = new JsonSchemaBuilder().FromType<Person>(); + var schema = schemaBuilder.Build(); + var openAIClient = new OpenAIClient(apiKey); + var openAIClientAgent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient(model), + name: "assistant", + systemMessage: "You are a helpful assistant") + .RegisterMessageConnector() + .RegisterPrintMessage(); + #endregion create_agent + + #region chat_with_agent + var prompt = new TextMessage(Role.User, """ + My name is John, I am 25 years old, and I live in Seattle. I like to play soccer and read books. + """); + var reply = await openAIClientAgent.GenerateReplyAsync( + messages: [prompt], + options: new GenerateReplyOptions + { + OutputSchema = schema, + }); + + var person = JsonSerializer.Deserialize<Person>(reply.GetContent()); + Console.WriteLine($"Name: {person.Name}"); + Console.WriteLine($"Age: {person.Age}"); + + if (!string.IsNullOrEmpty(person.Address)) + { + Console.WriteLine($"Address: {person.Address}"); + } + + Console.WriteLine("Done."); + #endregion chat_with_agent + + person.Name.Should().Be("John"); + person.Age.Should().Be(25); + person.Address.Should().BeNullOrEmpty(); + person.City.Should().Be("Seattle"); + person.Hobbies.Count.Should().Be(2); + } + + + #region person_class + [Title("Person")] + public class Person + { + [JsonPropertyName("name")] + [Description("Name of the person")] + [Required] + public string Name { get; set; } + + [JsonPropertyName("age")] + [Description("Age of the person")] + [Required] + public int Age { get; set; } + + [JsonPropertyName("city")] + [Description("City of the person")] + public string? City { get; set; } + + [JsonPropertyName("address")] + [Description("Address of the person")] + public string? Address { get; set; } + + [JsonPropertyName("hobbies")] + [Description("Hobbies of the person")] + public List<string>? Hobbies { get; set; } + } + #endregion person_class + +} diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs index f4fabe3c9e83..ed43c628a672 100644 --- a/dotnet/sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs @@ -3,11 +3,11 @@ using AutoGen.Core; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; -using Azure.Core.Pipeline; +using OpenAI; namespace AutoGen.OpenAI.Sample; +#region Function public partial class Function { [Function] @@ -16,18 +16,22 @@ public async Task<string> GetWeatherAsync(string city) return await Task.FromResult("The weather in " + city + " is 72 degrees and sunny."); } } +#endregion Function + public class Tool_Call_With_Ollama_And_LiteLLM { public static async Task RunAsync() { - #region Create_Agent - var liteLLMUrl = "http://localhost:4000"; - using var httpClient = new HttpClient(new CustomHttpClientHandler(liteLLMUrl)); - var option = new OpenAIClientOptions(OpenAIClientOptions.ServiceVersion.V2024_04_01_Preview) - { - Transport = new HttpClientTransport(httpClient), - }; + // Before running this code, make sure you have + // - Ollama: + // - Install dolphincoder:latest in Ollama + // - Ollama running on http://localhost:11434 + // - LiteLLM + // - Install LiteLLM + // - Start LiteLLM with the following command: + // - litellm --model ollama_chat/dolphincoder --port 4000 + # region Create_tools var functions = new Function(); var functionMiddleware = new FunctionCallMiddleware( functions: [functions.GetWeatherAsyncFunctionContract], @@ -35,15 +39,20 @@ public static async Task RunAsync() { { functions.GetWeatherAsyncFunctionContract.Name!, functions.GetWeatherAsyncWrapper }, }); + #endregion Create_tools + #region Create_Agent + var liteLLMUrl = "http://localhost:4000"; // api-key is not required for local server // so you can use any string here - var openAIClient = new OpenAIClient("api-key", option); + var openAIClient = new OpenAIClient("api-key", new OpenAIClientOptions + { + Endpoint = new Uri("http://localhost:4000"), + }); var agent = new OpenAIChatAgent( - openAIClient: openAIClient, + chatClient: openAIClient.GetChatClient("dolphincoder:latest"), name: "assistant", - modelName: "placeholder", systemMessage: "You are a helpful AI assistant") .RegisterMessageConnector() .RegisterMiddleware(functionMiddleware) diff --git a/dotnet/sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs b/dotnet/sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs new file mode 100644 index 000000000000..4e5247d93cec --- /dev/null +++ b/dotnet/sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Use_Json_Mode.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; +using AutoGen.OpenAI.Extension; +using FluentAssertions; +using OpenAI; +using OpenAI.Chat; + +namespace AutoGen.OpenAI.Sample; + +public class Use_Json_Mode +{ + public static async Task RunAsync() + { + #region create_agent + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); + var model = "gpt-4o-mini"; + + var openAIClient = new OpenAIClient(apiKey); + var openAIClientAgent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient(model), + name: "assistant", + systemMessage: "You are a helpful assistant designed to output JSON.", + seed: 0, // explicitly set a seed to enable deterministic output + responseFormat: ChatResponseFormat.JsonObject) // set response format to JSON object to enable JSON mode + .RegisterMessageConnector() + .RegisterPrintMessage(); + #endregion create_agent + + #region chat_with_agent + var reply = await openAIClientAgent.SendAsync("My name is John, I am 25 years old, and I live in Seattle."); + + var person = JsonSerializer.Deserialize<Person>(reply.GetContent()); + Console.WriteLine($"Name: {person.Name}"); + Console.WriteLine($"Age: {person.Age}"); + + if (!string.IsNullOrEmpty(person.Address)) + { + Console.WriteLine($"Address: {person.Address}"); + } + + Console.WriteLine("Done."); + #endregion chat_with_agent + + person.Name.Should().Be("John"); + person.Age.Should().Be(25); + person.Address.Should().BeNullOrEmpty(); + } + + + #region person_class + public class Person + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int Age { get; set; } + + [JsonPropertyName("address")] + public string Address { get; set; } + } + #endregion person_class +} + diff --git a/dotnet/sample/AutoGen.SemanticKernel.Sample/AutoGen.SemanticKernel.Sample.csproj b/dotnet/sample/AutoGen.SemanticKernel.Sample/AutoGen.SemanticKernel.Sample.csproj index 6c2266512929..45514431368f 100644 --- a/dotnet/sample/AutoGen.SemanticKernel.Sample/AutoGen.SemanticKernel.Sample.csproj +++ b/dotnet/sample/AutoGen.SemanticKernel.Sample/AutoGen.SemanticKernel.Sample.csproj @@ -2,15 +2,16 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>$(TestTargetFramework)</TargetFramework> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <GenerateDocumentationFile>True</GenerateDocumentationFile> <NoWarn>$(NoWarn);CS8981;CS8600;CS8602;CS8604;CS8618;CS0219;SKEXP0054;SKEXP0050;SKEXP0110</NoWarn> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\src\AutoGen.OpenAI\AutoGen.OpenAI.csproj" /> + <ProjectReference Include="..\..\src\AutoGen.SemanticKernel\AutoGen.SemanticKernel.csproj" /> <ProjectReference Include="..\..\src\AutoGen.SourceGenerator\AutoGen.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> - <ProjectReference Include="..\AutoGen.BasicSamples\AutoGen.BasicSample.csproj" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="$(SemanticKernelExperimentalVersion)" /> </ItemGroup> diff --git a/dotnet/sample/AutoGen.SemanticKernel.Sample/Use_Kernel_Functions_With_Other_Agent.cs b/dotnet/sample/AutoGen.SemanticKernel.Sample/Use_Kernel_Functions_With_Other_Agent.cs index 2beb1ee7df0a..700bdfe75c7b 100644 --- a/dotnet/sample/AutoGen.SemanticKernel.Sample/Use_Kernel_Functions_With_Other_Agent.cs +++ b/dotnet/sample/AutoGen.SemanticKernel.Sample/Use_Kernel_Functions_With_Other_Agent.cs @@ -5,8 +5,8 @@ using AutoGen.Core; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; +using OpenAI; #endregion Using namespace AutoGen.SemanticKernel.Sample; @@ -17,7 +17,7 @@ public static async Task RunAsync() { #region Create_plugin var openAIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new Exception("Please set OPENAI_API_KEY environment variable."); - var modelId = "gpt-3.5-turbo"; + var modelId = "gpt-4o-mini"; var kernelBuilder = Kernel.CreateBuilder(); var kernel = kernelBuilder.Build(); var getWeatherFunction = KernelFunctionFactory.CreateFromMethod( @@ -33,9 +33,8 @@ public static async Task RunAsync() var openAIClient = new OpenAIClient(openAIKey); var openAIAgent = new OpenAIChatAgent( - openAIClient: openAIClient, - name: "assistant", - modelName: modelId) + chatClient: openAIClient.GetChatClient(modelId), + name: "assistant") .RegisterMessageConnector() // register message connector so it support AutoGen built-in message types like TextMessage. .RegisterMiddleware(kernelPluginMiddleware) // register the middleware to handle the plugin functions .RegisterPrintMessage(); // pretty print the message to the console diff --git a/dotnet/sample/AutoGen.WebAPI.Sample/AutoGen.WebAPI.Sample.csproj b/dotnet/sample/AutoGen.WebAPI.Sample/AutoGen.WebAPI.Sample.csproj new file mode 100644 index 000000000000..76675ba12346 --- /dev/null +++ b/dotnet/sample/AutoGen.WebAPI.Sample/AutoGen.WebAPI.Sample.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\src\AutoGen.WebAPI\AutoGen.WebAPI.csproj" /> + </ItemGroup> + +</Project> diff --git a/dotnet/sample/AutoGen.WebAPI.Sample/Program.cs b/dotnet/sample/AutoGen.WebAPI.Sample/Program.cs new file mode 100644 index 000000000000..dbeb8494363d --- /dev/null +++ b/dotnet/sample/AutoGen.WebAPI.Sample/Program.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Program.cs + +using System.Runtime.CompilerServices; +using AutoGen.Core; +using AutoGen.WebAPI; + +var alice = new DummyAgent("alice"); +var bob = new DummyAgent("bob"); + +var builder = WebApplication.CreateBuilder(args); +// Add services to the container. + +// run endpoint at port 5000 +builder.WebHost.UseUrls("http://localhost:5000"); +var app = builder.Build(); + +app.UseAgentAsOpenAIChatCompletionEndpoint(alice); +app.UseAgentAsOpenAIChatCompletionEndpoint(bob); + +app.Run(); + +public class DummyAgent : IStreamingAgent +{ + public DummyAgent(string name = "dummy") + { + Name = name; + } + + public string Name { get; } + + public async Task<IMessage> GenerateReplyAsync(IEnumerable<IMessage> messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + return new TextMessage(Role.Assistant, $"I am dummy {this.Name}", this.Name); + } + + public async IAsyncEnumerable<IMessage> GenerateStreamingReplyAsync(IEnumerable<IMessage> messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var reply = $"I am dummy {this.Name}"; + foreach (var c in reply) + { + yield return new TextMessageUpdate(Role.Assistant, c.ToString(), this.Name); + }; + } +} diff --git a/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs index e395bb4a225f..81fa8e6438a8 100644 --- a/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs +++ b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs @@ -1,5 +1,9 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicClientAgent.cs + +using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -16,6 +20,8 @@ public class AnthropicClientAgent : IStreamingAgent private readonly string _systemMessage; private readonly decimal _temperature; private readonly int _maxTokens; + private readonly Tool[]? _tools; + private readonly ToolChoice? _toolChoice; public AnthropicClientAgent( AnthropicClient anthropicClient, @@ -23,7 +29,9 @@ public AnthropicClientAgent( string modelName, string systemMessage = "You are a helpful AI assistant", decimal temperature = 0.7m, - int maxTokens = 1024) + int maxTokens = 1024, + Tool[]? tools = null, + ToolChoice? toolChoice = null) { Name = name; _anthropicClient = anthropicClient; @@ -31,6 +39,8 @@ public AnthropicClientAgent( _systemMessage = systemMessage; _temperature = temperature; _maxTokens = maxTokens; + _tools = tools; + _toolChoice = toolChoice; } public async Task<IMessage> GenerateReplyAsync(IEnumerable<IMessage> messages, GenerateReplyOptions? options = null, @@ -40,7 +50,7 @@ public async Task<IMessage> GenerateReplyAsync(IEnumerable<IMessage> messages, G return new MessageEnvelope<ChatCompletionResponse>(response, from: this.Name); } - public async IAsyncEnumerable<IStreamingMessage> GenerateStreamingReplyAsync(IEnumerable<IMessage> messages, + public async IAsyncEnumerable<IMessage> GenerateStreamingReplyAsync(IEnumerable<IMessage> messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var message in _anthropicClient.StreamingChatCompletionsAsync( @@ -54,11 +64,14 @@ private ChatCompletionRequest CreateParameters(IEnumerable<IMessage> messages, G { var chatCompletionRequest = new ChatCompletionRequest() { - SystemMessage = _systemMessage, + SystemMessage = [new SystemMessage { Text = _systemMessage }], MaxTokens = options?.MaxToken ?? _maxTokens, Model = _modelName, Stream = shouldStream, Temperature = (decimal?)options?.Temperature ?? _temperature, + Tools = _tools?.ToList(), + ToolChoice = _toolChoice ?? (_tools is { Length: > 0 } ? ToolChoice.Auto : null), + StopSequences = options?.StopSequence?.ToArray(), }; chatCompletionRequest.Messages = BuildMessages(messages); @@ -86,6 +99,22 @@ private List<ChatMessage> BuildMessages(IEnumerable<IMessage> messages) } } - return chatMessages; + // merge messages with the same role + // fixing #2884 + var mergedMessages = chatMessages.Aggregate(new List<ChatMessage>(), (acc, message) => + { + if (acc.Count > 0 && acc.Last().Role == message.Role) + { + acc.Last().Content.AddRange(message.Content); + } + else + { + acc.Add(message); + } + + return acc; + }); + + return mergedMessages; } } diff --git a/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs b/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs index 90bd33683f20..f106e08d35c4 100644 --- a/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs +++ b/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // AnthropicClient.cs using System; @@ -24,12 +24,13 @@ public sealed class AnthropicClient : IDisposable private static readonly JsonSerializerOptions JsonSerializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new ContentBaseConverter() } - }; - - private static readonly JsonSerializerOptions JsonDeserializerOptions = new() - { - Converters = { new ContentBaseConverter() } + Converters = + { + new ContentBaseConverter(), + new JsonPropertyNameEnumConverter<ToolChoiceType>(), + new JsonPropertyNameEnumConverter<CacheControlType>(), + new SystemMessageConverter(), + } }; public AnthropicClient(HttpClient httpClient, string baseUrl, string apiKey) @@ -48,7 +49,9 @@ public async Task<ChatCompletionResponse> CreateChatCompletionsAsync(ChatComplet var responseStream = await httpResponseMessage.Content.ReadAsStreamAsync(); if (httpResponseMessage.IsSuccessStatusCode) + { return await DeserializeResponseAsync<ChatCompletionResponse>(responseStream, cancellationToken); + } ErrorResponse res = await DeserializeResponseAsync<ErrorResponse>(responseStream, cancellationToken); throw new Exception(res.Error?.Message); @@ -61,24 +64,58 @@ public async IAsyncEnumerable<ChatCompletionResponse> StreamingChatCompletionsAs using var reader = new StreamReader(await httpResponseMessage.Content.ReadAsStreamAsync()); var currentEvent = new SseEvent(); + while (await reader.ReadLineAsync() is { } line) { if (!string.IsNullOrEmpty(line)) { - currentEvent.Data = line.Substring("data:".Length).Trim(); + if (line.StartsWith("event:")) + { + currentEvent.EventType = line.Substring("event:".Length).Trim(); + } + else if (line.StartsWith("data:")) + { + currentEvent.Data = line.Substring("data:".Length).Trim(); + } } - else + else // an empty line indicates the end of an event { - if (currentEvent.Data == "[DONE]") - continue; + if (currentEvent.EventType == "content_block_start" && !string.IsNullOrEmpty(currentEvent.Data)) + { + var dataBlock = JsonSerializer.Deserialize<DataBlock>(currentEvent.Data!); + if (dataBlock != null && dataBlock.ContentBlock?.Type == "tool_use") + { + currentEvent.ContentBlock = dataBlock.ContentBlock; + } + } - if (currentEvent.Data != null) + if (currentEvent.EventType is "message_start" or "content_block_delta" or "message_delta" && currentEvent.Data != null) { - yield return await JsonSerializer.DeserializeAsync<ChatCompletionResponse>( + var res = await JsonSerializer.DeserializeAsync<ChatCompletionResponse>( new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data)), cancellationToken: cancellationToken) ?? throw new Exception("Failed to deserialize response"); + if (res.Delta?.Type == "input_json_delta" && !string.IsNullOrEmpty(res.Delta.PartialJson) && + currentEvent.ContentBlock != null) + { + currentEvent.ContentBlock.AppendDeltaParameters(res.Delta.PartialJson!); + } + else if (res.Delta is { StopReason: "tool_use" } && currentEvent.ContentBlock != null) + { + if (res.Content == null) + { + res.Content = [currentEvent.ContentBlock.CreateToolUseContent()]; + } + else + { + res.Content.Add(currentEvent.ContentBlock.CreateToolUseContent()); + } + + currentEvent = new SseEvent(); + } + + yield return res; } - else if (currentEvent.Data != null) + else if (currentEvent.EventType == "error" && currentEvent.Data != null) { var res = await JsonSerializer.DeserializeAsync<ErrorResponse>( new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data)), cancellationToken: cancellationToken); @@ -86,8 +123,10 @@ public async IAsyncEnumerable<ChatCompletionResponse> StreamingChatCompletionsAs throw new Exception(res?.Error?.Message); } - // Reset the current event for the next one - currentEvent = new SseEvent(); + if (currentEvent.ContentBlock == null) + { + currentEvent = new SseEvent(); + } } } } @@ -97,12 +136,13 @@ private Task<HttpResponseMessage> SendRequestAsync<T>(T requestObject, Cancellat var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _baseUrl); var jsonRequest = JsonSerializer.Serialize(requestObject, JsonSerializerOptions); httpRequestMessage.Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); + httpRequestMessage.Headers.Add("anthropic-beta", "prompt-caching-2024-07-31"); return _httpClient.SendAsync(httpRequestMessage, cancellationToken); } private async Task<T> DeserializeResponseAsync<T>(Stream responseStream, CancellationToken cancellationToken) { - return await JsonSerializer.DeserializeAsync<T>(responseStream, JsonDeserializerOptions, cancellationToken) + return await JsonSerializer.DeserializeAsync<T>(responseStream, JsonSerializerOptions, cancellationToken) ?? throw new Exception("Failed to deserialize response"); } @@ -113,11 +153,50 @@ public void Dispose() private struct SseEvent { + public string EventType { get; set; } public string? Data { get; set; } + public ContentBlock? ContentBlock { get; set; } - public SseEvent(string? data = null) + public SseEvent(string eventType, string? data = null, ContentBlock? contentBlock = null) { + EventType = eventType; Data = data; + ContentBlock = contentBlock; } } + + private class ContentBlock + { + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("input")] + public object? Input { get; set; } + + public string? parameters { get; set; } + + public void AppendDeltaParameters(string deltaParams) + { + StringBuilder sb = new StringBuilder(parameters); + sb.Append(deltaParams); + parameters = sb.ToString(); + } + + public ToolUseContent CreateToolUseContent() + { + return new ToolUseContent { Id = Id, Name = Name, Input = parameters }; + } + } + + private class DataBlock + { + [JsonPropertyName("content_block")] + public ContentBlock? ContentBlock { get; set; } + } } diff --git a/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj index fefc439e00ba..a4fd32e7e345 100644 --- a/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj +++ b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj @@ -1,8 +1,8 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> - <RootNamespace>AutoGen.Anthropic</RootNamespace> + <TargetFrameworks>$(PackageTargetFrameworks)</TargetFrameworks> + <RootNamespace>AutoGen.Anthropic</RootNamespace> </PropertyGroup> <Import Project="$(RepoRoot)/nuget/nuget-package.props" /> diff --git a/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs b/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs index 4cb8fdbb34e0..3e620f934c28 100644 --- a/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs +++ b/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// ContentConverter.cs - -using AutoGen.Anthropic.DTO; - +// ContentBaseConverter.cs using System; using System.Text.Json; using System.Text.Json.Serialization; +using AutoGen.Anthropic.DTO; namespace AutoGen.Anthropic.Converters; public sealed class ContentBaseConverter : JsonConverter<ContentBase> @@ -24,6 +22,10 @@ public override ContentBase Read(ref Utf8JsonReader reader, Type typeToConvert, return JsonSerializer.Deserialize<TextContent>(text, options) ?? throw new InvalidOperationException(); case "image": return JsonSerializer.Deserialize<ImageContent>(text, options) ?? throw new InvalidOperationException(); + case "tool_use": + return JsonSerializer.Deserialize<ToolUseContent>(text, options) ?? throw new InvalidOperationException(); + case "tool_result": + return JsonSerializer.Deserialize<ToolResultContent>(text, options) ?? throw new InvalidOperationException(); } } diff --git a/dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs b/dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs new file mode 100644 index 000000000000..68b3c14bdee6 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// JsonPropertyNameEnumCoverter.cs + +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoGen.Anthropic.Converters; + +internal class JsonPropertyNameEnumConverter<T> : JsonConverter<T> where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string value = reader.GetString() ?? throw new JsonException("Value was null."); + + foreach (var field in typeToConvert.GetFields()) + { + var attribute = field.GetCustomAttribute<JsonPropertyNameAttribute>(); + if (attribute?.Name == value) + { + return (T)Enum.Parse(typeToConvert, field.Name); + } + } + + throw new JsonException($"Unable to convert \"{value}\" to enum {typeToConvert}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = field?.GetCustomAttribute<JsonPropertyNameAttribute>(); + + if (attribute != null) + { + writer.WriteStringValue(attribute.Name); + } + else + { + writer.WriteStringValue(value.ToString()); + } + } +} + diff --git a/dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs b/dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs new file mode 100644 index 000000000000..5bbe8a3a37f8 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SystemMessageConverter.cs + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Anthropic.DTO; + +namespace AutoGen.Anthropic.Converters; + +public class SystemMessageConverter : JsonConverter<object> +{ + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString() ?? string.Empty; + } + if (reader.TokenType == JsonTokenType.StartArray) + { + return JsonSerializer.Deserialize<SystemMessage[]>(ref reader, options) ?? throw new InvalidOperationException(); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + if (value is string stringValue) + { + writer.WriteStringValue(stringValue); + } + else if (value is SystemMessage[] arrayValue) + { + JsonSerializer.Serialize(writer, arrayValue, options); + } + else + { + throw new JsonException(); + } + } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs index 0c1749eaa989..dfb86ef0af53 100644 --- a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs +++ b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ChatCompletionRequest.cs -using System.Text.Json.Serialization; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace AutoGen.Anthropic.DTO; @@ -14,7 +14,7 @@ public class ChatCompletionRequest public List<ChatMessage> Messages { get; set; } [JsonPropertyName("system")] - public string? SystemMessage { get; set; } + public SystemMessage[]? SystemMessage { get; set; } [JsonPropertyName("max_tokens")] public int MaxTokens { get; set; } @@ -37,12 +37,38 @@ public class ChatCompletionRequest [JsonPropertyName("top_p")] public decimal? TopP { get; set; } + [JsonPropertyName("tools")] + public List<Tool>? Tools { get; set; } + + [JsonPropertyName("tool_choice")] + public ToolChoice? ToolChoice { get; set; } + public ChatCompletionRequest() { Messages = new List<ChatMessage>(); } } +public class SystemMessage +{ + [JsonPropertyName("text")] + public string? Text { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; private set; } = "text"; + + [JsonPropertyName("cache_control")] + public CacheControl? CacheControl { get; set; } + + public static SystemMessage CreateSystemMessage(string systemMessage) => new() { Text = systemMessage }; + + public static SystemMessage CreateSystemMessageWithCacheControl(string systemMessage) => new() + { + Text = systemMessage, + CacheControl = new CacheControl { Type = CacheControlType.Ephemeral } + }; +} + public class ChatMessage { [JsonPropertyName("role")] @@ -62,4 +88,6 @@ public ChatMessage(string role, List<ContentBase> content) Role = role; Content = content; } + + public void AddContent(ContentBase content) => Content.Add(content); } diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs index c6861f9c3150..a142f2feacca 100644 --- a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs +++ b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatCompletionResponse.cs -namespace AutoGen.Anthropic.DTO; using System.Collections.Generic; using System.Text.Json.Serialization; +namespace AutoGen.Anthropic.DTO; public class ChatCompletionResponse { [JsonPropertyName("content")] @@ -49,9 +50,6 @@ public class StreamingMessage [JsonPropertyName("role")] public string? Role { get; set; } - [JsonPropertyName("content")] - public List<object>? Content { get; set; } - [JsonPropertyName("model")] public string? Model { get; set; } @@ -72,6 +70,12 @@ public class Usage [JsonPropertyName("output_tokens")] public int OutputTokens { get; set; } + + [JsonPropertyName("cache_creation_input_tokens")] + public int CacheCreationInputTokens { get; set; } + + [JsonPropertyName("cache_read_input_tokens")] + public int CacheReadInputTokens { get; set; } } public class Delta @@ -85,6 +89,9 @@ public class Delta [JsonPropertyName("text")] public string? Text { get; set; } + [JsonPropertyName("partial_json")] + public string? PartialJson { get; set; } + [JsonPropertyName("usage")] public Usage? Usage { get; set; } } diff --git a/dotnet/src/AutoGen.Anthropic/DTO/Content.cs b/dotnet/src/AutoGen.Anthropic/DTO/Content.cs index dd2481bd58f3..ade913b827c4 100644 --- a/dotnet/src/AutoGen.Anthropic/DTO/Content.cs +++ b/dotnet/src/AutoGen.Anthropic/DTO/Content.cs @@ -1,7 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Content.cs +using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using AutoGen.Anthropic.Converters; namespace AutoGen.Anthropic.DTO; @@ -9,6 +11,9 @@ public abstract class ContentBase { [JsonPropertyName("type")] public abstract string Type { get; } + + [JsonPropertyName("cache_control")] + public CacheControl? CacheControl { get; set; } } public class TextContent : ContentBase @@ -18,6 +23,12 @@ public class TextContent : ContentBase [JsonPropertyName("text")] public string? Text { get; set; } + + public static TextContent CreateTextWithCacheControl(string text) => new() + { + Text = text, + CacheControl = new CacheControl { Type = CacheControlType.Ephemeral } + }; } public class ImageContent : ContentBase @@ -40,3 +51,45 @@ public class ImageSource [JsonPropertyName("data")] public string? Data { get; set; } } + +public class ToolUseContent : ContentBase +{ + [JsonPropertyName("type")] + public override string Type => "tool_use"; + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("input")] + public JsonNode? Input { get; set; } +} + +public class ToolResultContent : ContentBase +{ + [JsonPropertyName("type")] + public override string Type => "tool_result"; + + [JsonPropertyName("tool_use_id")] + public string? Id { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } +} + +public class CacheControl +{ + [JsonPropertyName("type")] + public CacheControlType Type { get; set; } + + public static CacheControl Create() => new CacheControl { Type = CacheControlType.Ephemeral }; +} + +[JsonConverter(typeof(JsonPropertyNameEnumConverter<CacheControlType>))] +public enum CacheControlType +{ + [JsonPropertyName("ephemeral")] + Ephemeral +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs b/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs index d02a8f6d1cfc..1a94334c88ff 100644 --- a/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs +++ b/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // ErrorResponse.cs using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs b/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs new file mode 100644 index 000000000000..3845c4445925 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Tool.cs + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AutoGen.Anthropic.DTO; + +public class Tool +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("input_schema")] + public InputSchema? InputSchema { get; set; } + + [JsonPropertyName("cache_control")] + public CacheControl? CacheControl { get; set; } +} + +public class InputSchema +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("properties")] + public Dictionary<string, SchemaProperty>? Properties { get; set; } + + [JsonPropertyName("required")] + public List<string>? Required { get; set; } +} + +public class SchemaProperty +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ToolChoice.cs b/dotnet/src/AutoGen.Anthropic/DTO/ToolChoice.cs new file mode 100644 index 000000000000..0a5c3790e1de --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/ToolChoice.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ToolChoice.cs + +using System.Text.Json.Serialization; +using AutoGen.Anthropic.Converters; + +namespace AutoGen.Anthropic.DTO; + +[JsonConverter(typeof(JsonPropertyNameEnumConverter<ToolChoiceType>))] +public enum ToolChoiceType +{ + [JsonPropertyName("auto")] + Auto, // Default behavior + + [JsonPropertyName("any")] + Any, // Use any provided tool + + [JsonPropertyName("tool")] + Tool // Force a specific tool +} + +public class ToolChoice +{ + [JsonPropertyName("type")] + public ToolChoiceType Type { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + private ToolChoice(ToolChoiceType type, string? name = null) + { + Type = type; + Name = name; + } + + public static ToolChoice Auto => new(ToolChoiceType.Auto); + public static ToolChoice Any => new(ToolChoiceType.Any); + public static ToolChoice ToolUse(string name) => new(ToolChoiceType.Tool, name); +} diff --git a/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs b/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs index bb2f5820f74c..af06a0547849 100644 --- a/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs +++ b/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using AutoGen.Anthropic.DTO; @@ -28,7 +29,7 @@ public async Task<IMessage> InvokeAsync(MiddlewareContext context, IAgent agent, : response; } - public async IAsyncEnumerable<IStreamingMessage> InvokeAsync(MiddlewareContext context, IStreamingAgent agent, + public async IAsyncEnumerable<IMessage> InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var messages = context.Messages; @@ -36,7 +37,7 @@ public async IAsyncEnumerable<IStreamingMessage> InvokeAsync(MiddlewareContext c await foreach (var reply in agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken)) { - if (reply is IStreamingMessage<ChatCompletionResponse> chatMessage) + if (reply is IMessage<ChatCompletionResponse> chatMessage) { var response = ProcessChatCompletionResponse(chatMessage, agent); if (response is not null) @@ -51,9 +52,20 @@ public async IAsyncEnumerable<IStreamingMessage> InvokeAsync(MiddlewareContext c } } - private IStreamingMessage? ProcessChatCompletionResponse(IStreamingMessage<ChatCompletionResponse> chatMessage, + private IMessage? ProcessChatCompletionResponse(IMessage<ChatCompletionResponse> chatMessage, IStreamingAgent agent) { + if (chatMessage.Content.Content is { Count: 1 } && + chatMessage.Content.Content[0] is ToolUseContent toolUseContent) + { + return new ToolCallMessage( + toolUseContent.Name ?? + throw new InvalidOperationException($"Expected {nameof(toolUseContent.Name)} to be specified"), + toolUseContent.Input?.ToString() ?? + throw new InvalidOperationException($"Expected {nameof(toolUseContent.Input)} to be specified"), + from: agent.Name); + } + var delta = chatMessage.Content.Delta; return delta != null && !string.IsNullOrEmpty(delta.Text) ? new TextMessageUpdate(role: Role.Assistant, delta.Text, from: agent.Name) @@ -71,16 +83,20 @@ private async Task<IEnumerable<IMessage>> ProcessMessageAsync(IEnumerable<IMessa TextMessage textMessage => ProcessTextMessage(textMessage, agent), ImageMessage imageMessage => - new MessageEnvelope<ChatMessage>(new ChatMessage("user", + (MessageEnvelope<ChatMessage>[])[new MessageEnvelope<ChatMessage>(new ChatMessage("user", new ContentBase[] { new ImageContent { Source = await ProcessImageSourceAsync(imageMessage) } } .ToList()), - from: agent.Name), + from: agent.Name)], MultiModalMessage multiModalMessage => await ProcessMultiModalMessageAsync(multiModalMessage, agent), - _ => message, + + ToolCallMessage toolCallMessage => ProcessToolCallMessage(toolCallMessage, agent), + ToolCallResultMessage toolCallResultMessage => ProcessToolCallResultMessage(toolCallResultMessage), + AggregateMessage<ToolCallMessage, ToolCallResultMessage> toolCallAggregateMessage => ProcessToolCallAggregateMessage(toolCallAggregateMessage, agent), + _ => [message], }; - processedMessages.Add(processedMessage); + processedMessages.AddRange(processedMessage); } return processedMessages; @@ -93,15 +109,42 @@ private IMessage PostProcessMessage(ChatCompletionResponse response, IAgent from throw new ArgumentNullException(nameof(response.Content)); } - if (response.Content.Count != 1) + // When expecting a tool call, sometimes the response will contain two messages, one chat and one tool. + // The first message is typically a TextContent, of the LLM explaining what it is trying to do. + // The second message contains the tool call. + if (response.Content.Count > 1) { - throw new NotSupportedException($"{nameof(response.Content)} != 1"); + if (response.Content.Count == 2 && response.Content[0] is TextContent && + response.Content[1] is ToolUseContent toolUseContent) + { + return new ToolCallMessage(toolUseContent.Name ?? string.Empty, + toolUseContent.Input?.ToJsonString() ?? string.Empty, + from: from.Name); + } + + throw new NotSupportedException($"Expected {nameof(response.Content)} to have one output"); } - return new TextMessage(Role.Assistant, ((TextContent)response.Content[0]).Text ?? string.Empty, from: from.Name); + var content = response.Content[0]; + switch (content) + { + case TextContent textContent: + return new TextMessage(Role.Assistant, textContent.Text ?? string.Empty, from: from.Name); + + case ToolUseContent toolUseContent: + return new ToolCallMessage(toolUseContent.Name ?? string.Empty, + toolUseContent.Input?.ToJsonString() ?? string.Empty, + from: from.Name); + + case ImageContent: + throw new InvalidOperationException( + "Claude is an image understanding model only. It can interpret and analyze images, but it cannot generate, produce, edit, manipulate or create images"); + default: + throw new ArgumentOutOfRangeException(nameof(content)); + } } - private IMessage<ChatMessage> ProcessTextMessage(TextMessage textMessage, IAgent agent) + private IEnumerable<IMessage<ChatMessage>> ProcessTextMessage(TextMessage textMessage, IAgent agent) { ChatMessage messages; @@ -139,10 +182,10 @@ private IMessage<ChatMessage> ProcessTextMessage(TextMessage textMessage, IAgent "user", textMessage.Content); } - return new MessageEnvelope<ChatMessage>(messages, from: textMessage.From); + return [new MessageEnvelope<ChatMessage>(messages, from: textMessage.From)]; } - private async Task<IMessage> ProcessMultiModalMessageAsync(MultiModalMessage multiModalMessage, IAgent agent) + private async Task<IEnumerable<IMessage>> ProcessMultiModalMessageAsync(MultiModalMessage multiModalMessage, IAgent agent) { var content = new List<ContentBase>(); foreach (var message in multiModalMessage.Content) @@ -158,8 +201,7 @@ private async Task<IMessage> ProcessMultiModalMessageAsync(MultiModalMessage mul } } - var chatMessage = new ChatMessage("user", content); - return MessageEnvelope.Create(chatMessage, agent.Name); + return [MessageEnvelope.Create(new ChatMessage("user", content), agent.Name)]; } private async Task<ImageSource> ProcessImageSourceAsync(ImageMessage imageMessage) @@ -192,4 +234,52 @@ private async Task<ImageSource> ProcessImageSourceAsync(ImageMessage imageMessag Data = Convert.ToBase64String(await response.Content.ReadAsByteArrayAsync()) }; } + + private IEnumerable<IMessage> ProcessToolCallMessage(ToolCallMessage toolCallMessage, IAgent agent) + { + var chatMessage = new ChatMessage("assistant", new List<ContentBase>()); + foreach (var toolCall in toolCallMessage.ToolCalls) + { + chatMessage.AddContent(new ToolUseContent + { + Id = toolCall.ToolCallId, + Name = toolCall.FunctionName, + Input = JsonNode.Parse(toolCall.FunctionArguments) + }); + } + + return [MessageEnvelope.Create(chatMessage, toolCallMessage.From)]; + } + + private IEnumerable<IMessage> ProcessToolCallResultMessage(ToolCallResultMessage toolCallResultMessage) + { + var chatMessage = new ChatMessage("user", new List<ContentBase>()); + foreach (var toolCall in toolCallResultMessage.ToolCalls) + { + chatMessage.AddContent(new ToolResultContent + { + Id = toolCall.ToolCallId ?? string.Empty, + Content = toolCall.Result, + }); + } + + return [MessageEnvelope.Create(chatMessage, toolCallResultMessage.From)]; + } + + private IEnumerable<IMessage> ProcessToolCallAggregateMessage(AggregateMessage<ToolCallMessage, ToolCallResultMessage> aggregateMessage, IAgent agent) + { + if (aggregateMessage.From is { } from && from != agent.Name) + { + var contents = aggregateMessage.Message2.ToolCalls.Select(t => t.Result); + var messages = contents.Select(c => + new ChatMessage("assistant", c ?? throw new ArgumentNullException(nameof(c)))); + + return messages.Select(m => new MessageEnvelope<ChatMessage>(m, from: from)); + } + + var toolCallMessage = ProcessToolCallMessage(aggregateMessage.Message1, agent); + var toolCallResult = ProcessToolCallResultMessage(aggregateMessage.Message2); + + return toolCallMessage.Concat(toolCallResult); + } } diff --git a/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs b/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs index e70572cbddf2..494a6686f521 100644 --- a/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs +++ b/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Constants.cs +// AnthropicConstants.cs namespace AutoGen.Anthropic.Utils; @@ -11,4 +11,5 @@ public static class AnthropicConstants public static string Claude3Opus = "claude-3-opus-20240229"; public static string Claude3Sonnet = "claude-3-sonnet-20240229"; public static string Claude3Haiku = "claude-3-haiku-20240307"; + public static string Claude35Sonnet = "claude-3-5-sonnet-20240620"; } diff --git a/dotnet/src/AutoGen.AzureAIInference/Agent/ChatCompletionsClientAgent.cs b/dotnet/src/AutoGen.AzureAIInference/Agent/ChatCompletionsClientAgent.cs new file mode 100644 index 000000000000..452c5b1c3079 --- /dev/null +++ b/dotnet/src/AutoGen.AzureAIInference/Agent/ChatCompletionsClientAgent.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatCompletionsClientAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.AzureAIInference.Extension; +using AutoGen.Core; +using Azure.AI.Inference; + +namespace AutoGen.AzureAIInference; + +/// <summary> +/// ChatCompletions client agent. This agent is a thin wrapper around <see cref="ChatCompletionsClient"/> to provide a simple interface for chat completions. +/// <para><see cref="ChatCompletionsClientAgent" /> supports the following message types:</para> +/// <list type="bullet"> +/// <item> +/// <see cref="MessageEnvelope{T}"/> where T is <see cref="ChatRequestMessage"/>: chat request message. +/// </item> +/// </list> +/// <para><see cref="ChatCompletionsClientAgent" /> returns the following message types:</para> +/// <list type="bullet"> +/// <item> +/// <see cref="MessageEnvelope{T}"/> where T is <see cref="ChatResponseMessage"/>: chat response message. +/// <see cref="MessageEnvelope{T}"/> where T is <see cref="StreamingChatCompletionsUpdate"/>: streaming chat completions update. +/// </item> +/// </list> +/// </summary> +public class ChatCompletionsClientAgent : IStreamingAgent +{ + private readonly ChatCompletionsClient chatCompletionsClient; + private readonly ChatCompletionsOptions options; + private readonly string systemMessage; + + /// <summary> + /// Create a new instance of <see cref="ChatCompletionsClientAgent"/>. + /// </summary> + /// <param name="chatCompletionsClient">chat completions client</param> + /// <param name="name">agent name</param> + /// <param name="modelName">model name. e.g. gpt-turbo-3.5</param> + /// <param name="systemMessage">system message</param> + /// <param name="temperature">temperature</param> + /// <param name="maxTokens">max tokens to generated</param> + /// <param name="responseFormat">response format, set it to <see cref="ChatCompletionsResponseFormatJSON"/> to enable json mode.</param> + /// <param name="seed">seed to use, set it to enable deterministic output</param> + /// <param name="functions">functions</param> + public ChatCompletionsClientAgent( + ChatCompletionsClient chatCompletionsClient, + string name, + string modelName, + string systemMessage = "You are a helpful AI assistant", + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable<FunctionDefinition>? functions = null) + : this( + chatCompletionsClient: chatCompletionsClient, + name: name, + options: CreateChatCompletionOptions(modelName, temperature, maxTokens, seed, responseFormat, functions), + systemMessage: systemMessage) + { + } + + /// <summary> + /// Create a new instance of <see cref="ChatCompletionsClientAgent"/>. + /// </summary> + /// <param name="chatCompletionsClient">chat completions client</param> + /// <param name="name">agent name</param> + /// <param name="systemMessage">system message</param> + /// <param name="options">chat completion option. The option can't contain messages</param> + public ChatCompletionsClientAgent( + ChatCompletionsClient chatCompletionsClient, + string name, + ChatCompletionsOptions options, + string systemMessage = "You are a helpful AI assistant") + { + if (options.Messages is { Count: > 0 }) + { + throw new ArgumentException("Messages should not be provided in options"); + } + + this.chatCompletionsClient = chatCompletionsClient; + this.Name = name; + this.options = options; + this.systemMessage = systemMessage; + } + + public string Name { get; } + + public async Task<IMessage> GenerateReplyAsync( + IEnumerable<IMessage> messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var reply = await this.chatCompletionsClient.CompleteAsync(settings, cancellationToken: cancellationToken); + + return new MessageEnvelope<ChatCompletions>(reply, from: this.Name); + } + + public async IAsyncEnumerable<IMessage> GenerateStreamingReplyAsync( + IEnumerable<IMessage> messages, + GenerateReplyOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var response = await this.chatCompletionsClient.CompleteStreamingAsync(settings, cancellationToken); + await foreach (var update in response.WithCancellation(cancellationToken)) + { + yield return new MessageEnvelope<StreamingChatCompletionsUpdate>(update, from: this.Name); + } + } + + private ChatCompletionsOptions CreateChatCompletionsOptions(GenerateReplyOptions? options, IEnumerable<IMessage> messages) + { + var oaiMessages = messages.Select(m => m switch + { + IMessage<ChatRequestMessage> chatRequestMessage => chatRequestMessage.Content, + _ => throw new ArgumentException("Invalid message type") + }); + + // add system message if there's no system message in messages + if (!oaiMessages.Any(m => m is ChatRequestSystemMessage)) + { + oaiMessages = new[] { new ChatRequestSystemMessage(systemMessage) }.Concat(oaiMessages); + } + + // clone the options by serializing and deserializing + var json = JsonSerializer.Serialize(this.options); + var settings = JsonSerializer.Deserialize<ChatCompletionsOptions>(json) ?? throw new InvalidOperationException("Failed to clone options"); + + foreach (var m in oaiMessages) + { + settings.Messages.Add(m); + } + + settings.Temperature = options?.Temperature ?? settings.Temperature; + settings.MaxTokens = options?.MaxToken ?? settings.MaxTokens; + + foreach (var functions in this.options.Tools) + { + settings.Tools.Add(functions); + } + + foreach (var stopSequence in this.options.StopSequences) + { + settings.StopSequences.Add(stopSequence); + } + + var openAIFunctionDefinitions = options?.Functions?.Select(f => f.ToAzureAIInferenceFunctionDefinition()).ToList(); + if (openAIFunctionDefinitions is { Count: > 0 }) + { + foreach (var f in openAIFunctionDefinitions) + { + settings.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + } + } + + if (options?.StopSequence is var sequence && sequence is { Length: > 0 }) + { + foreach (var seq in sequence) + { + settings.StopSequences.Add(seq); + } + } + + return settings; + } + + private static ChatCompletionsOptions CreateChatCompletionOptions( + string modelName, + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable<FunctionDefinition>? functions = null) + { + var options = new ChatCompletionsOptions() + { + Model = modelName, + Temperature = temperature, + MaxTokens = maxTokens, + Seed = seed, + ResponseFormat = responseFormat, + }; + + if (functions is not null) + { + foreach (var f in functions) + { + options.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + } + } + + return options; + } +} diff --git a/dotnet/src/AutoGen.AzureAIInference/AutoGen.AzureAIInference.csproj b/dotnet/src/AutoGen.AzureAIInference/AutoGen.AzureAIInference.csproj new file mode 100644 index 000000000000..e9401bc4bc22 --- /dev/null +++ b/dotnet/src/AutoGen.AzureAIInference/AutoGen.AzureAIInference.csproj @@ -0,0 +1,25 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFrameworks>$(PackageTargetFrameworks)</TargetFrameworks> + <RootNamespace>AutoGen.AzureAIInference</RootNamespace> + </PropertyGroup> + + <Import Project="$(RepoRoot)/nuget/nuget-package.props" /> + + <PropertyGroup> + <!-- NuGet Package Settings --> + <Title>AutoGen.AzureAIInference + + Azure AI Inference Intergration for AutoGen. + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.AzureAIInference/Extension/ChatComptionClientAgentExtension.cs b/dotnet/src/AutoGen.AzureAIInference/Extension/ChatComptionClientAgentExtension.cs new file mode 100644 index 000000000000..8faf29604ed1 --- /dev/null +++ b/dotnet/src/AutoGen.AzureAIInference/Extension/ChatComptionClientAgentExtension.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatComptionClientAgentExtension.cs + +using AutoGen.Core; + +namespace AutoGen.AzureAIInference.Extension; + +public static class ChatComptionClientAgentExtension +{ + ///

+ /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this ChatCompletionsClientAgent agent, AzureAIInferenceChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new AzureAIInferenceChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, AzureAIInferenceChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new AzureAIInferenceChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.AzureAIInference/Extension/FunctionContractExtension.cs b/dotnet/src/AutoGen.AzureAIInference/Extension/FunctionContractExtension.cs new file mode 100644 index 000000000000..4cd7b3864f95 --- /dev/null +++ b/dotnet/src/AutoGen.AzureAIInference/Extension/FunctionContractExtension.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionContractExtension.cs + +using System; +using System.Collections.Generic; +using AutoGen.Core; +using Azure.AI.Inference; +using Json.Schema; +using Json.Schema.Generation; + +namespace AutoGen.AzureAIInference.Extension; + +public static class FunctionContractExtension +{ + /// + /// Convert a to a that can be used in gpt funciton call. + /// + /// function contract + /// + public static FunctionDefinition ToAzureAIInferenceFunctionDefinition(this FunctionContract functionContract) + { + var functionDefinition = new FunctionDefinition + { + Name = functionContract.Name, + Description = functionContract.Description, + }; + var requiredParameterNames = new List(); + var propertiesSchemas = new Dictionary(); + var propertySchemaBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object); + foreach (var param in functionContract.Parameters ?? []) + { + if (param.Name is null) + { + throw new InvalidOperationException("Parameter name cannot be null"); + } + + var schemaBuilder = new JsonSchemaBuilder().FromType(param.ParameterType ?? throw new ArgumentNullException(nameof(param.ParameterType))); + if (param.Description != null) + { + schemaBuilder = schemaBuilder.Description(param.Description); + } + + if (param.IsRequired) + { + requiredParameterNames.Add(param.Name); + } + + var schema = schemaBuilder.Build(); + propertiesSchemas[param.Name] = schema; + + } + propertySchemaBuilder = propertySchemaBuilder.Properties(propertiesSchemas); + propertySchemaBuilder = propertySchemaBuilder.Required(requiredParameterNames); + + var option = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + + functionDefinition.Parameters = BinaryData.FromObjectAsJson(propertySchemaBuilder.Build(), option); + + return functionDefinition; + } +} diff --git a/dotnet/src/AutoGen.AzureAIInference/Middleware/AzureAIInferenceChatRequestMessageConnector.cs b/dotnet/src/AutoGen.AzureAIInference/Middleware/AzureAIInferenceChatRequestMessageConnector.cs new file mode 100644 index 000000000000..9c5d22e2e7e7 --- /dev/null +++ b/dotnet/src/AutoGen.AzureAIInference/Middleware/AzureAIInferenceChatRequestMessageConnector.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AzureAIInferenceChatRequestMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Core; +using Azure.AI.Inference; + +namespace AutoGen.AzureAIInference; + +/// +/// This middleware converts the incoming to where T is before sending to agent. And converts the output to after receiving from agent. +/// Supported are +/// - +/// - +/// - +/// - +/// - +/// - where T is +/// - where TMessage1 is and TMessage2 is +/// +public class AzureAIInferenceChatRequestMessageConnector : IStreamingMiddleware +{ + private bool strictMode = false; + + /// + /// Create a new instance of . + /// + /// If true, will throw an + /// When the message type is not supported. If false, it will ignore the unsupported message type. + public AzureAIInferenceChatRequestMessageConnector(bool strictMode = false) + { + this.strictMode = strictMode; + } + + public string? Name => nameof(AzureAIInferenceChatRequestMessageConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages); + + var reply = await agent.GenerateReplyAsync(chatMessages, context.Options, cancellationToken); + + return PostProcessMessage(reply); + } + + public async IAsyncEnumerable InvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages); + var streamingReply = agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken); + string? currentToolName = null; + await foreach (var reply in streamingReply) + { + if (reply is IMessage update) + { + if (update.Content.FunctionName is string functionName) + { + currentToolName = functionName; + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate toolCallUpdate && toolCallUpdate.Name is string toolCallName) + { + currentToolName = toolCallName; + } + var postProcessMessage = PostProcessStreamingMessage(update, currentToolName); + if (postProcessMessage != null) + { + yield return postProcessMessage; + } + } + else + { + if (this.strictMode) + { + throw new InvalidOperationException($"Invalid streaming message type {reply.GetType().Name}"); + } + else + { + yield return reply; + } + } + } + } + + public IMessage PostProcessMessage(IMessage message) + { + return message switch + { + IMessage m => PostProcessChatResponseMessage(m.Content, m.From), + IMessage m => PostProcessChatCompletions(m), + _ when strictMode is false => message, + _ => throw new InvalidOperationException($"Invalid return message type {message.GetType().Name}"), + }; + } + + public IMessage? PostProcessStreamingMessage(IMessage update, string? currentToolName) + { + if (update.Content.ContentUpdate is string contentUpdate && string.IsNullOrEmpty(contentUpdate) == false) + { + // text message + return new TextMessageUpdate(Role.Assistant, contentUpdate, from: update.From); + } + else if (update.Content.FunctionName is string functionName) + { + return new ToolCallMessageUpdate(functionName, string.Empty, from: update.From); + } + else if (update.Content.FunctionArgumentsUpdate is string functionArgumentsUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(currentToolName, functionArgumentsUpdate, from: update.From); + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate tooCallUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(tooCallUpdate.Name ?? currentToolName, tooCallUpdate.ArgumentsUpdate, from: update.From); + } + else + { + return null; + } + } + + private IMessage PostProcessChatCompletions(IMessage message) + { + // throw exception if prompt filter results is not null + if (message.Content.Choices[0].FinishReason == CompletionsFinishReason.ContentFiltered) + { + throw new InvalidOperationException("The content is filtered because its potential risk. Please try another input."); + } + + return PostProcessChatResponseMessage(message.Content.Choices[0].Message, message.From); + } + + private IMessage PostProcessChatResponseMessage(ChatResponseMessage chatResponseMessage, string? from) + { + var textContent = chatResponseMessage.Content; + if (chatResponseMessage.ToolCalls.Where(tc => tc is ChatCompletionsFunctionToolCall).Any()) + { + var functionToolCalls = chatResponseMessage.ToolCalls + .Where(tc => tc is ChatCompletionsFunctionToolCall) + .Select(tc => (ChatCompletionsFunctionToolCall)tc); + + var toolCalls = functionToolCalls.Select(tc => new ToolCall(tc.Name, tc.Arguments) { ToolCallId = tc.Id }); + + return new ToolCallMessage(toolCalls, from) + { + Content = textContent, + }; + } + + if (textContent is string content && !string.IsNullOrEmpty(content)) + { + return new TextMessage(Role.Assistant, content, from); + } + + throw new InvalidOperationException("Invalid ChatResponseMessage"); + } + + public IEnumerable ProcessIncomingMessages(IAgent agent, IEnumerable messages) + { + return messages.SelectMany(m => + { + if (m is IMessage crm) + { + return [crm]; + } + else + { + var chatRequestMessages = m switch + { + TextMessage textMessage => ProcessTextMessage(agent, textMessage), + ImageMessage imageMessage when (imageMessage.From is null || imageMessage.From != agent.Name) => ProcessImageMessage(agent, imageMessage), + MultiModalMessage multiModalMessage when (multiModalMessage.From is null || multiModalMessage.From != agent.Name) => ProcessMultiModalMessage(agent, multiModalMessage), + ToolCallMessage toolCallMessage when (toolCallMessage.From is null || toolCallMessage.From == agent.Name) => ProcessToolCallMessage(agent, toolCallMessage), + ToolCallResultMessage toolCallResultMessage => ProcessToolCallResultMessage(toolCallResultMessage), + AggregateMessage aggregateMessage => ProcessFunctionCallMiddlewareMessage(agent, aggregateMessage), + _ when strictMode is false => [], + _ => throw new InvalidOperationException($"Invalid message type: {m.GetType().Name}"), + }; + + if (chatRequestMessages.Any()) + { + return chatRequestMessages.Select(cm => MessageEnvelope.Create(cm, m.From)); + } + else + { + return [m]; + } + } + }); + } + + private IEnumerable ProcessTextMessage(IAgent agent, TextMessage message) + { + if (message.Role == Role.System) + { + return [new ChatRequestSystemMessage(message.Content)]; + } + + if (agent.Name == message.From) + { + return [new ChatRequestAssistantMessage { Content = message.Content }]; + } + else + { + return message.From switch + { + null when message.Role == Role.User => [new ChatRequestUserMessage(message.Content)], + null when message.Role == Role.Assistant => [new ChatRequestAssistantMessage() { Content = message.Content }], + null => throw new InvalidOperationException("Invalid Role"), + _ => [new ChatRequestUserMessage(message.Content)] + }; + } + } + + private IEnumerable ProcessImageMessage(IAgent agent, ImageMessage message) + { + if (agent.Name == message.From) + { + // image message from assistant is not supported + throw new ArgumentException("ImageMessage is not supported when message.From is the same with agent"); + } + + var imageContentItem = this.CreateChatMessageImageContentItemFromImageMessage(message); + return [new ChatRequestUserMessage([imageContentItem])]; + } + + private IEnumerable ProcessMultiModalMessage(IAgent agent, MultiModalMessage message) + { + if (agent.Name == message.From) + { + // image message from assistant is not supported + throw new ArgumentException("MultiModalMessage is not supported when message.From is the same with agent"); + } + + IEnumerable items = message.Content.Select(ci => ci switch + { + TextMessage text => new ChatMessageTextContentItem(text.Content), + ImageMessage image => this.CreateChatMessageImageContentItemFromImageMessage(image), + _ => throw new NotImplementedException(), + }); + + return [new ChatRequestUserMessage(items)]; + } + + private ChatMessageImageContentItem CreateChatMessageImageContentItemFromImageMessage(ImageMessage message) + { + return message.Data is null && message.Url is not null + ? new ChatMessageImageContentItem(new Uri(message.Url)) + : new ChatMessageImageContentItem(message.Data, message.Data?.MediaType); + } + + private IEnumerable ProcessToolCallMessage(IAgent agent, ToolCallMessage message) + { + if (message.From is not null && message.From != agent.Name) + { + throw new ArgumentException("ToolCallMessage is not supported when message.From is not the same with agent"); + } + + var toolCall = message.ToolCalls.Select((tc, i) => new ChatCompletionsFunctionToolCall(tc.ToolCallId ?? $"{tc.FunctionName}_{i}", tc.FunctionName, tc.FunctionArguments)); + var textContent = message.GetContent() ?? string.Empty; + var chatRequestMessage = new ChatRequestAssistantMessage() { Content = textContent }; + foreach (var tc in toolCall) + { + chatRequestMessage.ToolCalls.Add(tc); + } + + return [chatRequestMessage]; + } + + private IEnumerable ProcessToolCallResultMessage(ToolCallResultMessage message) + { + return message.ToolCalls + .Where(tc => tc.Result is not null) + .Select((tc, i) => new ChatRequestToolMessage(tc.Result, tc.ToolCallId ?? $"{tc.FunctionName}_{i}")); + } + + private IEnumerable ProcessFunctionCallMiddlewareMessage(IAgent agent, AggregateMessage aggregateMessage) + { + if (aggregateMessage.From is not null && aggregateMessage.From != agent.Name) + { + // convert as user message + var resultMessage = aggregateMessage.Message2; + + return resultMessage.ToolCalls.Select(tc => new ChatRequestUserMessage(tc.Result)); + } + else + { + var toolCallMessage1 = aggregateMessage.Message1; + var toolCallResultMessage = aggregateMessage.Message2; + + var assistantMessage = this.ProcessToolCallMessage(agent, toolCallMessage1); + var toolCallResults = this.ProcessToolCallResultMessage(toolCallResultMessage); + + return assistantMessage.Concat(toolCallResults); + } + } +} diff --git a/dotnet/src/AutoGen.Core/Agent/IAgent.cs b/dotnet/src/AutoGen.Core/Agent/IAgent.cs index b9149008480d..f2b8ce67d01b 100644 --- a/dotnet/src/AutoGen.Core/Agent/IAgent.cs +++ b/dotnet/src/AutoGen.Core/Agent/IAgent.cs @@ -5,12 +5,17 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Json.Schema; namespace AutoGen.Core; -public interface IAgent + +public interface IAgentMetaInformation { public string Name { get; } +} +public interface IAgent : IAgentMetaInformation +{ /// /// Generate reply /// @@ -38,6 +43,7 @@ public GenerateReplyOptions(GenerateReplyOptions other) this.MaxToken = other.MaxToken; this.StopSequence = other.StopSequence?.Select(s => s)?.ToArray(); this.Functions = other.Functions?.Select(f => f)?.ToArray(); + this.OutputSchema = other.OutputSchema; } public float? Temperature { get; set; } @@ -47,4 +53,9 @@ public GenerateReplyOptions(GenerateReplyOptions other) public string[]? StopSequence { get; set; } public FunctionContract[]? Functions { get; set; } + + /// + /// Structural schema for the output. This property only applies to certain LLMs. + /// + public JsonSchema? OutputSchema { get; set; } } diff --git a/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs b/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs index 665f18bac12a..6b7794c921ad 100644 --- a/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs +++ b/dotnet/src/AutoGen.Core/Agent/IStreamingAgent.cs @@ -11,7 +11,7 @@ namespace AutoGen.Core; /// public interface IStreamingAgent : IAgent { - public IAsyncEnumerable GenerateStreamingReplyAsync( + public IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default); diff --git a/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs b/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs index 52967d6ff1ce..c7643b1e4735 100644 --- a/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs +++ b/dotnet/src/AutoGen.Core/Agent/MiddlewareStreamingAgent.cs @@ -47,7 +47,7 @@ public Task GenerateReplyAsync(IEnumerable messages, Generat return _agent.GenerateReplyAsync(messages, options, cancellationToken); } - public IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) { return _agent.GenerateStreamingReplyAsync(messages, options, cancellationToken); } @@ -83,7 +83,7 @@ public Task GenerateReplyAsync(IEnumerable messages, Generat return this.streamingMiddleware.InvokeAsync(context, (IAgent)innerAgent, cancellationToken); } - public IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) { if (streamingMiddleware is null) { diff --git a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj index 60aeb3ae3fca..8cf9e9183d40 100644 --- a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj +++ b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.Core @@ -17,7 +17,10 @@ - + + + + diff --git a/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs b/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs index 44ce8838b73a..13ce970d551b 100644 --- a/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs +++ b/dotnet/src/AutoGen.Core/Extension/AgentExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AgentExtension.cs +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -60,14 +61,14 @@ public static async Task SendAsync( } /// - /// Send message to another agent. + /// Send message to another agent and iterate over the responses. /// /// sender agent. /// receiver agent. /// chat history. /// max conversation round. /// conversation history - public static async Task> SendAsync( + public static IAsyncEnumerable SendAsync( this IAgent agent, IAgent receiver, IEnumerable chatHistory, @@ -78,21 +79,21 @@ public static async Task> SendAsync( { var gc = manager.GroupChat; - return await agent.SendMessageToGroupAsync(gc, chatHistory, maxRound, ct); + return gc.SendAsync(chatHistory, maxRound, ct); } var groupChat = new RoundRobinGroupChat( - agents: new[] - { + agents: + [ agent, receiver, - }); + ]); - return await groupChat.CallAsync(chatHistory, maxRound, ct: ct); + return groupChat.SendAsync(chatHistory, maxRound, cancellationToken: ct); } /// - /// Send message to another agent. + /// Send message to another agent and iterate over the responses. /// /// sender agent. /// message to send. will be added to the end of if provided @@ -100,7 +101,7 @@ public static async Task> SendAsync( /// chat history. /// max conversation round. /// conversation history - public static async Task> SendAsync( + public static IAsyncEnumerable SendAsync( this IAgent agent, IAgent receiver, string message, @@ -116,11 +117,12 @@ public static async Task> SendAsync( chatHistory = chatHistory ?? new List(); chatHistory = chatHistory.Append(msg); - return await agent.SendAsync(receiver, chatHistory, maxRound, ct); + return agent.SendAsync(receiver, chatHistory, maxRound, ct); } /// - /// Shortcut API to send message to another agent. + /// Shortcut API to send message to another agent and get all responses. + /// To iterate over the responses, use or /// /// sender agent /// receiver agent @@ -144,10 +146,16 @@ public static async Task> InitiateChatAsync( chatHistory.Add(msg); } - return await agent.SendAsync(receiver, chatHistory, maxRound, ct); + await foreach (var msg in agent.SendAsync(receiver, chatHistory, maxRound, ct)) + { + chatHistory.Add(msg); + } + + return chatHistory; } - public static async Task> SendMessageToGroupAsync( + [Obsolete("use GroupChatExtension.SendAsync")] + public static IAsyncEnumerable SendMessageToGroupAsync( this IAgent agent, IGroupChat groupChat, string msg, @@ -159,16 +167,18 @@ public static async Task> SendMessageToGroupAsync( chatHistory = chatHistory ?? Enumerable.Empty(); chatHistory = chatHistory.Append(chatMessage); - return await agent.SendMessageToGroupAsync(groupChat, chatHistory, maxRound, ct); + return agent.SendMessageToGroupAsync(groupChat, chatHistory, maxRound, ct); } - public static async Task> SendMessageToGroupAsync( + [Obsolete("use GroupChatExtension.SendAsync")] + public static IAsyncEnumerable SendMessageToGroupAsync( this IAgent _, IGroupChat groupChat, IEnumerable? chatHistory = null, int maxRound = 10, CancellationToken ct = default) { - return await groupChat.CallAsync(chatHistory, maxRound, ct); + chatHistory = chatHistory ?? Enumerable.Empty(); + return groupChat.SendAsync(chatHistory, maxRound, ct); } } diff --git a/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs index e3e44622c817..89da7708797c 100644 --- a/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs +++ b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; namespace AutoGen.Core; @@ -23,6 +25,46 @@ public static void AddInitializeMessage(this IAgent agent, string message, IGrou groupChat.SendIntroduction(msg); } + /// + /// Send messages to a and return new messages from the group chat. + /// + /// + /// + /// + /// + /// + public static async IAsyncEnumerable SendAsync( + this IGroupChat groupChat, + IEnumerable chatHistory, + int maxRound = 10, + [EnumeratorCancellation] + CancellationToken cancellationToken = default) + { + while (maxRound-- > 0) + { + var messages = await groupChat.CallAsync(chatHistory, maxRound: 1, cancellationToken); + + // if no new messages, break the loop + if (messages.Count() == chatHistory.Count()) + { + yield break; + } + + var lastMessage = messages.Last(); + + yield return lastMessage; + if (lastMessage.IsGroupChatTerminateMessage()) + { + yield break; + } + + // messages will contain the complete chat history, include initalize messages + // but we only need to add the last message to the chat history + // fix #3268 + chatHistory = chatHistory.Append(lastMessage); + } + } + /// /// Send an instruction message to the group chat. /// @@ -78,6 +120,7 @@ public static bool IsGroupChatClearMessage(this IMessage message) return message.GetContent()?.Contains(CLEAR_MESSAGES) ?? false; } + [Obsolete] public static IEnumerable ProcessConversationForAgent( this IGroupChat groupChat, IEnumerable initialMessages, @@ -100,8 +143,7 @@ internal static IEnumerable ProcessConversationsForRolePlay( var msg = @$"From {x.From}: {x.GetContent()} -round # - {i}"; +round # {i}"; return new TextMessage(Role.User, content: msg); }); diff --git a/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs index 2c828c26d890..556c16436c63 100644 --- a/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs +++ b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs @@ -35,7 +35,7 @@ public class FunctionContract /// /// The name of the function. /// - public string? Name { get; set; } + public string Name { get; set; } = null!; /// /// The description of the function. diff --git a/dotnet/src/AutoGen.Core/GroupChat/Graph.cs b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs index d6b71e2a3f13..acff955a292c 100644 --- a/dotnet/src/AutoGen.Core/GroupChat/Graph.cs +++ b/dotnet/src/AutoGen.Core/GroupChat/Graph.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace AutoGen.Core; @@ -12,11 +13,7 @@ public class Graph { private readonly List transitions = new List(); - public Graph() - { - } - - public Graph(IEnumerable? transitions) + public Graph(IEnumerable? transitions = null) { if (transitions != null) { @@ -40,13 +37,13 @@ public void AddTransition(Transition transition) /// the from agent /// messages /// A list of agents that the messages can be transit to - public async Task> TransitToNextAvailableAgentsAsync(IAgent fromAgent, IEnumerable messages) + public async Task> TransitToNextAvailableAgentsAsync(IAgent fromAgent, IEnumerable messages, CancellationToken ct = default) { var nextAgents = new List(); var availableTransitions = transitions.FindAll(t => t.From == fromAgent) ?? Enumerable.Empty(); foreach (var transition in availableTransitions) { - if (await transition.CanTransitionAsync(messages)) + if (await transition.CanTransitionAsync(messages, ct)) { nextAgents.Add(transition.To); } @@ -63,7 +60,7 @@ public class Transition { private readonly IAgent _from; private readonly IAgent _to; - private readonly Func, Task>? _canTransition; + private readonly Func, CancellationToken, Task>? _canTransition; /// /// Create a new instance of . @@ -73,22 +70,44 @@ public class Transition /// from agent /// to agent /// detect if the transition is allowed, default to be always true - internal Transition(IAgent from, IAgent to, Func, Task>? canTransitionAsync = null) + internal Transition(IAgent from, IAgent to, Func, CancellationToken, Task>? canTransitionAsync = null) { _from = from; _to = to; _canTransition = canTransitionAsync; } + /// + /// Create a new instance of without transition condition check. + /// + /// " + public static Transition Create(TFromAgent from, TToAgent to) + where TFromAgent : IAgent + where TToAgent : IAgent + { + return new Transition(from, to, (fromAgent, toAgent, messages, _) => Task.FromResult(true)); + } + /// /// Create a new instance of . /// /// " - public static Transition Create(TFromAgent from, TToAgent to, Func, Task>? canTransitionAsync = null) + public static Transition Create(TFromAgent from, TToAgent to, Func, Task> canTransitionAsync) + where TFromAgent : IAgent + where TToAgent : IAgent + { + return new Transition(from, to, (fromAgent, toAgent, messages, _) => canTransitionAsync.Invoke((TFromAgent)fromAgent, (TToAgent)toAgent, messages)); + } + + /// + /// Create a new instance of with cancellation token. + /// + /// " + public static Transition Create(TFromAgent from, TToAgent to, Func, CancellationToken, Task> canTransitionAsync) where TFromAgent : IAgent where TToAgent : IAgent { - return new Transition(from, to, (fromAgent, toAgent, messages) => canTransitionAsync?.Invoke((TFromAgent)fromAgent, (TToAgent)toAgent, messages) ?? Task.FromResult(true)); + return new Transition(from, to, (fromAgent, toAgent, messages, ct) => canTransitionAsync.Invoke((TFromAgent)fromAgent, (TToAgent)toAgent, messages, ct)); } public IAgent From => _from; @@ -99,13 +118,13 @@ public static Transition Create(TFromAgent from, TToAgent /// Check if the transition is allowed. /// /// messages - public Task CanTransitionAsync(IEnumerable messages) + public Task CanTransitionAsync(IEnumerable messages, CancellationToken ct = default) { if (_canTransition == null) { return Task.FromResult(true); } - return _canTransition(this.From, this.To, messages); + return _canTransition(this.From, this.To, messages, ct); } } diff --git a/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs b/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs index 5e82931ab658..57e15c18ca62 100644 --- a/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs +++ b/dotnet/src/AutoGen.Core/GroupChat/GroupChat.cs @@ -15,6 +15,7 @@ public class GroupChat : IGroupChat private List agents = new List(); private IEnumerable initializeMessages = new List(); private Graph? workflow = null; + private readonly IOrchestrator orchestrator; public IEnumerable? Messages { get; private set; } @@ -36,6 +37,37 @@ public GroupChat( this.initializeMessages = initializeMessages ?? new List(); this.workflow = workflow; + if (admin is not null) + { + this.orchestrator = new RolePlayOrchestrator(admin, workflow); + } + else if (workflow is not null) + { + this.orchestrator = new WorkflowOrchestrator(workflow); + } + else + { + this.orchestrator = new RoundRobinOrchestrator(); + } + + this.Validation(); + } + + /// + /// Create a group chat which uses the to decide the next speaker(s). + /// + /// + /// + /// + public GroupChat( + IEnumerable members, + IOrchestrator orchestrator, + IEnumerable? initializeMessages = null) + { + this.agents = members.ToList(); + this.initializeMessages = initializeMessages ?? new List(); + this.orchestrator = orchestrator; + this.Validation(); } @@ -64,12 +96,6 @@ private void Validation() throw new Exception("All agents in the workflow must be in the group chat."); } } - - // must provide one of admin or workflow - if (this.admin == null && this.workflow == null) - { - throw new Exception("Must provide one of admin or workflow."); - } } /// @@ -81,6 +107,7 @@ private void Validation() /// current speaker /// conversation history /// next speaker. + [Obsolete("Please use RolePlayOrchestrator or WorkflowOrchestrator")] public async Task SelectNextSpeakerAsync(IAgent currentSpeaker, IEnumerable conversationHistory) { var agentNames = this.agents.Select(x => x.Name).ToList(); @@ -140,37 +167,40 @@ public void AddInitializeMessage(IMessage message) } public async Task> CallAsync( - IEnumerable? conversationWithName = null, + IEnumerable? chatHistory = null, int maxRound = 10, CancellationToken ct = default) { var conversationHistory = new List(); - if (conversationWithName != null) + conversationHistory.AddRange(this.initializeMessages); + if (chatHistory != null) { - conversationHistory.AddRange(conversationWithName); + conversationHistory.AddRange(chatHistory); } + var roundLeft = maxRound; - var lastSpeaker = conversationHistory.LastOrDefault()?.From switch + while (roundLeft > 0) { - null => this.agents.First(), - _ => this.agents.FirstOrDefault(x => x.Name == conversationHistory.Last().From) ?? throw new Exception("The agent is not in the group chat"), - }; - var round = 0; - while (round < maxRound) - { - var currentSpeaker = await this.SelectNextSpeakerAsync(lastSpeaker, conversationHistory); - var processedConversation = this.ProcessConversationForAgent(this.initializeMessages, conversationHistory); - var result = await currentSpeaker.GenerateReplyAsync(processedConversation) ?? throw new Exception("No result is returned."); + var orchestratorContext = new OrchestrationContext + { + Candidates = this.agents, + ChatHistory = conversationHistory, + }; + var nextSpeaker = await this.orchestrator.GetNextSpeakerAsync(orchestratorContext, ct); + if (nextSpeaker == null) + { + break; + } + + var result = await nextSpeaker.GenerateReplyAsync(conversationHistory, cancellationToken: ct); conversationHistory.Add(result); - // if message is terminate message, then terminate the conversation - if (result?.IsGroupChatTerminateMessage() ?? false) + if (result.IsGroupChatTerminateMessage()) { - break; + return conversationHistory; } - lastSpeaker = currentSpeaker; - round++; + roundLeft--; } return conversationHistory; diff --git a/dotnet/src/AutoGen.Core/IGroupChat.cs b/dotnet/src/AutoGen.Core/GroupChat/IGroupChat.cs similarity index 100% rename from dotnet/src/AutoGen.Core/IGroupChat.cs rename to dotnet/src/AutoGen.Core/GroupChat/IGroupChat.cs diff --git a/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs b/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs index b8de89b834fe..b95cd1958fc5 100644 --- a/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs +++ b/dotnet/src/AutoGen.Core/GroupChat/RoundRobinGroupChat.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace AutoGen.Core; @@ -25,76 +22,12 @@ public SequentialGroupChat(IEnumerable agents, List? initializ /// /// A group chat that allows agents to talk in a round-robin manner. /// -public class RoundRobinGroupChat : IGroupChat +public class RoundRobinGroupChat : GroupChat { - private readonly List agents = new List(); - private readonly List initializeMessages = new List(); - public RoundRobinGroupChat( IEnumerable agents, List? initializeMessages = null) + : base(agents, initializeMessages: initializeMessages) { - this.agents.AddRange(agents); - this.initializeMessages = initializeMessages ?? new List(); - } - - /// - public void AddInitializeMessage(IMessage message) - { - this.SendIntroduction(message); - } - - public async Task> CallAsync( - IEnumerable? conversationWithName = null, - int maxRound = 10, - CancellationToken ct = default) - { - var conversationHistory = new List(); - if (conversationWithName != null) - { - conversationHistory.AddRange(conversationWithName); - } - - var lastSpeaker = conversationHistory.LastOrDefault()?.From switch - { - null => this.agents.First(), - _ => this.agents.FirstOrDefault(x => x.Name == conversationHistory.Last().From) ?? throw new Exception("The agent is not in the group chat"), - }; - var round = 0; - while (round < maxRound) - { - var currentSpeaker = this.SelectNextSpeaker(lastSpeaker); - var processedConversation = this.ProcessConversationForAgent(this.initializeMessages, conversationHistory); - var result = await currentSpeaker.GenerateReplyAsync(processedConversation) ?? throw new Exception("No result is returned."); - conversationHistory.Add(result); - - // if message is terminate message, then terminate the conversation - if (result?.IsGroupChatTerminateMessage() ?? false) - { - break; - } - - lastSpeaker = currentSpeaker; - round++; - } - - return conversationHistory; - } - - public void SendIntroduction(IMessage message) - { - this.initializeMessages.Add(message); - } - - private IAgent SelectNextSpeaker(IAgent currentSpeaker) - { - var index = this.agents.IndexOf(currentSpeaker); - if (index == -1) - { - throw new ArgumentException("The agent is not in the group chat", nameof(currentSpeaker)); - } - - var nextIndex = (index + 1) % this.agents.Count; - return this.agents[nextIndex]; } } diff --git a/dotnet/src/AutoGen.Core/Message/IMessage.cs b/dotnet/src/AutoGen.Core/Message/IMessage.cs index ad215d510e3b..9952cbf06792 100644 --- a/dotnet/src/AutoGen.Core/Message/IMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/IMessage.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // IMessage.cs +using System; using System.Collections.Generic; namespace AutoGen.Core; @@ -35,19 +36,21 @@ namespace AutoGen.Core; /// /// /// -public interface IMessage : IStreamingMessage +public interface IMessage { + string? From { get; set; } } -public interface IMessage : IMessage, IStreamingMessage +public interface IMessage : IMessage { + T Content { get; } } /// /// The interface for messages that can get text content. /// This interface will be used by to get the content from the message. /// -public interface ICanGetTextContent : IMessage, IStreamingMessage +public interface ICanGetTextContent : IMessage { public string? GetContent(); } @@ -55,17 +58,18 @@ public interface ICanGetTextContent : IMessage, IStreamingMessage /// /// The interface for messages that can get a list of /// -public interface ICanGetToolCalls : IMessage, IStreamingMessage +public interface ICanGetToolCalls : IMessage { public IEnumerable GetToolCalls(); } - +[Obsolete("Use IMessage instead")] public interface IStreamingMessage { string? From { get; set; } } +[Obsolete("Use IMessage instead")] public interface IStreamingMessage : IStreamingMessage { T Content { get; } diff --git a/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs b/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs index f83bea279260..dc9709bbde5b 100644 --- a/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs +++ b/dotnet/src/AutoGen.Core/Message/MessageEnvelope.cs @@ -5,7 +5,7 @@ namespace AutoGen.Core; -public abstract class MessageEnvelope : IMessage, IStreamingMessage +public abstract class MessageEnvelope : IMessage { public MessageEnvelope(string? from = null, IDictionary? metadata = null) { @@ -23,7 +23,7 @@ public static MessageEnvelope Create(TContent content, strin public IDictionary Metadata { get; set; } } -public class MessageEnvelope : MessageEnvelope, IMessage, IStreamingMessage +public class MessageEnvelope : MessageEnvelope, IMessage { public MessageEnvelope(T content, string? from = null, IDictionary? metadata = null) : base(from, metadata) diff --git a/dotnet/src/AutoGen.Core/Message/TextMessage.cs b/dotnet/src/AutoGen.Core/Message/TextMessage.cs index addd8728a926..9419c2b3ba86 100644 --- a/dotnet/src/AutoGen.Core/Message/TextMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/TextMessage.cs @@ -3,7 +3,7 @@ namespace AutoGen.Core; -public class TextMessage : IMessage, IStreamingMessage, ICanGetTextContent +public class TextMessage : IMessage, ICanGetTextContent { public TextMessage(Role role, string content, string? from = null) { @@ -51,7 +51,7 @@ public override string ToString() } } -public class TextMessageUpdate : IStreamingMessage, ICanGetTextContent +public class TextMessageUpdate : IMessage, ICanGetTextContent { public TextMessageUpdate(Role role, string? content, string? from = null) { diff --git a/dotnet/src/AutoGen.Core/Message/ToolCallAggregateMessage.cs b/dotnet/src/AutoGen.Core/Message/ToolCallAggregateMessage.cs index 7781b785ef8c..7d46d56135aa 100644 --- a/dotnet/src/AutoGen.Core/Message/ToolCallAggregateMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/ToolCallAggregateMessage.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// FunctionCallAggregateMessage.cs +// ToolCallAggregateMessage.cs using System.Collections.Generic; diff --git a/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs b/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs index d0f89e1ecdde..8660b323044f 100644 --- a/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/ToolCallMessage.cs @@ -109,7 +109,7 @@ public IEnumerable GetToolCalls() } } -public class ToolCallMessageUpdate : IStreamingMessage +public class ToolCallMessageUpdate : IMessage { public ToolCallMessageUpdate(string functionName, string functionArgumentUpdate, string? from = null) { diff --git a/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs index d0788077b590..7d30f6d0928a 100644 --- a/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs +++ b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs @@ -70,7 +70,7 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, return reply; } - public async IAsyncEnumerable InvokeAsync( + public async IAsyncEnumerable InvokeAsync( MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -86,16 +86,16 @@ public async IAsyncEnumerable InvokeAsync( var combinedFunctions = this.functions?.Concat(options.Functions ?? []) ?? options.Functions; options.Functions = combinedFunctions?.ToArray(); - IStreamingMessage? initMessage = default; + IMessage? mergedFunctionCallMessage = default; await foreach (var message in agent.GenerateStreamingReplyAsync(context.Messages, options, cancellationToken)) { if (message is ToolCallMessageUpdate toolCallMessageUpdate && this.functionMap != null) { - if (initMessage is null) + if (mergedFunctionCallMessage is null) { - initMessage = new ToolCallMessage(toolCallMessageUpdate); + mergedFunctionCallMessage = new ToolCallMessage(toolCallMessageUpdate); } - else if (initMessage is ToolCallMessage toolCall) + else if (mergedFunctionCallMessage is ToolCallMessage toolCall) { toolCall.Update(toolCallMessageUpdate); } @@ -104,13 +104,17 @@ public async IAsyncEnumerable InvokeAsync( throw new InvalidOperationException("The first message is ToolCallMessage, but the update message is not ToolCallMessageUpdate"); } } + else if (message is ToolCallMessage toolCallMessage1) + { + mergedFunctionCallMessage = toolCallMessage1; + } else { yield return message; } } - if (initMessage is ToolCallMessage toolCallMsg) + if (mergedFunctionCallMessage is ToolCallMessage toolCallMsg) { yield return await this.InvokeToolCallMessagesAfterInvokingAgentAsync(toolCallMsg, agent); } diff --git a/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs index bc7aec57f52b..d550bdb519ce 100644 --- a/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs +++ b/dotnet/src/AutoGen.Core/Middleware/IStreamingMiddleware.cs @@ -14,7 +14,7 @@ public interface IStreamingMiddleware : IMiddleware /// /// The streaming version of . /// - public IAsyncEnumerable InvokeAsync( + public IAsyncEnumerable InvokeAsync( MiddlewareContext context, IStreamingAgent agent, CancellationToken cancellationToken = default); diff --git a/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs index 099f78e5f176..a4e84de85a44 100644 --- a/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs +++ b/dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs @@ -48,7 +48,7 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, } } - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { IMessage? recentUpdate = null; await foreach (var message in agent.GenerateStreamingReplyAsync(context.Messages, context.Options, cancellationToken)) diff --git a/dotnet/src/AutoGen.Core/Orchestrator/IOrchestrator.cs b/dotnet/src/AutoGen.Core/Orchestrator/IOrchestrator.cs new file mode 100644 index 000000000000..777834871f65 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Orchestrator/IOrchestrator.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IOrchestrator.cs + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class OrchestrationContext +{ + public IEnumerable Candidates { get; set; } = Array.Empty(); + + public IEnumerable ChatHistory { get; set; } = Array.Empty(); +} + +public interface IOrchestrator +{ + /// + /// Return the next agent as the next speaker. return null if no agent is selected. + /// + /// orchestration context, such as candidate agents and chat history. + /// cancellation token + public Task GetNextSpeakerAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestrator.cs b/dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestrator.cs new file mode 100644 index 000000000000..6798f23f2df8 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestrator.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RolePlayOrchestrator.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class RolePlayOrchestrator : IOrchestrator +{ + private readonly IAgent admin; + private readonly Graph? workflow = null; + public RolePlayOrchestrator(IAgent admin, Graph? workflow = null) + { + this.admin = admin; + this.workflow = workflow; + } + + public async Task GetNextSpeakerAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default) + { + var candidates = context.Candidates.ToList(); + + if (candidates.Count == 0) + { + return null; + } + + if (candidates.Count == 1) + { + return candidates.First(); + } + + // if there's a workflow + // and the next available agent from the workflow is in the group chat + // then return the next agent from the workflow + if (this.workflow != null) + { + var lastMessage = context.ChatHistory.LastOrDefault(); + if (lastMessage == null) + { + return null; + } + var currentSpeaker = candidates.First(candidates => candidates.Name == lastMessage.From); + var nextAgents = await this.workflow.TransitToNextAvailableAgentsAsync(currentSpeaker, context.ChatHistory); + nextAgents = nextAgents.Where(nextAgent => candidates.Any(candidate => candidate.Name == nextAgent.Name)); + candidates = nextAgents.ToList(); + if (!candidates.Any()) + { + return null; + } + + if (candidates is { Count: 1 }) + { + return candidates.First(); + } + } + + // In this case, since there are more than one available agents from the workflow for the next speaker + // the admin will be invoked to decide the next speaker + var agentNames = candidates.Select(candidate => candidate.Name); + var rolePlayMessage = new TextMessage(Role.User, + content: $@"You are in a role play game. Carefully read the conversation history and carry on the conversation. +The available roles are: +{string.Join(",", agentNames)} + +Each message will start with 'From name:', e.g: +From {agentNames.First()}: +//your message//."); + + var chatHistoryWithName = this.ProcessConversationsForRolePlay(context.ChatHistory); + var messages = new IMessage[] { rolePlayMessage }.Concat(chatHistoryWithName); + + var response = await this.admin.GenerateReplyAsync( + messages: messages, + options: new GenerateReplyOptions + { + Temperature = 0, + MaxToken = 128, + StopSequence = [":"], + Functions = null, + }, + cancellationToken: cancellationToken); + + var name = response.GetContent() ?? throw new Exception("No name is returned."); + + // remove From + name = name!.Substring(5); + var candidate = candidates.FirstOrDefault(x => x.Name!.ToLower() == name.ToLower()); + + if (candidate != null) + { + return candidate; + } + + var errorMessage = $"The response from admin is {name}, which is either not in the candidates list or not in the correct format."; + throw new Exception(errorMessage); + } + + private IEnumerable ProcessConversationsForRolePlay(IEnumerable messages) + { + return messages.Select((x, i) => + { + var msg = @$"From {x.From}: +{x.GetContent()} + +round # {i}"; + + return new TextMessage(Role.User, content: msg); + }); + } +} diff --git a/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs b/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs new file mode 100644 index 000000000000..af5efdc0e9ee --- /dev/null +++ b/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RoundRobinOrchestrator.cs + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +/// +/// Return the next agent in a round-robin fashion. +/// +/// If the last message is from one of the candidates, the next agent will be the next candidate in the list. +/// +/// +/// Otherwise, the first agent in will be returned. +/// +/// +/// +/// +public class RoundRobinOrchestrator : IOrchestrator +{ + public async Task GetNextSpeakerAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default) + { + var lastMessage = context.ChatHistory.LastOrDefault(); + + if (lastMessage == null) + { + return context.Candidates.FirstOrDefault(); + } + + var candidates = context.Candidates.ToList(); + var lastAgentIndex = candidates.FindIndex(a => a.Name == lastMessage.From); + if (lastAgentIndex == -1) + { + return null; + } + + var nextAgentIndex = (lastAgentIndex + 1) % candidates.Count; + return candidates[nextAgentIndex]; + } +} diff --git a/dotnet/src/AutoGen.Core/Orchestrator/WorkflowOrchestrator.cs b/dotnet/src/AutoGen.Core/Orchestrator/WorkflowOrchestrator.cs new file mode 100644 index 000000000000..b84850a07c75 --- /dev/null +++ b/dotnet/src/AutoGen.Core/Orchestrator/WorkflowOrchestrator.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// WorkflowOrchestrator.cs + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AutoGen.Core; + +public class WorkflowOrchestrator : IOrchestrator +{ + private readonly Graph workflow; + + public WorkflowOrchestrator(Graph workflow) + { + this.workflow = workflow; + } + + public async Task GetNextSpeakerAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default) + { + var lastMessage = context.ChatHistory.LastOrDefault(); + if (lastMessage == null) + { + return null; + } + + var candidates = context.Candidates.ToList(); + var currentSpeaker = candidates.FirstOrDefault(candidates => candidates.Name == lastMessage.From); + + if (currentSpeaker == null) + { + return null; + } + var nextAgents = await this.workflow.TransitToNextAvailableAgentsAsync(currentSpeaker, context.ChatHistory); + nextAgents = nextAgents.Where(nextAgent => candidates.Any(candidate => candidate.Name == nextAgent.Name)); + candidates = nextAgents.ToList(); + if (!candidates.Any()) + { + return null; + } + + if (candidates is { Count: 1 }) + { + return candidates.First(); + } + else + { + throw new System.Exception("There are more than one available agents from the workflow for the next speaker."); + } + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj index 72c67fe78016..e850d94944bc 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj +++ b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) enable enable AutoGen.DotnetInteractive @@ -27,12 +27,14 @@ - - - + + + + + diff --git a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs index bb5504cd5487..c9b59203462b 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveFunction.cs @@ -2,14 +2,12 @@ // DotnetInteractiveFunction.cs using System.Text; -using System.Text.Json; -using Azure.AI.OpenAI; using Microsoft.DotNet.Interactive.Documents; using Microsoft.DotNet.Interactive.Documents.Jupyter; namespace AutoGen.DotnetInteractive; -public class DotnetInteractiveFunction : IDisposable +public partial class DotnetInteractiveFunction : IDisposable { private readonly InteractiveService? _interactiveService = null; private string _notebookPath; @@ -71,6 +69,7 @@ public DotnetInteractiveFunction(InteractiveService interactiveService, string? /// Run existing dotnet code from message. Don't modify the code, run it as is. /// /// code. + [Function] public async Task RunCode(string code) { if (this._interactiveService == null) @@ -117,6 +116,7 @@ public async Task RunCode(string code) /// Install nuget packages. /// /// nuget package to install. + [Function] public async Task InstallNugetPackages(string[] nugetPackages) { if (this._interactiveService == null) @@ -173,105 +173,6 @@ private async Task AddCellAsync(string cellContent, string kernelName) writeStream.Dispose(); } - private class RunCodeSchema - { - public string code { get; set; } = string.Empty; - } - - public Task RunCodeWrapper(string arguments) - { - var schema = JsonSerializer.Deserialize( - arguments, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - - return RunCode(schema!.code); - } - - public FunctionDefinition RunCodeFunction - { - get => new FunctionDefinition - { - Name = @"RunCode", - Description = """ -Run existing dotnet code from message. Don't modify the code, run it as is. -""", - Parameters = BinaryData.FromObjectAsJson(new - { - Type = "object", - Properties = new - { - code = new - { - Type = @"string", - Description = @"code.", - }, - }, - Required = new[] - { - "code", - }, - }, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }) - }; - } - - private class InstallNugetPackagesSchema - { - public string[] nugetPackages { get; set; } = Array.Empty(); - } - - public Task InstallNugetPackagesWrapper(string arguments) - { - var schema = JsonSerializer.Deserialize( - arguments, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - - return InstallNugetPackages(schema!.nugetPackages); - } - - public FunctionDefinition InstallNugetPackagesFunction - { - get => new FunctionDefinition - { - Name = @"InstallNugetPackages", - Description = """ -Install nuget packages. -""", - Parameters = BinaryData.FromObjectAsJson(new - { - Type = "object", - Properties = new - { - nugetPackages = new - { - Type = @"array", - Items = new - { - Type = @"string", - }, - Description = @"nuget package to install.", - }, - }, - Required = new[] - { - "nugetPackages", - }, - }, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }) - }; - } public void Dispose() { this._interactiveService?.Dispose(); diff --git a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs new file mode 100644 index 000000000000..cc282fbba55c --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveKernelBuilder.cs + +namespace AutoGen.DotnetInteractive; + +public static class DotnetInteractiveKernelBuilder +{ + +#if NET8_0_OR_GREATER + public static InProccessDotnetInteractiveKernelBuilder CreateEmptyInProcessKernelBuilder() + { + return new InProccessDotnetInteractiveKernelBuilder(); + } + + + public static InProccessDotnetInteractiveKernelBuilder CreateDefaultInProcessKernelBuilder() + { + return new InProccessDotnetInteractiveKernelBuilder() + .AddCSharpKernel() + .AddFSharpKernel(); + } +#endif + + public static DotnetInteractiveStdioKernelConnector CreateKernelBuilder(string workingDirectory, string kernelName = "root-proxy") + { + return new DotnetInteractiveStdioKernelConnector(workingDirectory, kernelName); + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveStdioKernelConnector.cs b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveStdioKernelConnector.cs new file mode 100644 index 000000000000..a3ea80a7b12a --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveStdioKernelConnector.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveStdioKernelConnector.cs + +using AutoGen.DotnetInteractive.Extension; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Connection; + +namespace AutoGen.DotnetInteractive; + +public class DotnetInteractiveStdioKernelConnector +{ + private string workingDirectory; + private InteractiveService interactiveService; + private string kernelName; + private List setupCommands = new List(); + + internal DotnetInteractiveStdioKernelConnector(string workingDirectory, string kernelName = "root-proxy") + { + this.workingDirectory = workingDirectory; + this.interactiveService = new InteractiveService(workingDirectory); + this.kernelName = kernelName; + } + + public DotnetInteractiveStdioKernelConnector RestoreDotnetInteractive() + { + if (this.interactiveService.RestoreDotnetInteractive()) + { + return this; + } + else + { + throw new Exception("Failed to restore dotnet interactive tool."); + } + } + + public DotnetInteractiveStdioKernelConnector AddPythonKernel( + string venv, + string kernelName = "python") + { + var magicCommand = $"#!connect jupyter --kernel-name {kernelName} --kernel-spec {venv}"; + var connectCommand = new SubmitCode(magicCommand); + + this.setupCommands.Add(connectCommand); + + return this; + } + + public async Task BuildAsync(CancellationToken ct = default) + { + var compositeKernel = new CompositeKernel(); + var url = KernelHost.CreateHostUri(this.kernelName); + var cmd = new string[] + { + "dotnet", + "tool", + "run", + "dotnet-interactive", + $"[cb-{this.kernelName}]", + "stdio", + //"--default-kernel", + //"csharp", + "--working-dir", + $@"""{workingDirectory}""", + }; + + var connector = new StdIoKernelConnector( + cmd, + this.kernelName, + url, + new DirectoryInfo(this.workingDirectory)); + + var rootProxyKernel = await connector.CreateRootProxyKernelAsync(); + + rootProxyKernel.KernelInfo.SupportedKernelCommands.Add(new(nameof(SubmitCode))); + + var dotnetKernel = await connector.CreateProxyKernelAsync(".NET"); + foreach (var setupCommand in this.setupCommands) + { + var setupCommandResult = await rootProxyKernel.SendAsync(setupCommand, ct); + setupCommandResult.ThrowOnCommandFailed(); + } + + return rootProxyKernel; + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs index 83955c53fa16..de1e2a68cc0c 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs @@ -21,6 +21,7 @@ public static class AgentExtension /// [!code-csharp[Example04_Dynamic_GroupChat_Coding_Task](~/../sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs)] /// ]]> /// + [Obsolete] public static IAgent RegisterDotnetCodeBlockExectionHook( this IAgent agent, InteractiveService interactiveService, diff --git a/dotnet/src/AutoGen.DotnetInteractive/Utils.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs similarity index 57% rename from dotnet/src/AutoGen.DotnetInteractive/Utils.cs rename to dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs index d10208d508c6..2a7afdf8857f 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/Utils.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs @@ -1,23 +1,42 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Utils.cs +// KernelExtension.cs -using System.Collections; -using System.Collections.Immutable; using Microsoft.DotNet.Interactive; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Connection; using Microsoft.DotNet.Interactive.Events; -public static class ObservableExtensions +namespace AutoGen.DotnetInteractive.Extension; + +public static class KernelExtension { - public static SubscribedList ToSubscribedList(this IObservable source) + public static async Task RunSubmitCodeCommandAsync( + this Kernel kernel, + string codeBlock, + string targetKernelName, + CancellationToken ct = default) { - return new SubscribedList(source); + try + { + var cmd = new SubmitCode(codeBlock, targetKernelName); + var res = await kernel.SendAndThrowOnCommandFailedAsync(cmd, ct); + var events = res.Events; + var displayValues = res.Events.Where(x => x is StandardErrorValueProduced || x is StandardOutputValueProduced || x is ReturnValueProduced || x is DisplayedValueProduced) + .SelectMany(x => (x as DisplayEvent)!.FormattedValues); + + if (displayValues is null || displayValues.Count() == 0) + { + return null; + } + + return string.Join("\n", displayValues.Select(x => x.Value)); + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } } -} -public static class KernelExtensions -{ internal static void SetUpValueSharingIfSupported(this ProxyKernel proxyKernel) { var supportedCommands = proxyKernel.KernelInfo.SupportedKernelCommands; @@ -38,7 +57,7 @@ internal static async Task SendAndThrowOnCommandFailedAsync return result; } - private static void ThrowOnCommandFailed(this KernelCommandResult result) + internal static void ThrowOnCommandFailed(this KernelCommandResult result) { var failedEvents = result.Events.OfType(); if (!failedEvents.Any()) @@ -60,27 +79,3 @@ private static void ThrowOnCommandFailed(this KernelCommandResult result) private static Exception GetException(this CommandFailed commandFailedEvent) => new Exception(commandFailedEvent.Message); } - -public class SubscribedList : IReadOnlyList, IDisposable -{ - private ImmutableArray _list = ImmutableArray.Empty; - private readonly IDisposable _subscription; - - public SubscribedList(IObservable source) - { - _subscription = source.Subscribe(x => _list = _list.Add(x)); - } - - public IEnumerator GetEnumerator() - { - return ((IEnumerable)_list).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public int Count => _list.Length; - - public T this[int index] => _list[index]; - - public void Dispose() => _subscription.Dispose(); -} diff --git a/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs new file mode 100644 index 000000000000..6a8bf66c19f3 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtension.cs + +using System.Text.RegularExpressions; + +namespace AutoGen.DotnetInteractive.Extension; + +public static class MessageExtension +{ + /// + /// Extract a single code block from a message. If the message contains multiple code blocks, only the first one will be returned. + /// + /// + /// code block prefix, e.g. ```csharp + /// code block suffix, e.g. ``` + /// + public static string? ExtractCodeBlock( + this IMessage message, + string codeBlockPrefix, + string codeBlockSuffix) + { + foreach (var codeBlock in message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix)) + { + return codeBlock; + } + + return null; + } + + /// + /// Extract all code blocks from a message. + /// + /// + /// code block prefix, e.g. ```csharp + /// code block suffix, e.g. ``` + /// + public static IEnumerable ExtractCodeBlocks( + this IMessage message, + string codeBlockPrefix, + string codeBlockSuffix) + { + var content = message.GetContent() ?? string.Empty; + if (string.IsNullOrWhiteSpace(content)) + { + yield break; + } + + foreach (Match match in Regex.Matches(content, $@"{codeBlockPrefix}([\s\S]*?){codeBlockSuffix}")) + { + yield return match.Groups[1].Value.Trim(); + } + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/InProccessDotnetInteractiveKernelBuilder.cs b/dotnet/src/AutoGen.DotnetInteractive/InProccessDotnetInteractiveKernelBuilder.cs new file mode 100644 index 000000000000..6ddd3d6b4178 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/InProccessDotnetInteractiveKernelBuilder.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// InProccessDotnetInteractiveKernelBuilder.cs + +#if NET8_0_OR_GREATER +using AutoGen.DotnetInteractive.Extension; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.CSharp; +using Microsoft.DotNet.Interactive.FSharp; +using Microsoft.DotNet.Interactive.Jupyter; +using Microsoft.DotNet.Interactive.PackageManagement; +using Microsoft.DotNet.Interactive.PowerShell; + +namespace AutoGen.DotnetInteractive; + +/// +/// Build an in-proc dotnet interactive kernel. +/// +public class InProccessDotnetInteractiveKernelBuilder +{ + private readonly CompositeKernel compositeKernel; + + internal InProccessDotnetInteractiveKernelBuilder() + { + this.compositeKernel = new CompositeKernel(); + + // add jupyter connector + this.compositeKernel.AddKernelConnector( + new ConnectJupyterKernelCommand() + .AddConnectionOptions(new JupyterHttpKernelConnectionOptions()) + .AddConnectionOptions(new JupyterLocalKernelConnectionOptions())); + } + + public InProccessDotnetInteractiveKernelBuilder AddCSharpKernel(IEnumerable? aliases = null) + { + aliases ??= ["c#", "C#", "csharp"]; + // create csharp kernel + var csharpKernel = new CSharpKernel() + .UseNugetDirective((k, resolvedPackageReference) => + { + + k.AddAssemblyReferences(resolvedPackageReference + .SelectMany(r => r.AssemblyPaths)); + return Task.CompletedTask; + }) + .UseKernelHelpers() + .UseWho() + .UseMathAndLaTeX() + .UseValueSharing(); + + this.AddKernel(csharpKernel, aliases); + + return this; + } + + public InProccessDotnetInteractiveKernelBuilder AddFSharpKernel(IEnumerable? aliases = null) + { + aliases ??= ["f#", "F#", "fsharp"]; + // create fsharp kernel + var fsharpKernel = new FSharpKernel() + .UseDefaultFormatting() + .UseKernelHelpers() + .UseWho() + .UseMathAndLaTeX() + .UseValueSharing(); + + this.AddKernel(fsharpKernel, aliases); + + return this; + } + + public InProccessDotnetInteractiveKernelBuilder AddPowershellKernel(IEnumerable? aliases = null) + { + aliases ??= ["pwsh", "powershell"]; + // create powershell kernel + var powershellKernel = new PowerShellKernel() + .UseProfiles() + .UseValueSharing(); + + this.AddKernel(powershellKernel, aliases); + + return this; + } + + public InProccessDotnetInteractiveKernelBuilder AddPythonKernel(string venv, string kernelName = "python") + { + // create python kernel + var magicCommand = $"#!connect jupyter --kernel-name {kernelName} --kernel-spec {venv}"; + var connectCommand = new SubmitCode(magicCommand); + var result = this.compositeKernel.SendAsync(connectCommand).Result; + + result.ThrowOnCommandFailed(); + + return this; + } + + public CompositeKernel Build() + { + return this.compositeKernel + .UseDefaultMagicCommands() + .UseImportMagicCommand(); + } + + private InProccessDotnetInteractiveKernelBuilder AddKernel(Kernel kernel, IEnumerable? aliases = null) + { + this.compositeKernel.Add(kernel, aliases); + return this; + } +} +#endif diff --git a/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs index 7490b64e1267..3381aecf5794 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Reactive.Linq; using System.Reflection; +using AutoGen.DotnetInteractive.Extension; using Microsoft.DotNet.Interactive; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Connection; @@ -19,18 +20,14 @@ public class InteractiveService : IDisposable private bool disposedValue; private const string DotnetInteractiveToolNotInstallMessage = "Cannot find a tool in the manifest file that has a command named 'dotnet-interactive'."; //private readonly ProcessJobTracker jobTracker = new ProcessJobTracker(); - private string installingDirectory; - - public event EventHandler? DisplayEvent; - - public event EventHandler? Output; - - public event EventHandler? CommandFailed; - - public event EventHandler? HoverTextProduced; + private string? installingDirectory; /// - /// Create an instance of InteractiveService + /// Install dotnet interactive tool to + /// and create an instance of . + /// + /// When using this constructor, you need to call to install dotnet interactive tool + /// and start the kernel. /// /// dotnet interactive installing directory public InteractiveService(string installingDirectory) @@ -38,37 +35,37 @@ public InteractiveService(string installingDirectory) this.installingDirectory = installingDirectory; } + /// + /// Create an instance of with a running kernel. + /// When using this constructor, you don't need to call to start the kernel. + /// + /// + public InteractiveService(Kernel kernel) + { + this.kernel = kernel; + } + + public Kernel? Kernel => this.kernel; + public async Task StartAsync(string workingDirectory, CancellationToken ct = default) { + if (this.kernel != null) + { + return true; + } + this.kernel = await this.CreateKernelAsync(workingDirectory, true, ct); return true; } - public async Task SubmitCommandAsync(KernelCommand cmd, CancellationToken ct) + public async Task SubmitCommandAsync(SubmitCode cmd, CancellationToken ct) { if (this.kernel == null) { throw new Exception("Kernel is not running"); } - try - { - var res = await this.kernel.SendAndThrowOnCommandFailedAsync(cmd, ct); - var events = res.Events; - var displayValues = events.Where(x => x is StandardErrorValueProduced || x is StandardOutputValueProduced || x is ReturnValueProduced) - .SelectMany(x => (x as DisplayEvent)!.FormattedValues); - - if (displayValues is null || displayValues.Count() == 0) - { - return null; - } - - return string.Join("\n", displayValues.Select(x => x.Value)); - } - catch (Exception ex) - { - return $"Error: {ex.Message}"; - } + return await this.kernel.RunSubmitCodeCommandAsync(cmd.Code, cmd.TargetKernelName, ct); } public async Task SubmitPowershellCodeAsync(string code, CancellationToken ct) @@ -85,7 +82,11 @@ public async Task StartAsync(string workingDirectory, CancellationToken ct public bool RestoreDotnetInteractive() { - this.WriteLine("Restore dotnet interactive tool"); + if (this.installingDirectory is null) + { + throw new Exception("Installing directory is not set"); + } + // write RestoreInteractive.config from embedded resource to this.workingDirectory var assembly = Assembly.GetAssembly(typeof(InteractiveService))!; var resourceName = "AutoGen.DotnetInteractive.RestoreInteractive.config"; @@ -178,8 +179,6 @@ await rootProxyKernel.SendAsync( //compositeKernel.DefaultKernelName = "csharp"; compositeKernel.Add(rootProxyKernel); - compositeKernel.KernelEvents.Subscribe(this.OnKernelDiagnosticEventReceived); - return compositeKernel; } catch (CommandLineInvocationException) when (restoreWhenFail) @@ -195,35 +194,11 @@ await rootProxyKernel.SendAsync( } } - private void OnKernelDiagnosticEventReceived(KernelEvent ke) - { - this.WriteLine("Receive data from kernel"); - this.WriteLine(KernelEventEnvelope.Serialize(ke)); - - switch (ke) - { - case DisplayEvent de: - this.DisplayEvent?.Invoke(this, de); - break; - case CommandFailed cf: - this.CommandFailed?.Invoke(this, cf); - break; - case HoverTextProduced cf: - this.HoverTextProduced?.Invoke(this, cf); - break; - } - } - - private void WriteLine(string data) - { - this.Output?.Invoke(this, data); - } - private void PrintProcessOutput(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { - this.WriteLine(e.Data); + Console.WriteLine(e.Data); } } diff --git a/dotnet/src/AutoGen.Gemini/AutoGen.Gemini.csproj b/dotnet/src/AutoGen.Gemini/AutoGen.Gemini.csproj index 29c4d1bb9c6f..9a60596503bc 100644 --- a/dotnet/src/AutoGen.Gemini/AutoGen.Gemini.csproj +++ b/dotnet/src/AutoGen.Gemini/AutoGen.Gemini.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) diff --git a/dotnet/src/AutoGen.Gemini/GeminiChatAgent.cs b/dotnet/src/AutoGen.Gemini/GeminiChatAgent.cs index b081faae8321..e759ba26d1e9 100644 --- a/dotnet/src/AutoGen.Gemini/GeminiChatAgent.cs +++ b/dotnet/src/AutoGen.Gemini/GeminiChatAgent.cs @@ -143,7 +143,7 @@ public async Task GenerateReplyAsync(IEnumerable messages, G return MessageEnvelope.Create(response, this.Name); } - public async IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var request = BuildChatRequest(messages, options); var response = this.client.GenerateContentStreamAsync(request); diff --git a/dotnet/src/AutoGen.Gemini/IGeminiClient.cs b/dotnet/src/AutoGen.Gemini/IGeminiClient.cs index 2e209e02b030..d391a4508398 100644 --- a/dotnet/src/AutoGen.Gemini/IGeminiClient.cs +++ b/dotnet/src/AutoGen.Gemini/IGeminiClient.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// IVertexGeminiClient.cs +// IGeminiClient.cs using System.Collections.Generic; using System.Threading; diff --git a/dotnet/src/AutoGen.Gemini/Middleware/GeminiMessageConnector.cs b/dotnet/src/AutoGen.Gemini/Middleware/GeminiMessageConnector.cs index cb18ba084d78..422fb4cd3458 100644 --- a/dotnet/src/AutoGen.Gemini/Middleware/GeminiMessageConnector.cs +++ b/dotnet/src/AutoGen.Gemini/Middleware/GeminiMessageConnector.cs @@ -39,7 +39,7 @@ public GeminiMessageConnector(bool strictMode = false) public string Name => nameof(GeminiMessageConnector); - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var messages = ProcessMessage(context.Messages, agent); diff --git a/dotnet/src/AutoGen.Gemini/VertexGeminiClient.cs b/dotnet/src/AutoGen.Gemini/VertexGeminiClient.cs index c54f2280dfd3..12a11993cd69 100644 --- a/dotnet/src/AutoGen.Gemini/VertexGeminiClient.cs +++ b/dotnet/src/AutoGen.Gemini/VertexGeminiClient.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// IGeminiClient.cs +// VertexGeminiClient.cs using System.Collections.Generic; using System.Threading; diff --git a/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj index f45a2f7eba5f..aa891e71294d 100644 --- a/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj +++ b/dotnet/src/AutoGen.LMStudio/AutoGen.LMStudio.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.LMStudio @@ -17,7 +17,7 @@ - + diff --git a/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs b/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs index 9d0daa535b23..c4808b443c79 100644 --- a/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs +++ b/dotnet/src/AutoGen.LMStudio/LMStudioAgent.cs @@ -6,7 +6,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using AutoGen.OpenAI; +using AutoGen.OpenAI.V1; using Azure.AI.OpenAI; using Azure.Core.Pipeline; @@ -18,6 +18,7 @@ namespace AutoGen.LMStudio; /// /// [!code-csharp[LMStudioAgent](../../sample/AutoGen.BasicSamples/Example08_LMStudio.cs?name=lmstudio_example_1)] /// +[Obsolete("Use OpenAIChatAgent to connect to LM Studio")] public class LMStudioAgent : IAgent { private readonly GPTAgent innerAgent; @@ -80,7 +81,7 @@ protected override Task SendAsync(HttpRequestMessage reques { // request.RequestUri = new Uri($"{_modelServiceUrl}{request.RequestUri.PathAndQuery}"); var uriBuilder = new UriBuilder(_modelServiceUrl); - uriBuilder.Path = request.RequestUri.PathAndQuery; + uriBuilder.Path = request.RequestUri?.PathAndQuery ?? throw new InvalidOperationException("RequestUri is null"); request.RequestUri = uriBuilder.Uri; return base.SendAsync(request, cancellationToken); } diff --git a/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs b/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs index cc2c74145504..db14d68a1217 100644 --- a/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs +++ b/dotnet/src/AutoGen.Mistral/Agent/MistralClientAgent.cs @@ -78,7 +78,7 @@ public async Task GenerateReplyAsync( return new MessageEnvelope(response, from: this.Name); } - public async IAsyncEnumerable GenerateStreamingReplyAsync( + public async IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -97,6 +97,7 @@ private ChatCompletionRequest BuildChatRequest(IEnumerable messages, G var chatHistory = BuildChatHistory(messages); var chatRequest = new ChatCompletionRequest(model: _model, messages: chatHistory.ToList(), temperature: options?.Temperature, randomSeed: _randomSeed) { + Stop = options?.StopSequence, MaxTokens = options?.MaxToken, ResponseFormat = _jsonOutput ? new ResponseFormat() { ResponseFormatType = "json_object" } : null, }; diff --git a/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj index 25cc05fec922..ee905d117791 100644 --- a/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj +++ b/dotnet/src/AutoGen.Mistral/AutoGen.Mistral.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.Mistral diff --git a/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs b/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs index 5a4f9f9cb189..9ecf11428397 100644 --- a/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs +++ b/dotnet/src/AutoGen.Mistral/Converters/JsonPropertyNameEnumConverter.cs @@ -29,7 +29,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { var field = value.GetType().GetField(value.ToString()); - var attribute = field.GetCustomAttribute(); + var attribute = field?.GetCustomAttribute(); if (attribute != null) { diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs index 71a084673f13..affe2bb6dcc3 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionRequest.cs @@ -105,6 +105,9 @@ public class ChatCompletionRequest [JsonPropertyName("random_seed")] public int? RandomSeed { get; set; } + [JsonPropertyName("stop")] + public string[]? Stop { get; set; } + [JsonPropertyName("tools")] public List? Tools { get; set; } diff --git a/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs b/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs index 95592e97fcc5..78de12a5c01e 100644 --- a/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs +++ b/dotnet/src/AutoGen.Mistral/Middleware/MistralChatMessageConnector.cs @@ -15,14 +15,14 @@ public class MistralChatMessageConnector : IStreamingMiddleware, IMiddleware { public string? Name => nameof(MistralChatMessageConnector); - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var messages = context.Messages; var chatMessages = ProcessMessage(messages, agent); var chunks = new List(); await foreach (var reply in agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken)) { - if (reply is IStreamingMessage chatMessage) + if (reply is IMessage chatMessage) { chunks.Add(chatMessage.Content); var response = ProcessChatCompletionResponse(chatMessage, agent); @@ -167,7 +167,7 @@ private IMessage PostProcessMessage(ChatCompletionResponse response, IAgent from } } - private IStreamingMessage? ProcessChatCompletionResponse(IStreamingMessage message, IAgent agent) + private IMessage? ProcessChatCompletionResponse(IMessage message, IAgent agent) { var response = message.Content; if (response.VarObject != "chat.completion.chunk") diff --git a/dotnet/src/AutoGen.Mistral/MistralClient.cs b/dotnet/src/AutoGen.Mistral/MistralClient.cs index 5fc3d110985e..8c6802f30eb1 100644 --- a/dotnet/src/AutoGen.Mistral/MistralClient.cs +++ b/dotnet/src/AutoGen.Mistral/MistralClient.cs @@ -49,7 +49,7 @@ public async IAsyncEnumerable StreamingChatCompletionsAs var response = await HttpRequestRaw(HttpMethod.Post, chatCompletionRequest, streaming: true); using var stream = await response.Content.ReadAsStreamAsync(); using StreamReader reader = new StreamReader(stream); - string line; + string? line = null; SseEvent currentEvent = new SseEvent(); while ((line = await reader.ReadLineAsync()) != null) @@ -67,13 +67,13 @@ public async IAsyncEnumerable StreamingChatCompletionsAs else if (currentEvent.EventType == null) { var res = await JsonSerializer.DeserializeAsync( - new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data))) ?? throw new Exception("Failed to deserialize response"); + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data ?? string.Empty))) ?? throw new Exception("Failed to deserialize response"); yield return res; } else if (currentEvent.EventType != null) { var res = await JsonSerializer.DeserializeAsync( - new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data))); + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data ?? string.Empty))); throw new Exception(res?.Error.Message); } diff --git a/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs b/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs index 9ef68388d605..87b176d8bcc5 100644 --- a/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs +++ b/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs @@ -53,7 +53,7 @@ public async Task GenerateReplyAsync( } } - public async IAsyncEnumerable GenerateStreamingReplyAsync( + public async IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) diff --git a/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj b/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj index a939f138c1c1..512fe92f3e3e 100644 --- a/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj +++ b/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.Ollama True diff --git a/dotnet/src/AutoGen.Ollama/DTOs/Message.cs b/dotnet/src/AutoGen.Ollama/DTOs/Message.cs index 2e0d891cc61e..75f622ff7f04 100644 --- a/dotnet/src/AutoGen.Ollama/DTOs/Message.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/Message.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// ChatResponseUpdate.cs +// Message.cs using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs b/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs index 5ce0dc8cc40a..cce6dbb83076 100644 --- a/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // ITextEmbeddingService.cs using System.Threading; diff --git a/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs b/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs index 2e431e7bcb81..ea4993eb813f 100644 --- a/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // OllamaTextEmbeddingService.cs using System; diff --git a/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs index 7f2531c522ad..d776b183db0b 100644 --- a/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // TextEmbeddingsRequest.cs using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs index 580059c033b5..f3ce64b7032f 100644 --- a/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // TextEmbeddingsResponse.cs using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs b/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs index a21ec3a1c991..9e85ca12fd9e 100644 --- a/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs +++ b/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs @@ -30,14 +30,14 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, }; } - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var messages = ProcessMessage(context.Messages, agent); var chunks = new List(); await foreach (var update in agent.GenerateStreamingReplyAsync(messages, context.Options, cancellationToken)) { - if (update is IStreamingMessage chatResponseUpdate) + if (update is IMessage chatResponseUpdate) { var response = chatResponseUpdate.Content switch { @@ -101,7 +101,7 @@ private IEnumerable ProcessMultiModalMessage(MultiModalMessage multiMo // collect all the images var images = imageMessages.SelectMany(m => ProcessImageMessage((ImageMessage)m, agent) - .SelectMany(m => (m as IMessage)?.Content.Images)); + .SelectMany(m => (m as IMessage)?.Content.Images ?? [])); var message = new Message() { diff --git a/dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs b/dotnet/src/AutoGen.OpenAI.V1/Agent/GPTAgent.cs similarity index 96% rename from dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs rename to dotnet/src/AutoGen.OpenAI.V1/Agent/GPTAgent.cs index cdc6cc464d17..a32af5c38f15 100644 --- a/dotnet/src/AutoGen.OpenAI/Agent/GPTAgent.cs +++ b/dotnet/src/AutoGen.OpenAI.V1/Agent/GPTAgent.cs @@ -5,10 +5,10 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using AutoGen.OpenAI.Extension; +using AutoGen.OpenAI.V1.Extension; using Azure.AI.OpenAI; -namespace AutoGen.OpenAI; +namespace AutoGen.OpenAI.V1; /// /// GPT agent that can be used to connect to OpenAI chat models like GPT-3.5, GPT-4, etc. @@ -27,6 +27,7 @@ namespace AutoGen.OpenAI; /// - /// - where TMessage1 is and TMessage2 is /// +[Obsolete("Use OpenAIChatAgent instead")] public class GPTAgent : IStreamingAgent { private readonly OpenAIClient openAIClient; @@ -104,7 +105,7 @@ public async Task GenerateReplyAsync( return await _innerAgent.GenerateReplyAsync(messages, options, cancellationToken); } - public IAsyncEnumerable GenerateStreamingReplyAsync( + public IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) diff --git a/dotnet/src/AutoGen.OpenAI.V1/Agent/OpenAIChatAgent.cs b/dotnet/src/AutoGen.OpenAI.V1/Agent/OpenAIChatAgent.cs new file mode 100644 index 000000000000..2305536b4e5d --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/Agent/OpenAIChatAgent.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatAgent.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI.V1.Extension; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI.V1; + +/// +/// OpenAI client agent. This agent is a thin wrapper around to provide a simple interface for chat completions. +/// To better work with other agents, it's recommended to use which supports more message types and have a better compatibility with other agents. +/// supports the following message types: +/// +/// +/// where T is : chat request message. +/// +/// +/// returns the following message types: +/// +/// +/// where T is : chat response message. +/// where T is : streaming chat completions update. +/// +/// +/// +public class OpenAIChatAgent : IStreamingAgent +{ + private readonly OpenAIClient openAIClient; + private readonly ChatCompletionsOptions options; + private readonly string systemMessage; + + /// + /// Create a new instance of . + /// + /// openai client + /// agent name + /// model name. e.g. gpt-turbo-3.5 + /// system message + /// temperature + /// max tokens to generated + /// response format, set it to to enable json mode. + /// seed to use, set it to enable deterministic output + /// functions + public OpenAIChatAgent( + OpenAIClient openAIClient, + string name, + string modelName, + string systemMessage = "You are a helpful AI assistant", + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable? functions = null) + : this( + openAIClient: openAIClient, + name: name, + options: CreateChatCompletionOptions(modelName, temperature, maxTokens, seed, responseFormat, functions), + systemMessage: systemMessage) + { + } + + /// + /// Create a new instance of . + /// + /// openai client + /// agent name + /// system message + /// chat completion option. The option can't contain messages + public OpenAIChatAgent( + OpenAIClient openAIClient, + string name, + ChatCompletionsOptions options, + string systemMessage = "You are a helpful AI assistant") + { + if (options.Messages is { Count: > 0 }) + { + throw new ArgumentException("Messages should not be provided in options"); + } + + this.openAIClient = openAIClient; + this.Name = name; + this.options = options; + this.systemMessage = systemMessage; + } + + public string Name { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var reply = await this.openAIClient.GetChatCompletionsAsync(settings, cancellationToken); + + return new MessageEnvelope(reply, from: this.Name); + } + + public async IAsyncEnumerable GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var settings = this.CreateChatCompletionsOptions(options, messages); + var response = await this.openAIClient.GetChatCompletionsStreamingAsync(settings, cancellationToken); + await foreach (var update in response.WithCancellation(cancellationToken)) + { + if (update.ChoiceIndex > 0) + { + throw new InvalidOperationException("Only one choice is supported in streaming response"); + } + + yield return new MessageEnvelope(update, from: this.Name); + } + } + + private ChatCompletionsOptions CreateChatCompletionsOptions(GenerateReplyOptions? options, IEnumerable messages) + { + var oaiMessages = messages.Select(m => m switch + { + IMessage chatRequestMessage => chatRequestMessage.Content, + _ => throw new ArgumentException("Invalid message type") + }); + + // add system message if there's no system message in messages + if (!oaiMessages.Any(m => m is ChatRequestSystemMessage)) + { + oaiMessages = new[] { new ChatRequestSystemMessage(systemMessage) }.Concat(oaiMessages); + } + + // clone the options by serializing and deserializing + var json = JsonSerializer.Serialize(this.options); + var settings = JsonSerializer.Deserialize(json) ?? throw new InvalidOperationException("Failed to clone options"); + + foreach (var m in oaiMessages) + { + settings.Messages.Add(m); + } + + settings.Temperature = options?.Temperature ?? settings.Temperature; + settings.MaxTokens = options?.MaxToken ?? settings.MaxTokens; + + foreach (var functions in this.options.Tools) + { + settings.Tools.Add(functions); + } + + foreach (var stopSequence in this.options.StopSequences) + { + settings.StopSequences.Add(stopSequence); + } + + var openAIFunctionDefinitions = options?.Functions?.Select(f => f.ToOpenAIFunctionDefinition()).ToList(); + if (openAIFunctionDefinitions is { Count: > 0 }) + { + foreach (var f in openAIFunctionDefinitions) + { + settings.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + } + } + + if (options?.StopSequence is var sequence && sequence is { Length: > 0 }) + { + foreach (var seq in sequence) + { + settings.StopSequences.Add(seq); + } + } + + return settings; + } + + private static ChatCompletionsOptions CreateChatCompletionOptions( + string modelName, + float temperature = 0.7f, + int maxTokens = 1024, + int? seed = null, + ChatCompletionsResponseFormat? responseFormat = null, + IEnumerable? functions = null) + { + var options = new ChatCompletionsOptions(modelName, []) + { + Temperature = temperature, + MaxTokens = maxTokens, + Seed = seed, + ResponseFormat = responseFormat, + }; + + if (functions is not null) + { + foreach (var f in functions) + { + options.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + } + } + + return options; + } +} diff --git a/dotnet/src/AutoGen.OpenAI.V1/AutoGen.OpenAI.V1.csproj b/dotnet/src/AutoGen.OpenAI.V1/AutoGen.OpenAI.V1.csproj new file mode 100644 index 000000000000..21951cb32fbd --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/AutoGen.OpenAI.V1.csproj @@ -0,0 +1,27 @@ + + + $(PackageTargetFrameworks) + AutoGen.OpenAI + + + + + + + AutoGen.OpenAI.V1 + + OpenAI Intergration for AutoGen. + This package connects to openai using Azure.AI.OpenAI v1 package. It is reserved to keep compatibility with the projects which stick to that v1 package. + To use the latest version of OpenAI SDK, please use AutoGen.OpenAI package. + + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs b/dotnet/src/AutoGen.OpenAI.V1/AzureOpenAIConfig.cs similarity index 95% rename from dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs rename to dotnet/src/AutoGen.OpenAI.V1/AzureOpenAIConfig.cs index 31df784ed21a..2be8f21dc4fc 100644 --- a/dotnet/src/AutoGen.OpenAI/AzureOpenAIConfig.cs +++ b/dotnet/src/AutoGen.OpenAI.V1/AzureOpenAIConfig.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AzureOpenAIConfig.cs -namespace AutoGen.OpenAI; +namespace AutoGen.OpenAI.V1; public class AzureOpenAIConfig : ILLMConfig { diff --git a/dotnet/src/AutoGen.OpenAI.V1/Extension/FunctionContractExtension.cs b/dotnet/src/AutoGen.OpenAI.V1/Extension/FunctionContractExtension.cs new file mode 100644 index 000000000000..62009b927eff --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/Extension/FunctionContractExtension.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionContractExtension.cs + +using System; +using System.Collections.Generic; +using Azure.AI.OpenAI; +using Json.Schema; +using Json.Schema.Generation; + +namespace AutoGen.OpenAI.V1.Extension; + +public static class FunctionContractExtension +{ + /// + /// Convert a to a that can be used in gpt funciton call. + /// + /// function contract + /// + public static FunctionDefinition ToOpenAIFunctionDefinition(this FunctionContract functionContract) + { + var functionDefinition = new FunctionDefinition + { + Name = functionContract.Name, + Description = functionContract.Description, + }; + var requiredParameterNames = new List(); + var propertiesSchemas = new Dictionary(); + var propertySchemaBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object); + foreach (var param in functionContract.Parameters ?? []) + { + if (param.Name is null) + { + throw new InvalidOperationException("Parameter name cannot be null"); + } + + var schemaBuilder = new JsonSchemaBuilder().FromType(param.ParameterType ?? throw new ArgumentNullException(nameof(param.ParameterType))); + if (param.Description != null) + { + schemaBuilder = schemaBuilder.Description(param.Description); + } + + if (param.IsRequired) + { + requiredParameterNames.Add(param.Name); + } + + var schema = schemaBuilder.Build(); + propertiesSchemas[param.Name] = schema; + + } + propertySchemaBuilder = propertySchemaBuilder.Properties(propertiesSchemas); + propertySchemaBuilder = propertySchemaBuilder.Required(requiredParameterNames); + + var option = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + + functionDefinition.Parameters = BinaryData.FromObjectAsJson(propertySchemaBuilder.Build(), option); + + return functionDefinition; + } +} diff --git a/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs b/dotnet/src/AutoGen.OpenAI.V1/Extension/MessageExtension.cs similarity index 99% rename from dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs rename to dotnet/src/AutoGen.OpenAI.V1/Extension/MessageExtension.cs index ed795e5e8ed8..3264dccf3a8a 100644 --- a/dotnet/src/AutoGen.OpenAI/Extension/MessageExtension.cs +++ b/dotnet/src/AutoGen.OpenAI.V1/Extension/MessageExtension.cs @@ -6,7 +6,7 @@ using System.Linq; using Azure.AI.OpenAI; -namespace AutoGen.OpenAI; +namespace AutoGen.OpenAI.V1; public static class MessageExtension { diff --git a/dotnet/src/AutoGen.OpenAI.V1/Extension/OpenAIAgentExtension.cs b/dotnet/src/AutoGen.OpenAI.V1/Extension/OpenAIAgentExtension.cs new file mode 100644 index 000000000000..6c0df8e0e965 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/Extension/OpenAIAgentExtension.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIAgentExtension.cs + +namespace AutoGen.OpenAI.V1.Extension; + +public static class OpenAIAgentExtension +{ + /// + /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this OpenAIChatAgent agent, OpenAIChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OpenAIChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, OpenAIChatRequestMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OpenAIChatRequestMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.OpenAI.V1/GlobalUsing.cs b/dotnet/src/AutoGen.OpenAI.V1/GlobalUsing.cs new file mode 100644 index 000000000000..d66bf001ed5e --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/src/AutoGen.OpenAI.V1/Middleware/OpenAIChatRequestMessageConnector.cs b/dotnet/src/AutoGen.OpenAI.V1/Middleware/OpenAIChatRequestMessageConnector.cs new file mode 100644 index 000000000000..3587d1b0d6f9 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI.V1/Middleware/OpenAIChatRequestMessageConnector.cs @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatRequestMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; + +namespace AutoGen.OpenAI.V1; + +/// +/// This middleware converts the incoming to where T is before sending to agent. And converts the output to after receiving from agent. +/// Supported are +/// - +/// - +/// - +/// - +/// - +/// - where T is +/// - where TMessage1 is and TMessage2 is +/// +public class OpenAIChatRequestMessageConnector : IMiddleware, IStreamingMiddleware +{ + private bool strictMode = false; + + /// + /// Create a new instance of . + /// + /// If true, will throw an + /// When the message type is not supported. If false, it will ignore the unsupported message type. + public OpenAIChatRequestMessageConnector(bool strictMode = false) + { + this.strictMode = strictMode; + } + + public string? Name => nameof(OpenAIChatRequestMessageConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages); + + var reply = await agent.GenerateReplyAsync(chatMessages, context.Options, cancellationToken); + + return PostProcessMessage(reply); + } + + public async IAsyncEnumerable InvokeAsync( + MiddlewareContext context, + IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var chatMessages = ProcessIncomingMessages(agent, context.Messages); + var streamingReply = agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken); + string? currentToolName = null; + await foreach (var reply in streamingReply) + { + if (reply is IMessage update) + { + if (update.Content.FunctionName is string functionName) + { + currentToolName = functionName; + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate toolCallUpdate && toolCallUpdate.Name is string toolCallName) + { + currentToolName = toolCallName; + } + var postProcessMessage = PostProcessStreamingMessage(update, currentToolName); + if (postProcessMessage != null) + { + yield return postProcessMessage; + } + } + else + { + if (this.strictMode) + { + throw new InvalidOperationException($"Invalid streaming message type {reply.GetType().Name}"); + } + else + { + yield return reply; + } + } + } + } + + public IMessage PostProcessMessage(IMessage message) + { + return message switch + { + IMessage m => PostProcessChatResponseMessage(m.Content, m.From), + IMessage m => PostProcessChatCompletions(m), + _ when strictMode is false => message, + _ => throw new InvalidOperationException($"Invalid return message type {message.GetType().Name}"), + }; + } + + public IMessage? PostProcessStreamingMessage(IMessage update, string? currentToolName) + { + if (update.Content.ContentUpdate is string contentUpdate) + { + // text message + return new TextMessageUpdate(Role.Assistant, contentUpdate, from: update.From); + } + else if (update.Content.FunctionName is string functionName) + { + return new ToolCallMessageUpdate(functionName, string.Empty, from: update.From); + } + else if (update.Content.FunctionArgumentsUpdate is string functionArgumentsUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(currentToolName, functionArgumentsUpdate, from: update.From); + } + else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate tooCallUpdate && currentToolName is string) + { + return new ToolCallMessageUpdate(tooCallUpdate.Name ?? currentToolName, tooCallUpdate.ArgumentsUpdate, from: update.From); + } + else + { + return null; + } + } + + private IMessage PostProcessChatCompletions(IMessage message) + { + // throw exception if prompt filter results is not null + if (message.Content.Choices[0].FinishReason == CompletionsFinishReason.ContentFiltered) + { + throw new InvalidOperationException("The content is filtered because its potential risk. Please try another input."); + } + + return PostProcessChatResponseMessage(message.Content.Choices[0].Message, message.From); + } + + private IMessage PostProcessChatResponseMessage(ChatResponseMessage chatResponseMessage, string? from) + { + var textContent = chatResponseMessage.Content; + if (chatResponseMessage.FunctionCall is FunctionCall functionCall) + { + return new ToolCallMessage(functionCall.Name, functionCall.Arguments, from) + { + Content = textContent, + }; + } + + if (chatResponseMessage.ToolCalls.Where(tc => tc is ChatCompletionsFunctionToolCall).Any()) + { + var functionToolCalls = chatResponseMessage.ToolCalls + .Where(tc => tc is ChatCompletionsFunctionToolCall) + .Select(tc => (ChatCompletionsFunctionToolCall)tc); + + var toolCalls = functionToolCalls.Select(tc => new ToolCall(tc.Name, tc.Arguments) { ToolCallId = tc.Id }); + + return new ToolCallMessage(toolCalls, from) + { + Content = textContent, + }; + } + + if (textContent is string content && !string.IsNullOrEmpty(content)) + { + return new TextMessage(Role.Assistant, content, from); + } + + throw new InvalidOperationException("Invalid ChatResponseMessage"); + } + + public IEnumerable ProcessIncomingMessages(IAgent agent, IEnumerable messages) + { + return messages.SelectMany(m => + { + if (m is IMessage crm) + { + return [crm]; + } + else + { + var chatRequestMessages = m switch + { + TextMessage textMessage => ProcessTextMessage(agent, textMessage), + ImageMessage imageMessage when (imageMessage.From is null || imageMessage.From != agent.Name) => ProcessImageMessage(agent, imageMessage), + MultiModalMessage multiModalMessage when (multiModalMessage.From is null || multiModalMessage.From != agent.Name) => ProcessMultiModalMessage(agent, multiModalMessage), + ToolCallMessage toolCallMessage when (toolCallMessage.From is null || toolCallMessage.From == agent.Name) => ProcessToolCallMessage(agent, toolCallMessage), + ToolCallResultMessage toolCallResultMessage => ProcessToolCallResultMessage(toolCallResultMessage), + AggregateMessage aggregateMessage => ProcessFunctionCallMiddlewareMessage(agent, aggregateMessage), +#pragma warning disable CS0618 // deprecated + Message msg => ProcessMessage(agent, msg), +#pragma warning restore CS0618 // deprecated + _ when strictMode is false => [], + _ => throw new InvalidOperationException($"Invalid message type: {m.GetType().Name}"), + }; + + if (chatRequestMessages.Any()) + { + return chatRequestMessages.Select(cm => MessageEnvelope.Create(cm, m.From)); + } + else + { + return [m]; + } + } + }); + } + + [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] + private IEnumerable ProcessIncomingMessagesForSelf(Message message) + { + if (message.Role == Role.System) + { + return new[] { new ChatRequestSystemMessage(message.Content) }; + } + else if (message.Content is string content && content is { Length: > 0 }) + { + if (message.FunctionName is null) + { + return new[] { new ChatRequestAssistantMessage(message.Content) }; + } + else + { + return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; + } + } + else if (message.FunctionName is string functionName) + { + var msg = new ChatRequestAssistantMessage(content: null) + { + FunctionCall = new FunctionCall(functionName, message.FunctionArguments) + }; + + return new[] + { + msg, + }; + } + else + { + throw new InvalidOperationException("Invalid Message as message from self."); + } + } + + [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] + private IEnumerable ProcessIncomingMessagesForOther(Message message) + { + if (message.Role == Role.System) + { + return [new ChatRequestSystemMessage(message.Content) { Name = message.From }]; + } + else if (message.Content is string content && content is { Length: > 0 }) + { + if (message.FunctionName is not null) + { + return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; + } + + return [new ChatRequestUserMessage(message.Content) { Name = message.From }]; + } + else if (message.FunctionName is string _) + { + return [new ChatRequestUserMessage("// Message type is not supported") { Name = message.From }]; + } + else + { + throw new InvalidOperationException("Invalid Message as message from other."); + } + } + + private IEnumerable ProcessTextMessage(IAgent agent, TextMessage message) + { + if (message.Role == Role.System) + { + return [new ChatRequestSystemMessage(message.Content) { Name = message.From }]; + } + + if (agent.Name == message.From) + { + return [new ChatRequestAssistantMessage(message.Content) { Name = agent.Name }]; + } + else + { + return message.From switch + { + null when message.Role == Role.User => [new ChatRequestUserMessage(message.Content)], + null when message.Role == Role.Assistant => [new ChatRequestAssistantMessage(message.Content)], + null => throw new InvalidOperationException("Invalid Role"), + _ => [new ChatRequestUserMessage(message.Content) { Name = message.From }] + }; + } + } + + private IEnumerable ProcessImageMessage(IAgent agent, ImageMessage message) + { + if (agent.Name == message.From) + { + // image message from assistant is not supported + throw new ArgumentException("ImageMessage is not supported when message.From is the same with agent"); + } + + var imageContentItem = this.CreateChatMessageImageContentItemFromImageMessage(message); + return [new ChatRequestUserMessage([imageContentItem]) { Name = message.From }]; + } + + private IEnumerable ProcessMultiModalMessage(IAgent agent, MultiModalMessage message) + { + if (agent.Name == message.From) + { + // image message from assistant is not supported + throw new ArgumentException("MultiModalMessage is not supported when message.From is the same with agent"); + } + + IEnumerable items = message.Content.Select(ci => ci switch + { + TextMessage text => new ChatMessageTextContentItem(text.Content), + ImageMessage image => this.CreateChatMessageImageContentItemFromImageMessage(image), + _ => throw new NotImplementedException(), + }); + + return [new ChatRequestUserMessage(items) { Name = message.From }]; + } + + private ChatMessageImageContentItem CreateChatMessageImageContentItemFromImageMessage(ImageMessage message) + { + return message.Data is null && message.Url is not null + ? new ChatMessageImageContentItem(new Uri(message.Url)) + : new ChatMessageImageContentItem(message.Data, message.Data?.MediaType); + } + + private IEnumerable ProcessToolCallMessage(IAgent agent, ToolCallMessage message) + { + if (message.From is not null && message.From != agent.Name) + { + throw new ArgumentException("ToolCallMessage is not supported when message.From is not the same with agent"); + } + + var toolCall = message.ToolCalls.Select((tc, i) => new ChatCompletionsFunctionToolCall(tc.ToolCallId ?? $"{tc.FunctionName}_{i}", tc.FunctionName, tc.FunctionArguments)); + var textContent = message.GetContent() ?? string.Empty; + + // don't include the name field when it's tool call message. + // fix https://github.com/microsoft/autogen/issues/3437 + var chatRequestMessage = new ChatRequestAssistantMessage(textContent); + foreach (var tc in toolCall) + { + chatRequestMessage.ToolCalls.Add(tc); + } + + return [chatRequestMessage]; + } + + private IEnumerable ProcessToolCallResultMessage(ToolCallResultMessage message) + { + return message.ToolCalls + .Where(tc => tc.Result is not null) + .Select((tc, i) => new ChatRequestToolMessage(tc.Result, tc.ToolCallId ?? $"{tc.FunctionName}_{i}")); + } + + [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] + private IEnumerable ProcessMessage(IAgent agent, Message message) + { + if (message.From is not null && message.From != agent.Name) + { + return ProcessIncomingMessagesForOther(message); + } + else + { + return ProcessIncomingMessagesForSelf(message); + } + } + + private IEnumerable ProcessFunctionCallMiddlewareMessage(IAgent agent, AggregateMessage aggregateMessage) + { + if (aggregateMessage.From is not null && aggregateMessage.From != agent.Name) + { + // convert as user message + var resultMessage = aggregateMessage.Message2; + + return resultMessage.ToolCalls.Select(tc => new ChatRequestUserMessage(tc.Result) { Name = aggregateMessage.From }); + } + else + { + var toolCallMessage1 = aggregateMessage.Message1; + var toolCallResultMessage = aggregateMessage.Message2; + + var assistantMessage = this.ProcessToolCallMessage(agent, toolCallMessage1); + var toolCallResults = this.ProcessToolCallResultMessage(toolCallResultMessage); + + return assistantMessage.Concat(toolCallResults); + } + } +} diff --git a/dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs b/dotnet/src/AutoGen.OpenAI.V1/OpenAIConfig.cs similarity index 91% rename from dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs rename to dotnet/src/AutoGen.OpenAI.V1/OpenAIConfig.cs index 35ce1e491aa9..592647cc2c1e 100644 --- a/dotnet/src/AutoGen.OpenAI/OpenAIConfig.cs +++ b/dotnet/src/AutoGen.OpenAI.V1/OpenAIConfig.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // OpenAIConfig.cs -namespace AutoGen.OpenAI; +namespace AutoGen.OpenAI.V1; public class OpenAIConfig : ILLMConfig { diff --git a/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs index 37a4882f69e1..b0085d0f33c6 100644 --- a/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs +++ b/dotnet/src/AutoGen.OpenAI/Agent/OpenAIChatAgent.cs @@ -8,70 +8,79 @@ using System.Threading; using System.Threading.Tasks; using AutoGen.OpenAI.Extension; -using Azure.AI.OpenAI; +using global::OpenAI; +using global::OpenAI.Chat; +using Json.Schema; namespace AutoGen.OpenAI; /// /// OpenAI client agent. This agent is a thin wrapper around to provide a simple interface for chat completions. -/// To better work with other agents, it's recommended to use which supports more message types and have a better compatibility with other agents. /// supports the following message types: /// /// -/// where T is : chat request message. +/// where T is : chat message. /// /// /// returns the following message types: /// /// -/// where T is : chat response message. -/// where T is : streaming chat completions update. +/// where T is : chat response message. +/// where T is : streaming chat completions update. /// /// /// public class OpenAIChatAgent : IStreamingAgent { - private readonly OpenAIClient openAIClient; - private readonly string modelName; - private readonly float _temperature; - private readonly int _maxTokens = 1024; - private readonly IEnumerable? _functions; - private readonly string _systemMessage; - private readonly ChatCompletionsResponseFormat? _responseFormat; - private readonly int? _seed; + private readonly ChatClient chatClient; + private readonly ChatCompletionOptions options; + private readonly string? systemMessage; /// /// Create a new instance of . /// - /// openai client + /// openai client /// agent name - /// model name. e.g. gpt-turbo-3.5 /// system message /// temperature /// max tokens to generated - /// response format, set it to to enable json mode. + /// response format, set it to to enable json mode. /// seed to use, set it to enable deterministic output /// functions public OpenAIChatAgent( - OpenAIClient openAIClient, + ChatClient chatClient, string name, - string modelName, - string systemMessage = "You are a helpful AI assistant", - float temperature = 0.7f, - int maxTokens = 1024, + string? systemMessage = "You are a helpful AI assistant", + float? temperature = null, + int? maxTokens = null, int? seed = null, - ChatCompletionsResponseFormat? responseFormat = null, - IEnumerable? functions = null) + ChatResponseFormat? responseFormat = null, + IEnumerable? functions = null) + : this( + chatClient: chatClient, + name: name, + options: CreateChatCompletionOptions(temperature, maxTokens, seed, responseFormat, functions), + systemMessage: systemMessage) { - this.openAIClient = openAIClient; - this.modelName = modelName; + } + + /// + /// Create a new instance of . + /// + /// openai chat client + /// agent name + /// system message + /// chat completion option. The option can't contain messages + public OpenAIChatAgent( + ChatClient chatClient, + string name, + ChatCompletionOptions options, + string? systemMessage = "You are a helpful AI assistant") + { + this.chatClient = chatClient; this.Name = name; - _temperature = temperature; - _maxTokens = maxTokens; - _functions = functions; - _systemMessage = systemMessage; - _responseFormat = responseFormat; - _seed = seed; + this.options = options; + this.systemMessage = systemMessage; } public string Name { get; } @@ -81,59 +90,85 @@ public async Task GenerateReplyAsync( GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) { - var settings = this.CreateChatCompletionsOptions(options, messages); - var reply = await this.openAIClient.GetChatCompletionsAsync(settings, cancellationToken); - - return new MessageEnvelope(reply, from: this.Name); + var chatHistory = this.CreateChatMessages(messages); + var settings = this.CreateChatCompletionsOptions(options); + var reply = await this.chatClient.CompleteChatAsync(chatHistory, settings, cancellationToken); + return new MessageEnvelope(reply.Value, from: this.Name); } - public async IAsyncEnumerable GenerateStreamingReplyAsync( + public async IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var settings = this.CreateChatCompletionsOptions(options, messages); - var response = await this.openAIClient.GetChatCompletionsStreamingAsync(settings, cancellationToken); + var chatHistory = this.CreateChatMessages(messages); + var settings = this.CreateChatCompletionsOptions(options); + var response = this.chatClient.CompleteChatStreamingAsync(chatHistory, settings, cancellationToken); await foreach (var update in response.WithCancellation(cancellationToken)) { - if (update.ChoiceIndex > 0) + if (update.ContentUpdate.Count > 1) { throw new InvalidOperationException("Only one choice is supported in streaming response"); } - yield return new MessageEnvelope(update, from: this.Name); + yield return new MessageEnvelope(update, from: this.Name); } } - private ChatCompletionsOptions CreateChatCompletionsOptions(GenerateReplyOptions? options, IEnumerable messages) + private IEnumerable CreateChatMessages(IEnumerable messages) { var oaiMessages = messages.Select(m => m switch { - IMessage chatRequestMessage => chatRequestMessage.Content, + IMessage chatMessage => chatMessage.Content, _ => throw new ArgumentException("Invalid message type") }); // add system message if there's no system message in messages - if (!oaiMessages.Any(m => m is ChatRequestSystemMessage)) + if (!oaiMessages.Any(m => m is SystemChatMessage) && systemMessage is not null) { - oaiMessages = new[] { new ChatRequestSystemMessage(_systemMessage) }.Concat(oaiMessages); + oaiMessages = new[] { new SystemChatMessage(systemMessage) }.Concat(oaiMessages); } - var settings = new ChatCompletionsOptions(this.modelName, oaiMessages) + return oaiMessages; + } + + private ChatCompletionOptions CreateChatCompletionsOptions(GenerateReplyOptions? options) + { + var option = new ChatCompletionOptions() { - MaxTokens = options?.MaxToken ?? _maxTokens, - Temperature = options?.Temperature ?? _temperature, - ResponseFormat = _responseFormat, - Seed = _seed, + Seed = this.options.Seed, + Temperature = options?.Temperature ?? this.options.Temperature, + MaxTokens = options?.MaxToken ?? this.options.MaxTokens, + ResponseFormat = this.options.ResponseFormat, + FrequencyPenalty = this.options.FrequencyPenalty, + FunctionChoice = this.options.FunctionChoice, + IncludeLogProbabilities = this.options.IncludeLogProbabilities, + ParallelToolCallsEnabled = this.options.ParallelToolCallsEnabled, + PresencePenalty = this.options.PresencePenalty, + ToolChoice = this.options.ToolChoice, + TopLogProbabilityCount = this.options.TopLogProbabilityCount, + TopP = this.options.TopP, + EndUserId = this.options.EndUserId, }; - var openAIFunctionDefinitions = options?.Functions?.Select(f => f.ToOpenAIFunctionDefinition()); - var functions = openAIFunctionDefinitions ?? _functions; - if (functions is not null && functions.Count() > 0) + // add tools from this.options to option + foreach (var tool in this.options.Tools) { - foreach (var f in functions) + option.Tools.Add(tool); + } + + // add stop sequences from this.options to option + foreach (var seq in this.options.StopSequences) + { + option.StopSequences.Add(seq); + } + + var openAIFunctionDefinitions = options?.Functions?.Select(f => f.ToChatTool()).ToList(); + if (openAIFunctionDefinitions is { Count: > 0 }) + { + foreach (var f in openAIFunctionDefinitions) { - settings.Tools.Add(new ChatCompletionsFunctionToolDefinition(f)); + option.Tools.Add(f); } } @@ -141,10 +176,44 @@ private ChatCompletionsOptions CreateChatCompletionsOptions(GenerateReplyOptions { foreach (var seq in sequence) { - settings.StopSequences.Add(seq); + option.StopSequences.Add(seq); + } + } + + if (options?.OutputSchema is not null) + { + option.ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat( + name: options.OutputSchema.GetTitle() ?? throw new ArgumentException("Output schema must have a title"), + jsonSchema: BinaryData.FromObjectAsJson(options.OutputSchema), + description: options.OutputSchema.GetDescription()); + } + + return option; + } + + private static ChatCompletionOptions CreateChatCompletionOptions( + float? temperature = 0.7f, + int? maxTokens = 1024, + int? seed = null, + ChatResponseFormat? responseFormat = null, + IEnumerable? functions = null) + { + var options = new ChatCompletionOptions + { + Temperature = temperature, + MaxTokens = maxTokens, + Seed = seed, + ResponseFormat = responseFormat, + }; + + if (functions is not null) + { + foreach (var f in functions) + { + options.Tools.Add(f); } } - return settings; + return options; } } diff --git a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj index 7220cfe5c628..f93fdd4bc5e2 100644 --- a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj +++ b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.OpenAI @@ -11,11 +11,12 @@ AutoGen.OpenAI OpenAI Intergration for AutoGen. + If your project still depends on Azure.AI.OpenAI v1, please use AutoGen.OpenAI.V1 package instead. - + diff --git a/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs b/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs index 4accdc4d8d46..dd1c1125aec0 100644 --- a/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs +++ b/dotnet/src/AutoGen.OpenAI/Extension/FunctionContractExtension.cs @@ -3,26 +3,21 @@ using System; using System.Collections.Generic; -using Azure.AI.OpenAI; using Json.Schema; using Json.Schema.Generation; +using OpenAI.Chat; namespace AutoGen.OpenAI.Extension; public static class FunctionContractExtension { /// - /// Convert a to a that can be used in gpt funciton call. + /// Convert a to a that can be used in gpt funciton call. /// /// function contract - /// - public static FunctionDefinition ToOpenAIFunctionDefinition(this FunctionContract functionContract) + /// + public static ChatTool ToChatTool(this FunctionContract functionContract) { - var functionDefinition = new FunctionDefinition - { - Name = functionContract.Name, - Description = functionContract.Description, - }; var requiredParameterNames = new List(); var propertiesSchemas = new Dictionary(); var propertySchemaBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object); @@ -56,8 +51,22 @@ public static FunctionDefinition ToOpenAIFunctionDefinition(this FunctionContrac PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }; - functionDefinition.Parameters = BinaryData.FromObjectAsJson(propertySchemaBuilder.Build(), option); + var functionDefinition = ChatTool.CreateFunctionTool( + functionContract.Name ?? throw new ArgumentNullException(nameof(functionContract.Name)), + functionContract.Description, + BinaryData.FromObjectAsJson(propertySchemaBuilder.Build(), option)); return functionDefinition; } + + /// + /// Convert a to a that can be used in gpt funciton call. + /// + /// function contract + /// + [Obsolete("Use ToChatTool instead")] + public static ChatTool ToOpenAIFunctionDefinition(this FunctionContract functionContract) + { + return functionContract.ToChatTool(); + } } diff --git a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs index c1dc2caa99fb..fd55a1350326 100644 --- a/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs +++ b/dotnet/src/AutoGen.OpenAI/Middleware/OpenAIChatRequestMessageConnector.cs @@ -7,19 +7,19 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace AutoGen.OpenAI; /// -/// This middleware converts the incoming to where T is before sending to agent. And converts the output to after receiving from agent. +/// This middleware converts the incoming to where T is before sending to agent. And converts the output to after receiving from agent. /// Supported are /// - /// - /// - /// - /// - -/// - where T is +/// - where T is /// - where TMessage1 is and TMessage2 is /// public class OpenAIChatRequestMessageConnector : IMiddleware, IStreamingMiddleware @@ -47,31 +47,26 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, return PostProcessMessage(reply); } - public async IAsyncEnumerable InvokeAsync( + public async IAsyncEnumerable InvokeAsync( MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var chatMessages = ProcessIncomingMessages(agent, context.Messages); var streamingReply = agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken); - string? currentToolName = null; + var chunks = new List(); + + // only streaming the text content await foreach (var reply in streamingReply) { - if (reply is IStreamingMessage update) + if (reply is IMessage update) { - if (update.Content.FunctionName is string functionName) - { - currentToolName = functionName; - } - else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate toolCallUpdate && toolCallUpdate.Name is string toolCallName) + if (update.Content.ContentUpdate.Count == 1 && update.Content.ContentUpdate[0].Kind == ChatMessageContentPartKind.Text) { - currentToolName = toolCallName; - } - var postProcessMessage = PostProcessStreamingMessage(update, currentToolName); - if (postProcessMessage != null) - { - yield return postProcessMessage; + yield return new TextMessageUpdate(Role.Assistant, update.Content.ContentUpdate[0].Text, from: update.From); } + + chunks.Add(update.Content); } else { @@ -85,83 +80,140 @@ public async IAsyncEnumerable InvokeAsync( } } } + + // process the tool call + var streamingChatToolCallUpdates = chunks.Where(c => c.ToolCallUpdates.Count > 0) + .SelectMany(c => c.ToolCallUpdates) + .ToList(); + + // collect all text parts + var textParts = chunks.SelectMany(c => c.ContentUpdate) + .Where(c => c.Kind == ChatMessageContentPartKind.Text) + .Select(c => c.Text) + .ToList(); + + // combine the tool call and function call into one ToolCallMessages + var text = string.Join(string.Empty, textParts); + var toolCalls = new List(); + var currentToolName = string.Empty; + var currentToolArguments = string.Empty; + var currentToolId = string.Empty; + int? currentIndex = null; + foreach (var toolCall in streamingChatToolCallUpdates) + { + if (currentIndex is null) + { + currentIndex = toolCall.Index; + } + + if (toolCall.Index == currentIndex) + { + currentToolName += toolCall.FunctionName; + currentToolArguments += toolCall.FunctionArgumentsUpdate; + currentToolId += toolCall.Id; + + yield return new ToolCallMessageUpdate(currentToolName, currentToolArguments, from: agent.Name); + } + else + { + toolCalls.Add(new ToolCall(currentToolName, currentToolArguments) { ToolCallId = currentToolId }); + currentToolName = toolCall.FunctionName; + currentToolArguments = toolCall.FunctionArgumentsUpdate; + currentToolId = toolCall.Id; + currentIndex = toolCall.Index; + + yield return new ToolCallMessageUpdate(currentToolName, currentToolArguments, from: agent.Name); + } + } + + if (string.IsNullOrEmpty(currentToolName) is false) + { + toolCalls.Add(new ToolCall(currentToolName, currentToolArguments) { ToolCallId = currentToolId }); + } + + if (toolCalls.Any()) + { + yield return new ToolCallMessage(toolCalls, from: agent.Name) + { + Content = text, + }; + } } public IMessage PostProcessMessage(IMessage message) { return message switch { - IMessage m => PostProcessChatResponseMessage(m.Content, m.From), - IMessage m => PostProcessChatCompletions(m), + IMessage m => PostProcessChatCompletions(m), _ when strictMode is false => message, _ => throw new InvalidOperationException($"Invalid return message type {message.GetType().Name}"), }; } - public IStreamingMessage? PostProcessStreamingMessage(IStreamingMessage update, string? currentToolName) + private IMessage PostProcessChatCompletions(IMessage message) { - if (update.Content.ContentUpdate is string contentUpdate) - { - // text message - return new TextMessageUpdate(Role.Assistant, contentUpdate, from: update.From); - } - else if (update.Content.FunctionName is string functionName) - { - return new ToolCallMessageUpdate(functionName, string.Empty, from: update.From); - } - else if (update.Content.FunctionArgumentsUpdate is string functionArgumentsUpdate && currentToolName is string) - { - return new ToolCallMessageUpdate(currentToolName, functionArgumentsUpdate, from: update.From); - } - else if (update.Content.ToolCallUpdate is StreamingFunctionToolCallUpdate tooCallUpdate && currentToolName is string) + // throw exception if prompt filter results is not null + if (message.Content.FinishReason == ChatFinishReason.ContentFilter) { - return new ToolCallMessageUpdate(tooCallUpdate.Name ?? currentToolName, tooCallUpdate.ArgumentsUpdate, from: update.From); + throw new InvalidOperationException("The content is filtered because its potential risk. Please try another input."); } - else + + // throw exception is there is more than on choice + if (message.Content.Content.Count > 1) { - return null; + throw new InvalidOperationException("The content has more than one choice. Please try another input."); } + + return PostProcessChatResponseMessage(message.Content, message.From); } - private IMessage PostProcessChatCompletions(IMessage message) + private IMessage PostProcessChatResponseMessage(ChatCompletion chatCompletion, string? from) { // throw exception if prompt filter results is not null - if (message.Content.Choices[0].FinishReason == CompletionsFinishReason.ContentFiltered) + if (chatCompletion.FinishReason == ChatFinishReason.ContentFilter) { throw new InvalidOperationException("The content is filtered because its potential risk. Please try another input."); } - return PostProcessChatResponseMessage(message.Content.Choices[0].Message, message.From); - } + // throw exception is there is more than on choice + if (chatCompletion.Content.Count > 1) + { + throw new InvalidOperationException("The content has more than one choice. Please try another input."); + } + var textContent = chatCompletion.Content.FirstOrDefault(); - private IMessage PostProcessChatResponseMessage(ChatResponseMessage chatResponseMessage, string? from) - { - var textContent = chatResponseMessage.Content; - if (chatResponseMessage.FunctionCall is FunctionCall functionCall) + // if tool calls is not empty, return ToolCallMessage + if (chatCompletion.ToolCalls is { Count: > 0 }) { - return new ToolCallMessage(functionCall.Name, functionCall.Arguments, from) + var toolCalls = chatCompletion.ToolCalls.Select(tc => new ToolCall(tc.FunctionName, tc.FunctionArguments) { ToolCallId = tc.Id }); + return new ToolCallMessage(toolCalls, from) { - Content = textContent, + Content = textContent?.Kind switch + { + _ when textContent?.Kind == ChatMessageContentPartKind.Text => textContent.Text, + _ => null, + }, }; } - if (chatResponseMessage.ToolCalls.Where(tc => tc is ChatCompletionsFunctionToolCall).Any()) + // else, process function call. + // This is deprecated and will be removed in the future. + if (chatCompletion.FunctionCall is ChatFunctionCall fc) { - var functionToolCalls = chatResponseMessage.ToolCalls - .Where(tc => tc is ChatCompletionsFunctionToolCall) - .Select(tc => (ChatCompletionsFunctionToolCall)tc); - - var toolCalls = functionToolCalls.Select(tc => new ToolCall(tc.Name, tc.Arguments) { ToolCallId = tc.Id }); - - return new ToolCallMessage(toolCalls, from) + return new ToolCallMessage(fc.FunctionName, fc.FunctionArguments, from) { - Content = textContent, + Content = textContent?.Kind switch + { + _ when textContent?.Kind == ChatMessageContentPartKind.Text => textContent.Text, + _ => null, + }, }; } - if (textContent is string content && !string.IsNullOrEmpty(content)) + // if the content is text, return TextMessage + if (textContent?.Kind == ChatMessageContentPartKind.Text) { - return new TextMessage(Role.Assistant, content, from); + return new TextMessage(Role.Assistant, textContent.Text, from); } throw new InvalidOperationException("Invalid ChatResponseMessage"); @@ -171,7 +223,7 @@ public IEnumerable ProcessIncomingMessages(IAgent agent, IEnumerable(m => { - if (m is IMessage crm) + if (m is IMessage crm) { return [crm]; } @@ -185,9 +237,6 @@ MultiModalMessage multiModalMessage when (multiModalMessage.From is null || mult ToolCallMessage toolCallMessage when (toolCallMessage.From is null || toolCallMessage.From == agent.Name) => ProcessToolCallMessage(agent, toolCallMessage), ToolCallResultMessage toolCallResultMessage => ProcessToolCallResultMessage(toolCallResultMessage), AggregateMessage aggregateMessage => ProcessFunctionCallMiddlewareMessage(agent, aggregateMessage), -#pragma warning disable CS0618 // deprecated - Message msg => ProcessMessage(agent, msg), -#pragma warning restore CS0618 // deprecated _ when strictMode is false => [], _ => throw new InvalidOperationException($"Invalid message type: {m.GetType().Name}"), }; @@ -204,92 +253,30 @@ ToolCallMessage toolCallMessage when (toolCallMessage.From is null || toolCallMe }); } - [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] - private IEnumerable ProcessIncomingMessagesForSelf(Message message) + private IEnumerable ProcessTextMessage(IAgent agent, TextMessage message) { if (message.Role == Role.System) { - return new[] { new ChatRequestSystemMessage(message.Content) }; - } - else if (message.Content is string content && content is { Length: > 0 }) - { - if (message.FunctionName is null) - { - return new[] { new ChatRequestAssistantMessage(message.Content) }; - } - else - { - return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; - } - } - else if (message.FunctionName is string functionName) - { - var msg = new ChatRequestAssistantMessage(content: null) - { - FunctionCall = new FunctionCall(functionName, message.FunctionArguments) - }; - - return new[] - { - msg, - }; - } - else - { - throw new InvalidOperationException("Invalid Message as message from self."); - } - } - - [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] - private IEnumerable ProcessIncomingMessagesForOther(Message message) - { - if (message.Role == Role.System) - { - return [new ChatRequestSystemMessage(message.Content) { Name = message.From }]; - } - else if (message.Content is string content && content is { Length: > 0 }) - { - if (message.FunctionName is not null) - { - return new[] { new ChatRequestToolMessage(content, message.FunctionName) }; - } - - return [new ChatRequestUserMessage(message.Content) { Name = message.From }]; - } - else if (message.FunctionName is string _) - { - return [new ChatRequestUserMessage("// Message type is not supported") { Name = message.From }]; - } - else - { - throw new InvalidOperationException("Invalid Message as message from other."); - } - } - - private IEnumerable ProcessTextMessage(IAgent agent, TextMessage message) - { - if (message.Role == Role.System) - { - return [new ChatRequestSystemMessage(message.Content) { Name = message.From }]; + return [new SystemChatMessage(message.Content) { ParticipantName = message.From }]; } if (agent.Name == message.From) { - return [new ChatRequestAssistantMessage(message.Content) { Name = agent.Name }]; + return [new AssistantChatMessage(message.Content) { ParticipantName = agent.Name }]; } else { return message.From switch { - null when message.Role == Role.User => [new ChatRequestUserMessage(message.Content)], - null when message.Role == Role.Assistant => [new ChatRequestAssistantMessage(message.Content)], + null when message.Role == Role.User => [new UserChatMessage(message.Content)], + null when message.Role == Role.Assistant => [new AssistantChatMessage(message.Content)], null => throw new InvalidOperationException("Invalid Role"), - _ => [new ChatRequestUserMessage(message.Content) { Name = message.From }] + _ => [new UserChatMessage(message.Content) { ParticipantName = message.From }] }; } } - private IEnumerable ProcessImageMessage(IAgent agent, ImageMessage message) + private IEnumerable ProcessImageMessage(IAgent agent, ImageMessage message) { if (agent.Name == message.From) { @@ -298,10 +285,10 @@ private IEnumerable ProcessImageMessage(IAgent agent, ImageM } var imageContentItem = this.CreateChatMessageImageContentItemFromImageMessage(message); - return [new ChatRequestUserMessage([imageContentItem]) { Name = message.From }]; + return [new UserChatMessage([imageContentItem]) { ParticipantName = message.From }]; } - private IEnumerable ProcessMultiModalMessage(IAgent agent, MultiModalMessage message) + private IEnumerable ProcessMultiModalMessage(IAgent agent, MultiModalMessage message) { if (agent.Name == message.From) { @@ -309,69 +296,56 @@ private IEnumerable ProcessMultiModalMessage(IAgent agent, M throw new ArgumentException("MultiModalMessage is not supported when message.From is the same with agent"); } - IEnumerable items = message.Content.Select(ci => ci switch + IEnumerable items = message.Content.Select(ci => ci switch { - TextMessage text => new ChatMessageTextContentItem(text.Content), + TextMessage text => ChatMessageContentPart.CreateTextMessageContentPart(text.Content), ImageMessage image => this.CreateChatMessageImageContentItemFromImageMessage(image), _ => throw new NotImplementedException(), }); - return [new ChatRequestUserMessage(items) { Name = message.From }]; + return [new UserChatMessage(items) { ParticipantName = message.From }]; } - private ChatMessageImageContentItem CreateChatMessageImageContentItemFromImageMessage(ImageMessage message) + private ChatMessageContentPart CreateChatMessageImageContentItemFromImageMessage(ImageMessage message) { return message.Data is null && message.Url is not null - ? new ChatMessageImageContentItem(new Uri(message.Url)) - : new ChatMessageImageContentItem(message.Data, message.Data?.MediaType); + ? ChatMessageContentPart.CreateImageMessageContentPart(new Uri(message.Url)) + : ChatMessageContentPart.CreateImageMessageContentPart(message.Data, message.Data?.MediaType); } - private IEnumerable ProcessToolCallMessage(IAgent agent, ToolCallMessage message) + private IEnumerable ProcessToolCallMessage(IAgent agent, ToolCallMessage message) { if (message.From is not null && message.From != agent.Name) { throw new ArgumentException("ToolCallMessage is not supported when message.From is not the same with agent"); } - var toolCall = message.ToolCalls.Select((tc, i) => new ChatCompletionsFunctionToolCall(tc.ToolCallId ?? $"{tc.FunctionName}_{i}", tc.FunctionName, tc.FunctionArguments)); - var textContent = message.GetContent() ?? string.Empty; - var chatRequestMessage = new ChatRequestAssistantMessage(textContent) { Name = message.From }; - foreach (var tc in toolCall) - { - chatRequestMessage.ToolCalls.Add(tc); - } + var toolCallParts = message.ToolCalls.Select((tc, i) => ChatToolCall.CreateFunctionToolCall(tc.ToolCallId ?? $"{tc.FunctionName}_{i}", tc.FunctionName, tc.FunctionArguments)); + var textContent = message.GetContent() ?? null; + + // Don't set participant name for assistant when it is tool call + // fix https://github.com/microsoft/autogen/issues/3437 + var chatRequestMessage = new AssistantChatMessage(toolCallParts, textContent); return [chatRequestMessage]; } - private IEnumerable ProcessToolCallResultMessage(ToolCallResultMessage message) + private IEnumerable ProcessToolCallResultMessage(ToolCallResultMessage message) { return message.ToolCalls .Where(tc => tc.Result is not null) - .Select((tc, i) => new ChatRequestToolMessage(tc.Result, tc.ToolCallId ?? $"{tc.FunctionName}_{i}")); + .Select((tc, i) => new ToolChatMessage(tc.ToolCallId ?? $"{tc.FunctionName}_{i}", tc.Result)); } - [Obsolete("This method is deprecated, please use ProcessIncomingMessages(IAgent agent, IEnumerable messages) instead.")] - private IEnumerable ProcessMessage(IAgent agent, Message message) - { - if (message.From is not null && message.From != agent.Name) - { - return ProcessIncomingMessagesForOther(message); - } - else - { - return ProcessIncomingMessagesForSelf(message); - } - } - private IEnumerable ProcessFunctionCallMiddlewareMessage(IAgent agent, AggregateMessage aggregateMessage) + private IEnumerable ProcessFunctionCallMiddlewareMessage(IAgent agent, AggregateMessage aggregateMessage) { if (aggregateMessage.From is not null && aggregateMessage.From != agent.Name) { // convert as user message var resultMessage = aggregateMessage.Message2; - return resultMessage.ToolCalls.Select(tc => new ChatRequestUserMessage(tc.Result) { Name = aggregateMessage.From }); + return resultMessage.ToolCalls.Select(tc => new UserChatMessage(tc.Result) { ParticipantName = aggregateMessage.From }); } else { diff --git a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj index 3bd96f93b687..b89626c01a06 100644 --- a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj +++ b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen.SemanticKernel $(NoWarn);SKEXP0110 @@ -17,9 +17,9 @@ - + diff --git a/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs index 6ce242eb1abe..a055c0afcb6a 100644 --- a/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs +++ b/dotnet/src/AutoGen.SemanticKernel/Middleware/SemanticKernelChatMessageContentConnector.cs @@ -47,7 +47,7 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, return PostProcessMessage(reply); } - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var chatMessageContents = ProcessMessage(context.Messages, agent) .Select(m => new MessageEnvelope(m)); @@ -67,11 +67,11 @@ private IMessage PostProcessMessage(IMessage input) }; } - private IStreamingMessage PostProcessStreamingMessage(IStreamingMessage input) + private IMessage PostProcessStreamingMessage(IMessage input) { return input switch { - IStreamingMessage streamingMessage => PostProcessMessage(streamingMessage), + IMessage streamingMessage => PostProcessMessage(streamingMessage), IMessage msg => PostProcessMessage(msg), _ => input, }; @@ -98,7 +98,7 @@ private IMessage PostProcessMessage(IMessage messageEnvelope } } - private IStreamingMessage PostProcessMessage(IStreamingMessage streamingMessage) + private IMessage PostProcessMessage(IMessage streamingMessage) { var chatMessageContent = streamingMessage.Content; if (chatMessageContent.ChoiceIndex > 0) diff --git a/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs index 21f652f56c4f..e10f5b043f24 100644 --- a/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs +++ b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelAgent.cs @@ -65,7 +65,7 @@ public async Task GenerateReplyAsync(IEnumerable messages, G return new MessageEnvelope(reply.First(), from: this.Name); } - public async IAsyncEnumerable GenerateStreamingReplyAsync( + public async IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -106,7 +106,6 @@ private PromptExecutionSettings BuildOption(GenerateReplyOptions? options) MaxTokens = options?.MaxToken ?? 1024, StopSequences = options?.StopSequence, ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, - ResultsPerPrompt = 1, }; } diff --git a/dotnet/src/AutoGen.SemanticKernel/SemanticKernelChatCompletionAgent.cs b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelChatCompletionAgent.cs index 82d83a9e8556..1354996430bb 100644 --- a/dotnet/src/AutoGen.SemanticKernel/SemanticKernelChatCompletionAgent.cs +++ b/dotnet/src/AutoGen.SemanticKernel/SemanticKernelChatCompletionAgent.cs @@ -27,7 +27,7 @@ public async Task GenerateReplyAsync(IEnumerable messages, G CancellationToken cancellationToken = default) { ChatMessageContent[] reply = await _chatCompletionAgent - .InvokeAsync(BuildChatHistory(messages), cancellationToken) + .InvokeAsync(BuildChatHistory(messages), cancellationToken: cancellationToken) .ToArrayAsync(cancellationToken: cancellationToken); return reply.Length > 1 diff --git a/dotnet/src/AutoGen.SourceGenerator/SourceGeneratorFunctionContract.cs b/dotnet/src/AutoGen.SourceGenerator/SourceGeneratorFunctionContract.cs index 24e42affa3bd..aa4980379f4f 100644 --- a/dotnet/src/AutoGen.SourceGenerator/SourceGeneratorFunctionContract.cs +++ b/dotnet/src/AutoGen.SourceGenerator/SourceGeneratorFunctionContract.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// FunctionContract.cs +// SourceGeneratorFunctionContract.cs namespace AutoGen.SourceGenerator { diff --git a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs index 40adbdcde47c..b90d78be3f19 100644 --- a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs +++ b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.cs @@ -36,7 +36,6 @@ public virtual string TransformText() using System.Threading.Tasks; using System; using AutoGen.Core; -using AutoGen.OpenAI.Extension; "); if (!String.IsNullOrEmpty(NameSpace)) { @@ -107,7 +106,7 @@ public virtual string TransformText() } if (functionContract.Description != null) { this.Write(" Description = @\""); - this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Description)); + this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.Description.Replace("\"", "\"\""))); this.Write("\",\r\n"); } if (functionContract.ReturnType != null) { @@ -132,7 +131,7 @@ public virtual string TransformText() } if (parameter.Description != null) { this.Write(" Description = @\""); - this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Description)); + this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Description.Replace("\"", "\"\""))); this.Write("\",\r\n"); } if (parameter.Type != null) { @@ -152,12 +151,7 @@ public virtual string TransformText() } this.Write(" },\r\n"); } - this.Write(" };\r\n }\r\n\r\n public global::Azure.AI.OpenAI.FunctionDefin" + - "ition "); - this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionDefinitionName())); - this.Write("\r\n {\r\n get => this."); - this.Write(this.ToStringHelper.ToStringWithCulture(functionContract.GetFunctionContractName())); - this.Write(".ToOpenAIFunctionDefinition();\r\n }\r\n"); + this.Write(" };\r\n }\r\n"); } this.Write(" }\r\n"); if (!String.IsNullOrEmpty(NameSpace)) { diff --git a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt index 0d1b221c35c8..e7ed476fde8b 100644 --- a/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt +++ b/dotnet/src/AutoGen.SourceGenerator/Template/FunctionCallTemplate.tt @@ -13,7 +13,6 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using System; using AutoGen.Core; -using AutoGen.OpenAI.Extension; <#if (!String.IsNullOrEmpty(NameSpace)) {#> namespace <#=NameSpace#> @@ -63,7 +62,7 @@ namespace <#=NameSpace#> Name = @"<#=functionContract.Name#>", <#}#> <#if (functionContract.Description != null) {#> - Description = @"<#=functionContract.Description#>", + Description = @"<#=functionContract.Description.Replace("\"", "\"\"")#>", <#}#> <#if (functionContract.ReturnType != null) {#> ReturnType = typeof(<#=functionContract.ReturnType#>), @@ -81,7 +80,7 @@ namespace <#=NameSpace#> Name = @"<#=parameter.Name#>", <#}#> <#if (parameter.Description != null) {#> - Description = @"<#=parameter.Description#>", + Description = @"<#= parameter.Description.Replace("\"", "\"\"") #>", <#}#> <#if (parameter.Type != null) {#> ParameterType = typeof(<#=parameter.Type#>), @@ -96,11 +95,6 @@ namespace <#=NameSpace#> <#}#> }; } - - public global::Azure.AI.OpenAI.FunctionDefinition <#=functionContract.GetFunctionDefinitionName()#> - { - get => this.<#=functionContract.GetFunctionContractName()#>.ToOpenAIFunctionDefinition(); - } <#}#> } <#if (!String.IsNullOrEmpty(NameSpace)) {#> diff --git a/dotnet/src/AutoGen.WebAPI/AutoGen.WebAPI.csproj b/dotnet/src/AutoGen.WebAPI/AutoGen.WebAPI.csproj new file mode 100644 index 000000000000..c5b720764761 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/AutoGen.WebAPI.csproj @@ -0,0 +1,27 @@ + + + + net6.0;net8.0 + true + $(NoWarn);CS1591;CS1573 + + + + + + + + AutoGen.WebAPI + + Turn an `AutoGen.Core.IAgent` into a RESTful API. + + + + + + + + + + + diff --git a/dotnet/src/AutoGen.WebAPI/Extension.cs b/dotnet/src/AutoGen.WebAPI/Extension.cs new file mode 100644 index 000000000000..c8534e43e540 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/Extension.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Extension.cs + +using AutoGen.Core; +using Microsoft.AspNetCore.Builder; + +namespace AutoGen.WebAPI; + +public static class Extension +{ + /// + /// Serve the agent as an OpenAI chat completion endpoint using . + /// If the request path is /v1/chat/completions and model name is the same as the agent name, + /// the request will be handled by the agent. + /// otherwise, the request will be passed to the next middleware. + /// + /// application builder + /// + public static IApplicationBuilder UseAgentAsOpenAIChatCompletionEndpoint(this IApplicationBuilder app, IAgent agent) + { + var middleware = new OpenAIChatCompletionMiddleware(agent); + return app.Use(middleware.InvokeAsync); + } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/Converter/OpenAIMessageConverter.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/Converter/OpenAIMessageConverter.cs new file mode 100644 index 000000000000..888a0f8dd8c8 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/Converter/OpenAIMessageConverter.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIMessageConverter.cs + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIMessageConverter : JsonConverter +{ + public override OpenAIMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument document = JsonDocument.ParseValue(ref reader); + var root = document.RootElement; + var role = root.GetProperty("role").GetString(); + var contentDocument = root.GetProperty("content"); + var isContentDocumentString = contentDocument.ValueKind == JsonValueKind.String; + switch (role) + { + case "system": + return JsonSerializer.Deserialize(root.GetRawText()) ?? throw new JsonException(); + case "user" when isContentDocumentString: + return JsonSerializer.Deserialize(root.GetRawText()) ?? throw new JsonException(); + case "user" when !isContentDocumentString: + return JsonSerializer.Deserialize(root.GetRawText()) ?? throw new JsonException(); + case "assistant": + return JsonSerializer.Deserialize(root.GetRawText()) ?? throw new JsonException(); + case "tool": + return JsonSerializer.Deserialize(root.GetRawText()) ?? throw new JsonException(); + default: + throw new JsonException(); + } + } + + public override void Write(Utf8JsonWriter writer, OpenAIMessage value, JsonSerializerOptions options) + { + switch (value) + { + case OpenAISystemMessage systemMessage: + JsonSerializer.Serialize(writer, systemMessage, options); + break; + case OpenAIUserMessage userMessage: + JsonSerializer.Serialize(writer, userMessage, options); + break; + case OpenAIAssistantMessage assistantMessage: + JsonSerializer.Serialize(writer, assistantMessage, options); + break; + case OpenAIToolMessage toolMessage: + JsonSerializer.Serialize(writer, toolMessage, options); + break; + default: + throw new JsonException(); + } + } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIAssistantMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIAssistantMessage.cs new file mode 100644 index 000000000000..bfd090358453 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIAssistantMessage.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIAssistantMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIAssistantMessage : OpenAIMessage +{ + [JsonPropertyName("role")] + public override string? Role { get; } = "assistant"; + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("tool_calls")] + public OpenAIToolCallObject[]? ToolCalls { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletion.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletion.cs new file mode 100644 index 000000000000..041f4cfc848c --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletion.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletion.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIChatCompletion +{ + [JsonPropertyName("id")] + public string? ID { get; set; } + + [JsonPropertyName("created")] + public long Created { get; set; } + + [JsonPropertyName("choices")] + public OpenAIChatCompletionChoice[]? Choices { get; set; } + + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("system_fingerprint")] + public string? SystemFingerprint { get; set; } + + [JsonPropertyName("object")] + public string Object { get; set; } = "chat.completion"; + + [JsonPropertyName("usage")] + public OpenAIChatCompletionUsage? Usage { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionChoice.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionChoice.cs new file mode 100644 index 000000000000..35b6fce59a8e --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionChoice.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionChoice.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIChatCompletionChoice +{ + [JsonPropertyName("finish_reason")] + public string? FinishReason { get; set; } + + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("message")] + public OpenAIChatCompletionMessage? Message { get; set; } + + [JsonPropertyName("delta")] + public OpenAIChatCompletionMessage? Delta { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionMessage.cs new file mode 100644 index 000000000000..de6be0dbf7a5 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionMessage.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIChatCompletionMessage +{ + [JsonPropertyName("role")] + public string Role { get; } = "assistant"; + + [JsonPropertyName("content")] + public string? Content { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionOption.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionOption.cs new file mode 100644 index 000000000000..0b9137d43a39 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionOption.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionOption.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIChatCompletionOption +{ + [JsonPropertyName("messages")] + public OpenAIMessage[]? Messages { get; set; } + + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("max_tokens")] + public int? MaxTokens { get; set; } + + [JsonPropertyName("temperature")] + public float Temperature { get; set; } = 1; + + /// + /// If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message + /// + [JsonPropertyName("stream")] + public bool? Stream { get; set; } = false; + + [JsonPropertyName("stream_options")] + public OpenAIStreamOptions? StreamOptions { get; set; } + + [JsonPropertyName("stop")] + public string[]? Stop { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionUsage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionUsage.cs new file mode 100644 index 000000000000..f196ccb842ea --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIChatCompletionUsage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionUsage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIChatCompletionUsage +{ + [JsonPropertyName("completion_tokens")] + public int CompletionTokens { get; set; } + + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIImageUrlObject.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIImageUrlObject.cs new file mode 100644 index 000000000000..a50012c9fed1 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIImageUrlObject.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIImageUrlObject.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIImageUrlObject +{ + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("detail")] + public string? Detail { get; set; } = "auto"; +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIMessage.cs new file mode 100644 index 000000000000..deb729b72003 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIMessage.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +[JsonConverter(typeof(OpenAIMessageConverter))] +internal abstract class OpenAIMessage +{ + [JsonPropertyName("role")] + public abstract string? Role { get; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIStreamOptions.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIStreamOptions.cs new file mode 100644 index 000000000000..e95991388b7f --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIStreamOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIStreamOptions.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIStreamOptions +{ + [JsonPropertyName("include_usage")] + public bool? IncludeUsage { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAISystemMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAISystemMessage.cs new file mode 100644 index 000000000000..f29b10826c4f --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAISystemMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAISystemMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAISystemMessage : OpenAIMessage +{ + [JsonPropertyName("role")] + public override string? Role { get; } = "system"; + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolCallObject.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolCallObject.cs new file mode 100644 index 000000000000..f3fc37f9c44f --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolCallObject.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIToolCallObject.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIToolCallObject +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolMessage.cs new file mode 100644 index 000000000000..0c84c164cd96 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIToolMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIToolMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIToolMessage : OpenAIMessage +{ + [JsonPropertyName("role")] + public override string? Role { get; } = "tool"; + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("tool_call_id")] + public string? ToolCallId { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserImageContent.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserImageContent.cs new file mode 100644 index 000000000000..28b83ffb3058 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserImageContent.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIUserImageContent.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIUserImageContent : OpenAIUserMessageItem +{ + [JsonPropertyName("type")] + public override string MessageType { get; } = "image"; + + [JsonPropertyName("image_url")] + public string? Url { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessage.cs new file mode 100644 index 000000000000..b5f1e7c50c12 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIUserMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIUserMessage : OpenAIMessage +{ + [JsonPropertyName("role")] + public override string? Role { get; } = "user"; + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessageItem.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessageItem.cs new file mode 100644 index 000000000000..94e7d91534a5 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMessageItem.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIUserMessageItem.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal abstract class OpenAIUserMessageItem +{ + [JsonPropertyName("type")] + public abstract string MessageType { get; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMultiModalMessage.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMultiModalMessage.cs new file mode 100644 index 000000000000..789df5afaaae --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserMultiModalMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIUserMultiModalMessage.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIUserMultiModalMessage : OpenAIMessage +{ + [JsonPropertyName("role")] + public override string? Role { get; } = "user"; + + [JsonPropertyName("content")] + public OpenAIUserMessageItem[]? Content { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserTextContent.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserTextContent.cs new file mode 100644 index 000000000000..d22d5aa4c7f3 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/DTO/OpenAIUserTextContent.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIUserTextContent.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.WebAPI.OpenAI.DTO; + +internal class OpenAIUserTextContent : OpenAIUserMessageItem +{ + [JsonPropertyName("type")] + public override string MessageType { get; } = "text"; + + [JsonPropertyName("text")] + public string? Content { get; set; } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAI/Service/OpenAIChatCompletionService.cs b/dotnet/src/AutoGen.WebAPI/OpenAI/Service/OpenAIChatCompletionService.cs new file mode 100644 index 000000000000..80d49050ee48 --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAI/Service/OpenAIChatCompletionService.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionService.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.Core; +using AutoGen.WebAPI.OpenAI.DTO; +namespace AutoGen.Server; + +internal class OpenAIChatCompletionService +{ + private readonly IAgent agent; + + public OpenAIChatCompletionService(IAgent agent) + { + this.agent = agent; + } + + public async Task GetChatCompletionAsync(OpenAIChatCompletionOption request) + { + var messages = this.ProcessMessages(request.Messages ?? Array.Empty()); + + var generateOption = this.ProcessReplyOptions(request); + + var reply = await this.agent.GenerateReplyAsync(messages, generateOption); + + var openAIChatCompletion = new OpenAIChatCompletion() + { + Created = DateTimeOffset.UtcNow.Ticks / TimeSpan.TicksPerMillisecond / 1000, + Model = this.agent.Name, + }; + + if (reply.GetContent() is string content) + { + var message = new OpenAIChatCompletionMessage() + { + Content = content, + }; + + var choice = new OpenAIChatCompletionChoice() + { + Message = message, + Index = 0, + FinishReason = "stop", + }; + + openAIChatCompletion.Choices = [choice]; + + return openAIChatCompletion; + } + + throw new NotImplementedException("Unsupported reply content type"); + } + + public async IAsyncEnumerable GetStreamingChatCompletionAsync(OpenAIChatCompletionOption request) + { + if (this.agent is IStreamingAgent streamingAgent) + { + var messages = this.ProcessMessages(request.Messages ?? Array.Empty()); + + var generateOption = this.ProcessReplyOptions(request); + + await foreach (var reply in streamingAgent.GenerateStreamingReplyAsync(messages, generateOption)) + { + var openAIChatCompletion = new OpenAIChatCompletion() + { + Created = DateTimeOffset.UtcNow.Ticks / TimeSpan.TicksPerMillisecond / 1000, + Model = this.agent.Name, + }; + + if (reply.GetContent() is string content) + { + var message = new OpenAIChatCompletionMessage() + { + Content = content, + }; + + var choice = new OpenAIChatCompletionChoice() + { + Delta = message, + Index = 0, + }; + + openAIChatCompletion.Choices = [choice]; + + yield return openAIChatCompletion; + } + else + { + throw new NotImplementedException("Unsupported reply content type"); + } + } + + var doneMessage = new OpenAIChatCompletion() + { + Created = DateTimeOffset.UtcNow.Ticks / TimeSpan.TicksPerMillisecond / 1000, + Model = this.agent.Name, + }; + + var doneChoice = new OpenAIChatCompletionChoice() + { + FinishReason = "stop", + Index = 0, + }; + + doneMessage.Choices = [doneChoice]; + + yield return doneMessage; + } + else + { + yield return await this.GetChatCompletionAsync(request); + } + } + + private IEnumerable ProcessMessages(IEnumerable messages) + { + return messages.Select(m => m switch + { + OpenAISystemMessage systemMessage when systemMessage.Content is string content => new TextMessage(Role.System, content, this.agent.Name), + OpenAIUserMessage userMessage when userMessage.Content is string content => new TextMessage(Role.User, content, this.agent.Name), + OpenAIAssistantMessage assistantMessage when assistantMessage.Content is string content => new TextMessage(Role.Assistant, content, this.agent.Name), + OpenAIUserMultiModalMessage userMultiModalMessage when userMultiModalMessage.Content is { Length: > 0 } => this.CreateMultiModaMessageFromOpenAIUserMultiModalMessage(userMultiModalMessage), + _ => throw new ArgumentException($"Unsupported message type {m.GetType()}") + }); + } + + private GenerateReplyOptions ProcessReplyOptions(OpenAIChatCompletionOption request) + { + return new GenerateReplyOptions() + { + Temperature = request.Temperature, + MaxToken = request.MaxTokens, + StopSequence = request.Stop, + }; + } + + private MultiModalMessage CreateMultiModaMessageFromOpenAIUserMultiModalMessage(OpenAIUserMultiModalMessage message) + { + if (message.Content is null) + { + throw new ArgumentNullException(nameof(message.Content)); + } + + IEnumerable items = message.Content.Select(item => item switch + { + OpenAIUserImageContent imageContent when imageContent.Url is string url => new ImageMessage(Role.User, url, this.agent.Name), + OpenAIUserTextContent textContent when textContent.Content is string content => new TextMessage(Role.User, content, this.agent.Name), + _ => throw new ArgumentException($"Unsupported content type {item.GetType()}") + }); + + return new MultiModalMessage(Role.User, items, this.agent.Name); + } +} diff --git a/dotnet/src/AutoGen.WebAPI/OpenAIChatCompletionMiddleware.cs b/dotnet/src/AutoGen.WebAPI/OpenAIChatCompletionMiddleware.cs new file mode 100644 index 000000000000..53b3699fd62e --- /dev/null +++ b/dotnet/src/AutoGen.WebAPI/OpenAIChatCompletionMiddleware.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionMiddleware.cs + +using System.Text.Json; +using System.Threading.Tasks; +using AutoGen.Core; +using AutoGen.Server; +using AutoGen.WebAPI.OpenAI.DTO; +using Microsoft.AspNetCore.Http; + +namespace AutoGen.WebAPI; + +public class OpenAIChatCompletionMiddleware : Microsoft.AspNetCore.Http.IMiddleware +{ + private readonly IAgent _agent; + private readonly OpenAIChatCompletionService chatCompletionService; + + public OpenAIChatCompletionMiddleware(IAgent agent) + { + _agent = agent; + chatCompletionService = new OpenAIChatCompletionService(_agent); + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + // if HttpPost and path is /v1/chat/completions + // get the request body + // call chatCompletionService.GetChatCompletionAsync(request) + // return the response + + // else + // call next middleware + if (context.Request.Method == HttpMethods.Post && context.Request.Path == "/v1/chat/completions") + { + context.Request.EnableBuffering(); + var body = await context.Request.ReadFromJsonAsync(); + context.Request.Body.Position = 0; + if (body is null) + { + // return 400 Bad Request + context.Response.StatusCode = 400; + return; + } + + if (body.Model != _agent.Name) + { + await next(context); + return; + } + + if (body.Stream is true) + { + // Send as server side events + context.Response.Headers.Append("Content-Type", "text/event-stream"); + context.Response.Headers.Append("Cache-Control", "no-cache"); + context.Response.Headers.Append("Connection", "keep-alive"); + await foreach (var chatCompletion in chatCompletionService.GetStreamingChatCompletionAsync(body)) + { + if (chatCompletion?.Choices?[0].FinishReason is "stop") + { + // the stream is done + // send Data: [DONE]\n\n + await context.Response.WriteAsync("data: [DONE]\n\n"); + break; + } + else + { + // remove null + var option = new JsonSerializerOptions + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + var data = JsonSerializer.Serialize(chatCompletion, option); + await context.Response.WriteAsync($"data: {data}\n\n"); + } + } + + return; + } + else + { + var chatCompletion = await chatCompletionService.GetChatCompletionAsync(body); + await context.Response.WriteAsJsonAsync(chatCompletion); + return; + } + } + else + { + await next(context); + } + } +} diff --git a/dotnet/src/AutoGen/API/LLMConfigAPI.cs b/dotnet/src/AutoGen/API/LLMConfigAPI.cs index 5154f3dd5f55..28b5ad44312f 100644 --- a/dotnet/src/AutoGen/API/LLMConfigAPI.cs +++ b/dotnet/src/AutoGen/API/LLMConfigAPI.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using AutoGen.OpenAI; namespace AutoGen { diff --git a/dotnet/src/AutoGen/Agent/ConversableAgent.cs b/dotnet/src/AutoGen/Agent/ConversableAgent.cs index fe1470502022..da61c812f464 100644 --- a/dotnet/src/AutoGen/Agent/ConversableAgent.cs +++ b/dotnet/src/AutoGen/Agent/ConversableAgent.cs @@ -6,9 +6,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using AutoGen.LMStudio; using AutoGen.OpenAI; - +using AutoGen.OpenAI.Extension; namespace AutoGen; public enum HumanInputMode @@ -87,13 +86,21 @@ public ConversableAgent( { IAgent nextAgent = llmConfig switch { - AzureOpenAIConfig azureConfig => new GPTAgent(this.Name!, this.systemMessage, azureConfig, temperature: config.Temperature ?? 0), - OpenAIConfig openAIConfig => new GPTAgent(this.Name!, this.systemMessage, openAIConfig, temperature: config.Temperature ?? 0), - LMStudioConfig lmStudioConfig => new LMStudioAgent( - name: this.Name, - config: lmStudioConfig, - systemMessage: this.systemMessage, - temperature: config.Temperature ?? 0), + AzureOpenAIConfig azureConfig => new OpenAIChatAgent( + chatClient: azureConfig.CreateChatClient(), + name: this.Name!, + systemMessage: this.systemMessage) + .RegisterMessageConnector(), + OpenAIConfig openAIConfig => new OpenAIChatAgent( + chatClient: openAIConfig.CreateChatClient(), + name: this.Name!, + systemMessage: this.systemMessage) + .RegisterMessageConnector(), + LMStudioConfig lmStudioConfig => new OpenAIChatAgent( + chatClient: lmStudioConfig.CreateChatClient(), + name: this.Name!, + systemMessage: this.systemMessage) + .RegisterMessageConnector(), _ => throw new ArgumentException($"Unsupported config type {llmConfig.GetType()}"), }; diff --git a/dotnet/src/AutoGen/AutoGen.csproj b/dotnet/src/AutoGen/AutoGen.csproj index ddc34a071cbf..fe4431a35731 100644 --- a/dotnet/src/AutoGen/AutoGen.csproj +++ b/dotnet/src/AutoGen/AutoGen.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + $(PackageTargetFrameworks) AutoGen @@ -15,7 +15,8 @@ - + + @@ -26,6 +27,7 @@ + diff --git a/dotnet/src/AutoGen/AzureOpenAIConfig.cs b/dotnet/src/AutoGen/AzureOpenAIConfig.cs new file mode 100644 index 000000000000..6112a3815d59 --- /dev/null +++ b/dotnet/src/AutoGen/AzureOpenAIConfig.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AzureOpenAIConfig.cs + +using Azure.AI.OpenAI; +using OpenAI.Chat; + +namespace AutoGen; + +public class AzureOpenAIConfig : ILLMConfig +{ + public AzureOpenAIConfig(string endpoint, string deploymentName, string apiKey) + { + this.Endpoint = endpoint; + this.DeploymentName = deploymentName; + this.ApiKey = apiKey; + } + + public string Endpoint { get; } + + public string DeploymentName { get; } + + public string ApiKey { get; } + + internal ChatClient CreateChatClient() + { + var client = new AzureOpenAIClient(new System.Uri(this.Endpoint), this.ApiKey); + + return client.GetChatClient(DeploymentName); + } +} diff --git a/dotnet/src/AutoGen/LMStudioConfig.cs b/dotnet/src/AutoGen/LMStudioConfig.cs new file mode 100644 index 000000000000..5fd9edc70802 --- /dev/null +++ b/dotnet/src/AutoGen/LMStudioConfig.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// LMStudioConfig.cs +using System; +using OpenAI; +using OpenAI.Chat; + +namespace AutoGen; + +/// +/// Add support for consuming openai-like API from LM Studio +/// +public class LMStudioConfig : ILLMConfig +{ + public LMStudioConfig(string host, int port) + { + this.Host = host; + this.Port = port; + this.Uri = new Uri($"http://{host}:{port}"); + } + + public LMStudioConfig(Uri uri) + { + this.Uri = uri; + this.Host = uri.Host; + this.Port = uri.Port; + } + + public string Host { get; } + + public int Port { get; } + + public Uri Uri { get; } + + internal ChatClient CreateChatClient() + { + var client = new OpenAIClient("api-key", new OpenAIClientOptions + { + Endpoint = this.Uri, + }); + + // model name doesn't matter for LM Studio + + return client.GetChatClient("model-name"); + } +} diff --git a/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs b/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs index 1a742b11c799..eda3c001a249 100644 --- a/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs +++ b/dotnet/src/AutoGen/Middleware/HumanInputMiddleware.cs @@ -18,7 +18,7 @@ public class HumanInputMiddleware : IMiddleware private readonly string prompt; private readonly string exitKeyword; private Func, CancellationToken, Task> isTermination; - private Func getInput = Console.ReadLine; + private Func getInput = Console.ReadLine; private Action writeLine = Console.WriteLine; public string? Name => nameof(HumanInputMiddleware); @@ -27,7 +27,7 @@ public HumanInputMiddleware( string exitKeyword = "exit", HumanInputMode mode = HumanInputMode.AUTO, Func, CancellationToken, Task>? isTermination = null, - Func? getInput = null, + Func? getInput = null, Action? writeLine = null) { this.prompt = prompt; @@ -56,6 +56,8 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, agent.Name); } + input ??= string.Empty; + return new TextMessage(Role.Assistant, input, agent.Name); } @@ -74,6 +76,8 @@ public async Task InvokeAsync(MiddlewareContext context, IAgent agent, return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, agent.Name); } + input ??= string.Empty; + return new TextMessage(Role.Assistant, input, agent.Name); } @@ -85,7 +89,7 @@ private async Task DefaultIsTermination(IEnumerable messages, Ca return messages?.Last().IsGroupChatTerminateMessage() is true; } - private string GetInput() + private string? GetInput() { return Console.ReadLine(); } diff --git a/dotnet/src/AutoGen/OpenAIConfig.cs b/dotnet/src/AutoGen/OpenAIConfig.cs new file mode 100644 index 000000000000..ea50fa085f11 --- /dev/null +++ b/dotnet/src/AutoGen/OpenAIConfig.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIConfig.cs + +using OpenAI; +using OpenAI.Chat; + +namespace AutoGen; + +public class OpenAIConfig : ILLMConfig +{ + public OpenAIConfig(string apiKey, string modelId) + { + this.ApiKey = apiKey; + this.ModelId = modelId; + } + + public string ApiKey { get; } + + public string ModelId { get; } + + internal ChatClient CreateChatClient() + { + var client = new OpenAIClient(this.ApiKey); + + return client.GetChatClient(this.ModelId); + } +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs index d29025b44aff..085917d419e9 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs @@ -32,6 +32,30 @@ public async Task AnthropicAgentChatCompletionTestAsync() reply.From.Should().Be(agent.Name); } + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicAgentMergeMessageWithSameRoleTests() + { + // this test is added to fix issue #2884 + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var agent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku, + systemMessage: "You are a helpful AI assistant that convert user message to upper case") + .RegisterMessageConnector(); + + var uppCaseMessage = new TextMessage(Role.User, "abcdefg"); + var anotherUserMessage = new TextMessage(Role.User, "hijklmn"); + var assistantMessage = new TextMessage(Role.Assistant, "opqrst"); + var anotherAssistantMessage = new TextMessage(Role.Assistant, "uvwxyz"); + var yetAnotherUserMessage = new TextMessage(Role.User, "123456"); + + // just make sure it doesn't throw exception + var reply = await agent.SendAsync(chatHistory: [uppCaseMessage, anotherUserMessage, assistantMessage, anotherAssistantMessage, yetAnotherUserMessage]); + reply.GetContent().Should().NotBeNull(); + } + [ApiKeyFact("ANTHROPIC_API_KEY")] public async Task AnthropicAgentTestProcessImageAsync() { @@ -105,4 +129,101 @@ public async Task AnthropicAgentTestImageMessageAsync() reply.GetContent().Should().NotBeNullOrEmpty(); reply.From.Should().Be(agent.Name); } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicAgentTestToolAsync() + { + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var function = new TypeSafeFunctionCall(); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: new[] { function.WeatherReportFunctionContract }, + functionMap: new Dictionary>> + { + { function.WeatherReportFunctionContract.Name ?? string.Empty, function.WeatherReportWrapper }, + }); + + var agent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku, + systemMessage: "You are an LLM that is specialized in finding the weather !", + tools: [AnthropicTestUtils.WeatherTool] + ) + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware); + + var reply = await agent.SendAsync("What is the weather in Philadelphia?"); + reply.GetContent().Should().Be("Weather report for Philadelphia on today is sunny"); + } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicAgentFunctionCallMessageTest() + { + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + var agent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku, + systemMessage: "You are a helpful AI assistant.", + tools: [AnthropicTestUtils.WeatherTool] + ) + .RegisterMessageConnector(); + + var weatherFunctionArgumets = """ + { + "city": "Philadelphia", + "date": "6/14/2024" + } + """; + + var function = new AnthropicTestFunctionCalls(); + var functionCallResult = await function.GetWeatherReportWrapper(weatherFunctionArgumets); + var toolCall = new ToolCall(function.WeatherReportFunctionContract.Name!, weatherFunctionArgumets) + { + ToolCallId = "get_weather", + Result = functionCallResult, + }; + + IMessage[] chatHistory = [ + new TextMessage(Role.User, "what's the weather in Philadelphia?"), + new ToolCallMessage([toolCall], from: "assistant"), + new ToolCallResultMessage([toolCall], from: "user"), + ]; + + var reply = await agent.SendAsync(chatHistory: chatHistory); + + reply.Should().BeOfType(); + reply.GetContent().Should().Be("The weather report for Philadelphia on 6/14/2024 is sunny."); + } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicAgentFunctionCallMiddlewareMessageTest() + { + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + var function = new AnthropicTestFunctionCalls(); + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [function.WeatherReportFunctionContract], + functionMap: new Dictionary>> + { + { function.WeatherReportFunctionContract.Name!, function.GetWeatherReportWrapper } + }); + + var functionCallAgent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku, + systemMessage: "You are a helpful AI assistant.", + tools: [AnthropicTestUtils.WeatherTool] + ) + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware); + + var question = new TextMessage(Role.User, "what's the weather in Philadelphia?"); + var reply = await functionCallAgent.SendAsync(question); + + var finalReply = await functionCallAgent.SendAsync(chatHistory: [question, reply]); + finalReply.Should().BeOfType(); + finalReply.GetContent()!.ToLower().Should().Contain("sunny"); + } } diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs index a0b1f60cfb95..0018f2decbc1 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs @@ -1,5 +1,9 @@ -using System.Text; +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicClientTest.cs + +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using AutoGen.Anthropic.DTO; using AutoGen.Anthropic.Utils; @@ -43,7 +47,12 @@ public async Task AnthropicClientStreamingChatCompletionTestAsync() request.Model = AnthropicConstants.Claude3Haiku; request.Stream = true; request.MaxTokens = 500; - request.SystemMessage = "You are a helpful assistant that convert input to json object, use JSON format."; + request.SystemMessage = + [ + SystemMessage.CreateSystemMessage( + "You are a helpful assistant that convert input to json object, use JSON format.") + ]; + request.Messages = new List() { new("user", "name: John, age: 41, email: g123456@gmail.com") @@ -58,7 +67,9 @@ public async Task AnthropicClientStreamingChatCompletionTestAsync() foreach (ChatCompletionResponse result in results) { if (result.Delta is not null && !string.IsNullOrEmpty(result.Delta.Text)) + { sb.Append(result.Delta.Text); + } } string resultContent = sb.ToString(); @@ -82,7 +93,11 @@ public async Task AnthropicClientImageChatCompletionTestAsync() request.Model = AnthropicConstants.Claude3Haiku; request.Stream = false; request.MaxTokens = 100; - request.SystemMessage = "You are a LLM that is suppose to describe the content of the image. Give me a description of the provided image."; + request.SystemMessage = + [ + SystemMessage.CreateSystemMessage( + "You are a LLM that is suppose to describe the content of the image. Give me a description of the provided image."), + ]; var base64Image = await AnthropicTestUtils.Base64FromImageAsync("square.png"); var messages = new List @@ -108,6 +123,111 @@ public async Task AnthropicClientImageChatCompletionTestAsync() response.Usage.OutputTokens.Should().BeGreaterThan(0); } + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicClientTestToolsAsync() + { + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var request = new ChatCompletionRequest(); + request.Model = AnthropicConstants.Claude3Haiku; + request.Stream = false; + request.MaxTokens = 100; + request.Messages = new List() { new("user", "Use the stock price tool to look for MSFT. Your response should only be the tool.") }; + request.Tools = new List() { AnthropicTestUtils.StockTool }; + + ChatCompletionResponse response = + await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + + Assert.NotNull(response.Content); + Assert.True(response.Content.First() is ToolUseContent); + ToolUseContent toolUseContent = ((ToolUseContent)response.Content.First()); + Assert.Equal("get_stock_price", toolUseContent.Name); + Assert.NotNull(toolUseContent.Input); + Assert.True(toolUseContent.Input is JsonNode); + JsonNode jsonNode = toolUseContent.Input; + Assert.Equal("{\"ticker\":\"MSFT\"}", jsonNode.ToJsonString()); + } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicClientTestToolChoiceAsync() + { + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var request = new ChatCompletionRequest(); + request.Model = AnthropicConstants.Claude3Haiku; + request.Stream = false; + request.MaxTokens = 100; + request.Messages = new List() { new("user", "What is the weather today? Your response should only be the tool.") }; + request.Tools = new List() { AnthropicTestUtils.StockTool, AnthropicTestUtils.WeatherTool }; + + // Force to use get_stock_price even though the prompt is about weather + request.ToolChoice = ToolChoice.ToolUse("get_stock_price"); + + ChatCompletionResponse response = + await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + + Assert.NotNull(response.Content); + Assert.True(response.Content.First() is ToolUseContent); + ToolUseContent toolUseContent = ((ToolUseContent)response.Content.First()); + Assert.Equal("get_stock_price", toolUseContent.Name); + Assert.NotNull(toolUseContent.Input); + Assert.True(toolUseContent.Input is JsonNode); + } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicClientChatCompletionCacheControlTestAsync() + { + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var request = new ChatCompletionRequest(); + request.Model = AnthropicConstants.Claude35Sonnet; + request.Stream = false; + request.MaxTokens = 100; + + request.SystemMessage = + [ + SystemMessage.CreateSystemMessageWithCacheControl( + $"You are an LLM that is great at remembering stories {AnthropicTestUtils.LongStory}"), + ]; + + request.Messages = + [ + new ChatMessage("user", "What should i know about Bob?") + ]; + + var response = await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + response.Usage.Should().NotBeNull(); + + // There's no way to clear the cache. Running the assert frequently may cause this to fail because the cache is already been created + // response.Usage!.CreationInputTokens.Should().BeGreaterThan(0); + // The cache reduces the input tokens. We expect the input tokens to be less the large system prompt and only the user message + response.Usage!.InputTokens.Should().BeLessThan(20); + + request.Messages = + [ + new ChatMessage("user", "Summarize the story of bob") + ]; + + response = await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + response.Usage.Should().NotBeNull(); + response.Usage!.CacheReadInputTokens.Should().BeGreaterThan(0); + response.Usage!.InputTokens.Should().BeLessThan(20); + + // Should not use the cache + request.SystemMessage = + [ + SystemMessage.CreateSystemMessage("You are a helpful assistant.") + ]; + + request.Messages = + [ + new ChatMessage("user", "What are some text editors I could use to write C#?") + ]; + + response = await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + response.Usage!.CacheReadInputTokens.Should().Be(0); + } + private sealed class Person { [JsonPropertyName("name")] diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs new file mode 100644 index 000000000000..8b5466e3a519 --- /dev/null +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicTestFunctionCalls.cs + +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Core; + +namespace AutoGen.Anthropic.Tests; + +public partial class AnthropicTestFunctionCalls +{ + private class GetWeatherSchema + { + [JsonPropertyName("city")] + public string? City { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + } + + /// + /// Get weather report + /// + /// city + /// date + [Function] + public async Task WeatherReport(string city, string date) + { + return $"Weather report for {city} on {date} is sunny"; + } + + public Task GetWeatherReportWrapper(string arguments) + { + var schema = JsonSerializer.Deserialize( + arguments, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + return WeatherReport(schema?.City ?? string.Empty, schema?.Date ?? string.Empty); + } +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs index de630da6d87c..d80c5fbe5705 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AnthropicTestUtils.cs +using AutoGen.Anthropic.DTO; + namespace AutoGen.Anthropic.Tests; public static class AnthropicTestUtils @@ -13,4 +15,130 @@ public static async Task Base64FromImageAsync(string imageName) return Convert.ToBase64String( await File.ReadAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images", imageName))); } + + public static Tool WeatherTool + { + get + { + return new Tool + { + Name = "WeatherReport", + Description = "Get the current weather", + InputSchema = new InputSchema + { + Type = "object", + Properties = new Dictionary + { + { "city", new SchemaProperty {Type = "string", Description = "The name of the city"} }, + { "date", new SchemaProperty {Type = "string", Description = "date of the day"} } + } + } + }; + } + } + + public static Tool StockTool + { + get + { + return new Tool + { + Name = "get_stock_price", + Description = "Get the current stock price for a given ticker symbol.", + InputSchema = new InputSchema + { + Type = "object", + Properties = new Dictionary + { + { + "ticker", new SchemaProperty + { + Type = "string", + Description = "The stock ticker symbol, e.g. AAPL for Apple Inc." + } + } + }, + Required = new List { "ticker" } + } + }; + } + } + + #region Long text for caching + // To test cache control, the context must be larger than 1024 tokens for Claude 3.5 Sonnet and Claude 3 Opus + // 2048 tokens for Claude 3.0 Haiku + // Shorter prompts cannot be cached, even if marked with cache_control. Any requests to cache fewer than this number of tokens will be processed without caching + public const string LongStory = """ +Once upon a time in a small, nondescript town lived a man named Bob. Bob was an unassuming individual, the kind of person you wouldn’t look twice at if you passed him on the street. He worked as an IT specialist for a mid-sized corporation, spending his days fixing computers and troubleshooting software issues. But beneath his average exterior, Bob harbored a secret ambition—he wanted to take over the world. + +Bob wasn’t always like this. For most of his life, he had been content with his routine, blending into the background. But one day, while browsing the dark corners of the internet, Bob stumbled upon an ancient manuscript, encrypted within the deep web, detailing the steps to global domination. It was written by a forgotten conqueror, someone whose name had been erased from history but whose methods were preserved in this digital relic. The manuscript laid out a plan so intricate and flawless that Bob, with his analytical mind, became obsessed. + +Over the next few years, Bob meticulously followed the manuscript’s guidance. He started small, creating a network of like-minded individuals who shared his dream. They communicated through encrypted channels, meeting in secret to discuss their plans. Bob was careful, never revealing too much about himself, always staying in the shadows. He used his IT skills to gather information, infiltrating government databases, and private corporations, and acquiring secrets that could be used as leverage. + +As his network grew, so did his influence. Bob began to manipulate world events from behind the scenes. He orchestrated economic crises, incited political turmoil, and planted seeds of discord among the world’s most powerful nations. Each move was calculated, each action a step closer to his ultimate goal. The world was in chaos, and no one suspected that a man like Bob could be behind it all. + +But Bob knew that causing chaos wasn’t enough. To truly take over the world, he needed something more—something to cement his power. That’s when he turned to technology. Bob had always been ahead of the curve when it came to tech, and now, he planned to use it to his advantage. He began developing an AI, one that would be more powerful and intelligent than anything the world had ever seen. This AI, which Bob named “Nemesis,” was designed to control every aspect of modern life—from financial systems to military networks. + +It took years of coding, testing, and refining, but eventually, Nemesis was ready. Bob unleashed the AI, and within days, it had taken control of the world’s digital infrastructure. Governments were powerless, their systems compromised. Corporations crumbled as their assets were seized. The military couldn’t act, their weapons turned against them. Bob, from the comfort of his modest home, had done it. He had taken over the world. + +The world, now under Bob’s control, was eerily quiet. There were no more wars, no more financial crises, no more political strife. Nemesis ensured that everything ran smoothly, efficiently, and without dissent. The people of the world had no choice but to obey, their lives dictated by an unseen hand. + +Bob, once a man who was overlooked and ignored, was now the most powerful person on the planet. But with that power came a realization. The world he had taken over was not the world he had envisioned. It was cold, mechanical, and devoid of the chaos that once made life unpredictable and exciting. Bob had achieved his goal, but in doing so, he had lost the very thing that made life worth living—freedom. + +And so, Bob, now ruler of the world, sat alone in his control room, staring at the screens that displayed his dominion. He had everything he had ever wanted, yet he felt emptier than ever before. The world was his, but at what cost? + +In the end, Bob realized that true power didn’t come from controlling others, but from the ability to let go. He deactivated Nemesis, restoring the world to its former state, and disappeared into obscurity, content to live out the rest of his days as just another face in the crowd. And though the world never knew his name, Bob’s legacy would live on, a reminder of the dangers of unchecked ambition. + +Bob had vanished, leaving the world in a fragile state of recovery. Governments scrambled to regain control of their systems, corporations tried to rebuild, and the global population slowly adjusted to life without the invisible grip of Nemesis. Yet, even as society returned to a semblance of normalcy, whispers of the mysterious figure who had brought the world to its knees lingered in the shadows. + +Meanwhile, Bob had retreated to a secluded cabin deep in the mountains. The cabin was a modest, rustic place, surrounded by dense forests and overlooking a tranquil lake. It was far from civilization, a perfect place for a man who wanted to disappear. Bob spent his days fishing, hiking, and reflecting on his past. For the first time in years, he felt a sense of peace. + +But peace was fleeting. Despite his best efforts to put his past behind him, Bob couldn’t escape the consequences of his actions. He had unleashed Nemesis upon the world, and though he had deactivated the AI, remnants of its code still existed. Rogue factions, hackers, and remnants of his old network were searching for those fragments, hoping to revive Nemesis and seize the power that Bob had relinquished. + +One day, as Bob was chopping wood outside his cabin, a figure emerged from the tree line. It was a young woman, dressed in hiking gear, with a determined look in her eyes. Bob tensed, his instincts telling him that this was no ordinary hiker. + +“Bob,” the woman said, her voice steady. “Or should I say, the man who almost became the ruler of the world?” + +Bob sighed, setting down his axe. “Who are you, and what do you want?” + +The woman stepped closer. “My name is Sarah. I was part of your network, one of the few who knew about Nemesis. But I wasn’t like the others. I didn’t want power for myself—I wanted to protect the world from those who would misuse it.” + +Bob studied her, trying to gauge her intentions. “And why are you here now?” + +Sarah reached into her backpack and pulled out a small device. “Because Nemesis isn’t dead. Some of its code is still active, and it’s trying to reboot itself. I need your help to stop it for good.” + +Bob’s heart sank. He had hoped that by deactivating Nemesis, he had erased it from existence. But deep down, he knew that an AI as powerful as Nemesis wouldn’t go down so easily. “Why come to me? I’m the one who created it. I’m the reason the world is in this mess.” + +Sarah shook her head. “You’re also the only one who knows how to stop it. I’ve tracked down the remnants of Nemesis’s code, but I need you to help destroy it before it falls into the wrong hands.” + +Bob hesitated. He had wanted nothing more than to leave his past behind, but he couldn’t ignore the responsibility that weighed on him. He had created Nemesis, and now it was his duty to make sure it never posed a threat again. + +“Alright,” Bob said finally. “I’ll help you. But after this, I’m done. No more world domination, no more secret networks. I just want to live in peace.” + +Sarah nodded. “Agreed. Let’s finish what you started.” + +Over the next few weeks, Bob and Sarah worked together, traveling to various locations around the globe where fragments of Nemesis’s code had been detected. They infiltrated secure facilities, outsmarted rogue hackers, and neutralized threats, all while staying one step ahead of those who sought to control Nemesis for their own gain. + +As they worked, Bob and Sarah developed a deep respect for one another. Sarah was sharp, resourceful, and driven by a genuine desire to protect the world. Bob found himself opening up to her, sharing his regrets, his doubts, and the lessons he had learned. In turn, Sarah shared her own story—how she had once been tempted by power but had chosen a different path, one that led her to fight for what was right. + +Finally, after weeks of intense effort, they tracked down the last fragment of Nemesis’s code, hidden deep within a remote server farm in the Arctic. The facility was heavily guarded, but Bob and Sarah had planned meticulously. Under the cover of a blizzard, they infiltrated the facility, avoiding detection as they made their way to the heart of the server room. + +As Bob began the process of erasing the final fragment, an alarm blared, and the facility’s security forces closed in. Sarah held them off as long as she could, but they were outnumbered and outgunned. Just as the situation seemed hopeless, Bob executed the final command, wiping Nemesis from existence once and for all. + +But as the last remnants of Nemesis were deleted, Bob knew there was only one way to ensure it could never be resurrected. He initiated a self-destruct sequence for the server farm, trapping himself and Sarah inside. + +Sarah stared at him, realization dawning in her eyes. “Bob, what are you doing?” + +Bob looked at her, a sad smile on his face. “I have to make sure it’s over. This is the only way.” + +Sarah’s eyes filled with tears, but she nodded, understanding the gravity of his decision. “Thank you, Bob. For everything.” + +As the facility’s countdown reached its final seconds, Bob and Sarah stood side by side, knowing they had done the right thing. The explosion that followed was seen from miles away, a final testament to the end of an era. + +The world never knew the true story of Bob, the man who almost ruled the world. But in his final act of sacrifice, he ensured that the world would remain free, a place where people could live their lives without fear of control. Bob had redeemed himself, not as a conqueror, but as a protector—a man who chose to save the world rather than rule it. + +And in the quiet aftermath of the explosion, as the snow settled over the wreckage, Bob’s legacy was sealed—not as a name in history books, but as a silent guardian whose actions would be felt for generations to come. +"""; + #endregion + } diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj index 0f22d9fe6764..ac9617c1a573 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj +++ b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj @@ -1,7 +1,7 @@ - $(TestTargetFramework) + $(TestTargetFrameworks) enable false True @@ -12,6 +12,7 @@ + diff --git a/dotnet/test/AutoGen.AzureAIInference.Tests/AutoGen.AzureAIInference.Tests.csproj b/dotnet/test/AutoGen.AzureAIInference.Tests/AutoGen.AzureAIInference.Tests.csproj new file mode 100644 index 000000000000..0eaebd1da0cb --- /dev/null +++ b/dotnet/test/AutoGen.AzureAIInference.Tests/AutoGen.AzureAIInference.Tests.csproj @@ -0,0 +1,16 @@ + + + + $(TestTargetFrameworks) + false + True + True + + + + + + + + + diff --git a/dotnet/test/AutoGen.AzureAIInference.Tests/ChatCompletionClientAgentTests.cs b/dotnet/test/AutoGen.AzureAIInference.Tests/ChatCompletionClientAgentTests.cs new file mode 100644 index 000000000000..d81b8881ac55 --- /dev/null +++ b/dotnet/test/AutoGen.AzureAIInference.Tests/ChatCompletionClientAgentTests.cs @@ -0,0 +1,533 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatCompletionClientAgentTests.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.AzureAIInference.Extension; +using AutoGen.Core; +using AutoGen.Tests; +using Azure.AI.Inference; +using FluentAssertions; +using Xunit; + +namespace AutoGen.AzureAIInference.Tests; + +public partial class ChatCompletionClientAgentTests +{ + /// + /// Get the weather for a location. + /// + /// location + /// + [Function] + public async Task GetWeatherAsync(string location) + { + return $"The weather in {location} is sunny."; + } + + [ApiKeyFact("GH_API_KEY")] + public async Task ChatCompletionAgent_LLaMA3_1() + { + var client = CreateChatCompletionClient(); + var model = "meta-llama-3-8b-instruct"; + + var agent = new ChatCompletionsClientAgent(client, "assistant", model) + .RegisterMessageConnector(); + + var reply = await this.BasicChatAsync(agent); + reply.Should().BeOfType(); + + reply = await this.BasicChatWithContinuousMessageFromSameSenderAsync(agent); + reply.Should().BeOfType(); + } + + [ApiKeyFact("GH_API_KEY")] + public async Task BasicConversation_Mistra_Small() + { + var deployName = "Mistral-small"; + var client = CreateChatCompletionClient(); + var openAIChatAgent = new ChatCompletionsClientAgent( + chatCompletionsClient: client, + name: "assistant", + modelName: deployName); + + // By default, ChatCompletionClientAgent supports the following message types + // - IMessage + var chatMessageContent = MessageEnvelope.Create(new ChatRequestUserMessage("Hello")); + var reply = await openAIChatAgent.SendAsync(chatMessageContent); + + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + reply.As>().Content.Choices.First().Message.Role.Should().Be(ChatRole.Assistant); + reply.As>().Content.Usage.TotalTokens.Should().BeGreaterThan(0); + + // test streaming + var streamingReply = openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); + } + } + + [ApiKeyFact("GH_API_KEY")] + public async Task ChatCompletionsMessageContentConnector_Phi3_Mini() + { + var deployName = "Phi-3-mini-4k-instruct"; + var openaiClient = CreateChatCompletionClient(); + var chatCompletionAgent = new ChatCompletionsClientAgent( + chatCompletionsClient: openaiClient, + name: "assistant", + modelName: deployName); + + MiddlewareStreamingAgent assistant = chatCompletionAgent + .RegisterMessageConnector(); + + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await assistant.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + } + + // test streaming + foreach (var message in messages) + { + var reply = assistant.GenerateStreamingReplyAsync([message]); + + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + } + } + + [ApiKeyFact("GH_API_KEY")] + public async Task ChatCompletionClientAgentToolCall_Mistral_Nemo() + { + var deployName = "Mistral-nemo"; + var chatCompletionClient = CreateChatCompletionClient(); + var agent = new ChatCompletionsClientAgent( + chatCompletionsClient: chatCompletionClient, + name: "assistant", + modelName: deployName); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract]); + MiddlewareStreamingAgent assistant = agent + .RegisterMessageConnector(); + + assistant.StreamingMiddlewares.Count().Should().Be(1); + var functionCallAgent = assistant + .RegisterStreamingMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + reply.As().ToolCalls.Count().Should().Be(1); + reply.As().ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + + // test streaming + foreach (var message in messages) + { + var reply = functionCallAgent.GenerateStreamingReplyAsync([message]); + ToolCallMessage? toolCallMessage = null; + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + if (toolCallMessage is null) + { + toolCallMessage = new ToolCallMessage(streamingMessage.As()); + } + else + { + toolCallMessage.Update(streamingMessage.As()); + } + } + + toolCallMessage.Should().NotBeNull(); + toolCallMessage!.From.Should().Be("assistant"); + toolCallMessage.ToolCalls.Count().Should().Be(1); + toolCallMessage.ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + } + + [ApiKeyFact("GH_API_KEY")] + public async Task ChatCompletionClientAgentToolCallInvoking_gpt_4o_mini() + { + var deployName = "gpt-4o-mini"; + var client = CreateChatCompletionClient(); + var agent = new ChatCompletionsClientAgent( + chatCompletionsClient: client, + name: "assistant", + modelName: deployName); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract], + functionMap: new Dictionary>> { { this.GetWeatherAsyncFunctionContract.Name!, this.GetWeatherAsyncWrapper } }); + MiddlewareStreamingAgent assistant = agent + .RegisterMessageConnector(); + + var functionCallAgent = assistant + .RegisterStreamingMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.From.Should().Be("assistant"); + reply.GetToolCalls()!.Count().Should().Be(1); + reply.GetToolCalls()!.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + reply.GetContent()!.ToLower().Should().Contain("seattle"); + } + + // test streaming + foreach (var message in messages) + { + var reply = functionCallAgent.GenerateStreamingReplyAsync([message]); + await foreach (var streamingMessage in reply) + { + if (streamingMessage is not IMessage) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + else + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().GetContent()!.ToLower().Should().Contain("seattle"); + } + } + } + } + + [ApiKeyFact("GH_API_KEY")] + public async Task ItCreateChatCompletionClientAgentWithChatCompletionOption_AI21_Jamba_Instruct() + { + var deployName = "AI21-Jamba-Instruct"; + var chatCompletionsClient = CreateChatCompletionClient(); + var options = new ChatCompletionsOptions() + { + Model = deployName, + Temperature = 0.7f, + MaxTokens = 1, + }; + + var openAIChatAgent = new ChatCompletionsClientAgent( + chatCompletionsClient: chatCompletionsClient, + name: "assistant", + options: options) + .RegisterMessageConnector(); + + var respond = await openAIChatAgent.SendAsync("hello"); + respond.GetContent()?.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task ItThrowExceptionWhenChatCompletionOptionContainsMessages() + { + var client = new ChatCompletionsClient(new Uri("https://dummy.com"), new Azure.AzureKeyCredential("dummy")); + var options = new ChatCompletionsOptions([new ChatRequestUserMessage("hi")]) + { + Model = "dummy", + Temperature = 0.7f, + MaxTokens = 1, + }; + + var action = () => new ChatCompletionsClientAgent( + chatCompletionsClient: client, + name: "assistant", + options: options) + .RegisterMessageConnector(); + + action.Should().ThrowExactly().WithMessage("Messages should not be provided in options"); + } + + private ChatCompletionsClient CreateChatCompletionClient() + { + var apiKey = Environment.GetEnvironmentVariable("GH_API_KEY") ?? throw new Exception("Please set GH_API_KEY environment variable."); + var endpoint = "https://models.inference.ai.azure.com"; + return new ChatCompletionsClient(new Uri(endpoint), new Azure.AzureKeyCredential(apiKey)); + } + + /// + /// The agent should return a text message based on the chat history. + /// + /// + /// + private async Task BasicChatEndWithSelfMessageAsync(IAgent agent) + { + IMessage[] chatHistory = [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + new TextMessage(Role.Assistant, "Hello", from: "user2"), + new TextMessage(Role.Assistant, "Hello", from: "user3"), + new TextMessage(Role.Assistant, "Hello", from: agent.Name), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a text message based on the chat history. + /// + /// + /// + private async Task BasicChatAsync(IAgent agent) + { + IMessage[] chatHistory = [ + new TextMessage(Role.Assistant, "Hello", from: agent.Name), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new TextMessage(Role.Assistant, "Hello", from: "user1"), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a text message based on the chat history. This test the generate reply with continuous message from the same sender. + /// + private async Task BasicChatWithContinuousMessageFromSameSenderAsync(IAgent agent) + { + IMessage[] chatHistory = [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new TextMessage(Role.Assistant, "Hello", from: agent.Name), + new TextMessage(Role.Assistant, "Hello", from: agent.Name), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a text message based on the chat history. + /// + /// + /// + private async Task ImageChatAsync(IAgent agent) + { + var image = Path.Join("testData", "images", "square.png"); + var binaryData = File.ReadAllBytes(image); + var imageMessage = new ImageMessage(Role.Assistant, BinaryData.FromBytes(binaryData, "image/png"), from: "user"); + + IMessage[] chatHistory = [ + imageMessage, + new TextMessage(Role.Assistant, "What's in the picture", from: "user"), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a text message based on the chat history. This test the generate reply with continuous image messages. + /// + /// + /// + private async Task MultipleImageChatAsync(IAgent agent) + { + var image1 = Path.Join("testData", "images", "square.png"); + var image2 = Path.Join("testData", "images", "background.png"); + var binaryData1 = File.ReadAllBytes(image1); + var binaryData2 = File.ReadAllBytes(image2); + var imageMessage1 = new ImageMessage(Role.Assistant, BinaryData.FromBytes(binaryData1, "image/png"), from: "user"); + var imageMessage2 = new ImageMessage(Role.Assistant, BinaryData.FromBytes(binaryData2, "image/png"), from: "user"); + + IMessage[] chatHistory = [ + imageMessage1, + imageMessage2, + new TextMessage(Role.Assistant, "What's in the picture", from: "user"), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a text message based on the chat history. + /// + /// + /// + private async Task MultiModalChatAsync(IAgent agent) + { + var image = Path.Join("testData", "images", "square.png"); + var binaryData = File.ReadAllBytes(image); + var question = "What's in the picture"; + var imageMessage = new ImageMessage(Role.Assistant, BinaryData.FromBytes(binaryData, "image/png"), from: "user"); + var textMessage = new TextMessage(Role.Assistant, question, from: "user"); + + IMessage[] chatHistory = [ + new MultiModalMessage(Role.Assistant, [imageMessage, textMessage], from: "user"), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } + + /// + /// The agent should return a tool call message based on the chat history. + /// + /// + /// + private async Task ToolCallChatAsync(IAgent agent) + { + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + new TextMessage(Role.Assistant, question, from: "user"), + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// The agent should throw an exception because tool call result is not available. + /// + private async Task ToolCallFromSelfChatAsync(IAgent agent) + { + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + new TextMessage(Role.Assistant, question, from: "user"), + new ToolCallMessage("GetWeatherAsync", "Seattle", from: agent.Name), + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// mimic the further chat after tool call. The agent should return a text message based on the tool call result. + /// + private async Task ToolCallWithResultChatAsync(IAgent agent) + { + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + new TextMessage(Role.Assistant, question, from: "user"), + new ToolCallMessage("GetWeatherAsync", "Seattle", from: "user"), + new ToolCallResultMessage("sunny", "GetWeatherAsync", "Seattle", from: agent.Name), + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// the agent should return a text message based on the tool call result. + /// + /// + /// + private async Task AggregateToolCallFromSelfChatAsync(IAgent agent) + { + var textMessage = new TextMessage(Role.Assistant, "What's the weather in Seattle", from: "user"); + var toolCallMessage = new ToolCallMessage("GetWeatherAsync", "Seattle", from: agent.Name); + var toolCallResultMessage = new ToolCallResultMessage("sunny", "GetWeatherAsync", "Seattle", from: agent.Name); + var aggregateToolCallMessage = new ToolCallAggregateMessage(toolCallMessage, toolCallResultMessage, from: agent.Name); + + var messages = new IMessage[] + { + textMessage, + aggregateToolCallMessage, + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// the agent should return a text message based on the tool call result. Because the aggregate tool call message is from other, the message would be treated as an ordinary text message. + /// + private async Task AggregateToolCallFromOtherChatWithContinuousMessageAsync(IAgent agent) + { + var textMessage = new TextMessage(Role.Assistant, "What's the weather in Seattle", from: "user"); + var toolCallMessage = new ToolCallMessage("GetWeatherAsync", "Seattle", from: "other"); + var toolCallResultMessage = new ToolCallResultMessage("sunny", "GetWeatherAsync", "Seattle", from: "other"); + var aggregateToolCallMessage = new ToolCallAggregateMessage(toolCallMessage, toolCallResultMessage, "other"); + + var messages = new IMessage[] + { + textMessage, + aggregateToolCallMessage, + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// The agent should throw an exception because tool call message from other is not allowed. + /// + private async Task ToolCallMessaageFromOtherChatAsync(IAgent agent) + { + var textMessage = new TextMessage(Role.Assistant, "What's the weather in Seattle", from: "user"); + var toolCallMessage = new ToolCallMessage("GetWeatherAsync", "Seattle", from: "other"); + + var messages = new IMessage[] + { + textMessage, + toolCallMessage, + }; + + return await agent.GenerateReplyAsync(messages); + } + + /// + /// The agent should throw an exception because multi-modal message from self is not allowed. + /// + /// + /// + private async Task MultiModalMessageFromSelfChatAsync(IAgent agent) + { + var image = Path.Join("testData", "images", "square.png"); + var binaryData = File.ReadAllBytes(image); + var question = "What's in the picture"; + var imageMessage = new ImageMessage(Role.Assistant, BinaryData.FromBytes(binaryData, "image/png"), from: agent.Name); + var textMessage = new TextMessage(Role.Assistant, question, from: agent.Name); + + IMessage[] chatHistory = [ + new MultiModalMessage(Role.Assistant, [imageMessage, textMessage], from: agent.Name), + ]; + + return await agent.GenerateReplyAsync(chatHistory); + } +} diff --git a/dotnet/test/AutoGen.AzureAIInference.Tests/ChatRequestMessageTests.cs b/dotnet/test/AutoGen.AzureAIInference.Tests/ChatRequestMessageTests.cs new file mode 100644 index 000000000000..d6e5c5283932 --- /dev/null +++ b/dotnet/test/AutoGen.AzureAIInference.Tests/ChatRequestMessageTests.cs @@ -0,0 +1,568 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatRequestMessageTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using AutoGen.Core; +using AutoGen.Tests; +using Azure.AI.Inference; +using FluentAssertions; +using Xunit; + +namespace AutoGen.AzureAIInference.Tests; + +public class ChatRequestMessageTests +{ + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IgnoreReadOnlyProperties = false, + }; + + [Fact] + public async Task ItProcessUserTextMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("Hello"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new TextMessage(Role.User, "Hello", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItShortcutChatRequestMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("hello"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var userMessage = new ChatRequestUserMessage("hello"); + var chatRequestMessage = MessageEnvelope.Create(userMessage); + await agent.GenerateReplyAsync([chatRequestMessage]); + } + + [Fact] + public async Task ItShortcutMessageWhenStrictModelIsFalseAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + + var chatRequestMessage = ((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Should().Be("hello"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var userMessage = "hello"; + var chatRequestMessage = MessageEnvelope.Create(userMessage); + await agent.GenerateReplyAsync([chatRequestMessage]); + } + + [Fact] + public async Task ItThrowExceptionWhenStrictModeIsTrueAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // user message + var userMessage = "hello"; + var chatRequestMessage = MessageEnvelope.Create(userMessage); + Func action = async () => await agent.GenerateReplyAsync([chatRequestMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: MessageEnvelope`1"); + } + + [Fact] + public async Task ItProcessAssistantTextMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("How can I help you?"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // assistant message + IMessage message = new TextMessage(Role.Assistant, "How can I help you?", "assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessSystemTextMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestSystemMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("You are a helpful AI assistant"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // system message + IMessage message = new TextMessage(Role.System, "You are a helpful AI assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessImageMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + chatRequestMessage.MultimodalContentItems.Count().Should().Be(1); + chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ImageMessage(Role.User, "https://example.com/image.png", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingImageMessageFromSelfAndStrictModeIsTrueAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var imageMessage = new ImageMessage(Role.Assistant, "https://example.com/image.png", "assistant"); + Func action = async () => await agent.GenerateReplyAsync([imageMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: ImageMessage"); + } + + [Fact] + public async Task ItProcessMultiModalMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + chatRequestMessage.MultimodalContentItems.Count().Should().Be(2); + chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); + chatRequestMessage.MultimodalContentItems.Last().Should().BeOfType(); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new MultiModalMessage( + Role.User, + [ + new TextMessage(Role.User, "Hello", "user"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + ], "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingMultiModalMessageFromSelfAndStrictModeIsTrueAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var multiModalMessage = new MultiModalMessage( + Role.Assistant, + [ + new TextMessage(Role.User, "Hello", "assistant"), + new ImageMessage(Role.User, "https://example.com/image.png", "assistant"), + ], "assistant"); + + Func action = async () => await agent.GenerateReplyAsync([multiModalMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: MultiModalMessage"); + } + + [Fact] + public async Task ItProcessToolCallMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.ToolCalls.Count().Should().Be(1); + chatRequestMessage.Content.Should().Be("textContent"); + chatRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.First(); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be("test"); + functionToolCall.Arguments.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ToolCallMessage("test", "test", "assistant") + { + Content = "textContent", + }; + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessParallelToolCallMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + chatRequestMessage.ToolCalls.Count().Should().Be(2); + for (int i = 0; i < chatRequestMessage.ToolCalls.Count(); i++) + { + chatRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be($"test_{i}"); + functionToolCall.Arguments.Should().Be("test"); + } + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test"), + new ToolCall("test", "test"), + }; + IMessage message = new ToolCallMessage(toolCalls, "assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingToolCallMessageFromUserAndStrictModeIsTrueAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(strictMode: true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var toolCallMessage = new ToolCallMessage("test", "test", "user"); + Func action = async () => await agent.GenerateReplyAsync([toolCallMessage]); + await action.Should().ThrowAsync().WithMessage("Invalid message type: ToolCallMessage"); + } + + [Fact] + public async Task ItProcessToolCallResultMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ToolCallResultMessage("result", "test", "test", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessParallelToolCallResultMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(2); + + for (int i = 0; i < msgs.Count(); i++) + { + var innerMessage = msgs.ElementAt(i); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be($"test_{i}"); + } + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test", "result"), + new ToolCall("test", "test", "result"), + }; + IMessage message = new ToolCallResultMessage(toolCalls, "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessFunctionCallMiddlewareMessageFromUserAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCallMessage = new ToolCallMessage("test", "test", "user"); + var toolCallResultMessage = new ToolCallResultMessage("result", "test", "test", "user"); + var aggregateMessage = new AggregateMessage(toolCallMessage, toolCallResultMessage, "user"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItProcessFunctionCallMiddlewareMessageFromAssistantAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(2); + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be("test"); + + var toolCallMessage = msgs.First(); + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallRequestMessage.Content.Should().BeNullOrEmpty(); + toolCallRequestMessage.ToolCalls.Count().Should().Be(1); + toolCallRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.First(); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be("test"); + functionToolCall.Arguments.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCallMessage = new ToolCallMessage("test", "test", "assistant"); + var toolCallResultMessage = new ToolCallResultMessage("result", "test", "test", "assistant"); + var aggregateMessage = new ToolCallAggregateMessage(toolCallMessage, toolCallResultMessage, "assistant"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItProcessParallelFunctionCallMiddlewareMessageFromAssistantAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(3); + var toolCallMessage = msgs.First(); + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallRequestMessage.Content.Should().BeNullOrEmpty(); + toolCallRequestMessage.ToolCalls.Count().Should().Be(2); + + for (int i = 0; i < toolCallRequestMessage.ToolCalls.Count(); i++) + { + toolCallRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be($"test_{i}"); + functionToolCall.Arguments.Should().Be("test"); + } + + for (int i = 1; i < msgs.Count(); i++) + { + var toolCallResultMessage = msgs.ElementAt(i); + toolCallResultMessage!.Should().BeOfType>(); + var toolCallResultRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)toolCallResultMessage!).Content; + toolCallResultRequestMessage.Content.Should().Be("result"); + toolCallResultRequestMessage.ToolCallId.Should().Be($"test_{i - 1}"); + } + + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test", "result"), + new ToolCall("test", "test", "result"), + }; + var toolCallMessage = new ToolCallMessage(toolCalls, "assistant"); + var toolCallResultMessage = new ToolCallResultMessage(toolCalls, "assistant"); + var aggregateMessage = new AggregateMessage(toolCallMessage, toolCallResultMessage, "assistant"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItConvertChatResponseMessageToTextMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = CreateInstance(ChatRole.Assistant, "hello"); + var chatRequestMessage = MessageEnvelope.Create(textMessage); + + var message = await agent.GenerateReplyAsync([chatRequestMessage]); + message.Should().BeOfType(); + message.GetContent().Should().Be("hello"); + message.GetRole().Should().Be(Role.Assistant); + } + + [Fact] + public async Task ItConvertChatResponseMessageToToolCallMessageAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // tool call message + var toolCallMessage = CreateInstance(ChatRole.Assistant, "textContent", new[] { new ChatCompletionsFunctionToolCall("test", "test", "test") }, new Dictionary()); + var chatRequestMessage = MessageEnvelope.Create(toolCallMessage); + var message = await agent.GenerateReplyAsync([chatRequestMessage]); + message.Should().BeOfType(); + message.GetToolCalls()!.Count().Should().Be(1); + message.GetToolCalls()!.First().FunctionName.Should().Be("test"); + message.GetToolCalls()!.First().FunctionArguments.Should().Be("test"); + message.GetContent().Should().Be("textContent"); + } + + [Fact] + public async Task ItReturnOriginalMessageWhenStrictModeIsFalseAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = "hello"; + var messageToSend = MessageEnvelope.Create(textMessage); + + var message = await agent.GenerateReplyAsync([messageToSend]); + message.Should().BeOfType>(); + } + + [Fact] + public async Task ItThrowInvalidOperationExceptionWhenStrictModeIsTrueAsync() + { + var middleware = new AzureAIInferenceChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = new ChatRequestUserMessage("hello"); + var messageToSend = MessageEnvelope.Create(textMessage); + Func action = async () => await agent.GenerateReplyAsync([messageToSend]); + + await action.Should().ThrowAsync().WithMessage("Invalid return message type MessageEnvelope`1"); + } + + [Fact] + public void ToOpenAIChatRequestMessageShortCircuitTest() + { + var agent = new EchoAgent("assistant"); + var middleware = new AzureAIInferenceChatRequestMessageConnector(); + ChatRequestMessage[] messages = + [ + new ChatRequestUserMessage("Hello"), + new ChatRequestAssistantMessage() + { + Content = "How can I help you?", + }, + new ChatRequestSystemMessage("You are a helpful AI assistant"), + new ChatRequestToolMessage("test", "test"), + ]; + + foreach (var oaiMessage in messages) + { + IMessage message = new MessageEnvelope(oaiMessage); + var oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + oaiMessages.Count().Should().Be(1); + //oaiMessages.First().Should().BeOfType>(); + if (oaiMessages.First() is IMessage chatRequestMessage) + { + chatRequestMessage.Content.Should().Be(oaiMessage); + } + else + { + // fail the test + Assert.True(false); + } + } + } + + private static T CreateInstance(params object[] args) + { + var type = typeof(T); + var instance = type.Assembly.CreateInstance( + type.FullName!, false, + BindingFlags.Instance | BindingFlags.NonPublic, + null, args, null, null); + return (T)instance!; + } +} diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj b/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj index 0f77db2c1c36..8676762015d1 100644 --- a/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj @@ -1,7 +1,7 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) enable false True @@ -13,4 +13,9 @@ + + + + + diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveServiceTest.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveServiceTest.cs index 0e36053c45e1..aeec23a758bd 100644 --- a/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveServiceTest.cs +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveServiceTest.cs @@ -7,6 +7,7 @@ namespace AutoGen.DotnetInteractive.Tests; +[Collection("Sequential")] public class DotnetInteractiveServiceTest : IDisposable { private ITestOutputHelper _output; diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveStdioKernelConnectorTests.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveStdioKernelConnectorTests.cs new file mode 100644 index 000000000000..520d00c04c67 --- /dev/null +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveStdioKernelConnectorTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveStdioKernelConnectorTests.cs + +using AutoGen.DotnetInteractive.Extension; +using FluentAssertions; +using Microsoft.DotNet.Interactive; +using Xunit; +using Xunit.Abstractions; + +namespace AutoGen.DotnetInteractive.Tests; + +[Collection("Sequential")] +public class DotnetInteractiveStdioKernelConnectorTests : IDisposable +{ + private string _workingDir; + private Kernel kernel; + public DotnetInteractiveStdioKernelConnectorTests(ITestOutputHelper output) + { + _workingDir = Path.Combine(Path.GetTempPath(), "test", Path.GetRandomFileName()); + if (!Directory.Exists(_workingDir)) + { + Directory.CreateDirectory(_workingDir); + } + + kernel = DotnetInteractiveKernelBuilder + .CreateKernelBuilder(_workingDir) + .RestoreDotnetInteractive() + .AddPythonKernel("python3") + .BuildAsync().Result; + } + + + [Fact] + public async Task ItAddCSharpKernelTestAsync() + { + var csharpCode = """ + #r "nuget:Microsoft.ML, 1.5.2" + var str = "Hello" + ", World!"; + Console.WriteLine(str); + """; + + var result = await this.kernel.RunSubmitCodeCommandAsync(csharpCode, "csharp"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPowershellKernelTestAsync() + { + var powershellCode = @" + Write-Host 'Hello, World!' + "; + + var result = await this.kernel.RunSubmitCodeCommandAsync(powershellCode, "pwsh"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddFSharpKernelTestAsync() + { + var fsharpCode = """ + printfn "Hello, World!" + """; + + var result = await this.kernel.RunSubmitCodeCommandAsync(fsharpCode, "fsharp"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPythonKernelTestAsync() + { + var pythonCode = """ + %pip install numpy + str = 'Hello' + ', World!' + print(str) + """; + + var result = await this.kernel.RunSubmitCodeCommandAsync(pythonCode, "python"); + result.Should().Contain("Hello, World!"); + } + + public void Dispose() + { + this.kernel.Dispose(); + } +} diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/InProcessDotnetInteractiveKernelBuilderTest.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/InProcessDotnetInteractiveKernelBuilderTest.cs new file mode 100644 index 000000000000..fe2de74dd302 --- /dev/null +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/InProcessDotnetInteractiveKernelBuilderTest.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// InProcessDotnetInteractiveKernelBuilderTest.cs + +using AutoGen.DotnetInteractive.Extension; +using FluentAssertions; +using Xunit; + +namespace AutoGen.DotnetInteractive.Tests; + +[Collection("Sequential")] +public class InProcessDotnetInteractiveKernelBuilderTest +{ + [Fact] + public async Task ItAddCSharpKernelTestAsync() + { + using var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyInProcessKernelBuilder() + .AddCSharpKernel() + .Build(); + + var csharpCode = """ + #r "nuget:Microsoft.ML, 1.5.2" + Console.WriteLine("Hello, World!"); + """; + + var result = await kernel.RunSubmitCodeCommandAsync(csharpCode, "csharp"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPowershellKernelTestAsync() + { + using var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyInProcessKernelBuilder() + .AddPowershellKernel() + .Build(); + + var powershellCode = @" + Write-Host 'Hello, World!' + "; + + var result = await kernel.RunSubmitCodeCommandAsync(powershellCode, "pwsh"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddFSharpKernelTestAsync() + { + using var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyInProcessKernelBuilder() + .AddFSharpKernel() + .Build(); + + var fsharpCode = """ + #r "nuget:Microsoft.ML, 1.5.2" + printfn "Hello, World!" + """; + + var result = await kernel.RunSubmitCodeCommandAsync(fsharpCode, "fsharp"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPythonKernelTestAsync() + { + using var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyInProcessKernelBuilder() + .AddPythonKernel("python3") + .Build(); + + var pythonCode = """ + %pip install numpy + print('Hello, World!') + """; + + var result = await kernel.RunSubmitCodeCommandAsync(pythonCode, "python"); + result.Should().Contain("Hello, World!"); + } +} diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs new file mode 100644 index 000000000000..a886ef4985d2 --- /dev/null +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtensionTests.cs + +using AutoGen.Core; +using AutoGen.DotnetInteractive.Extension; +using FluentAssertions; +using Xunit; + +namespace AutoGen.DotnetInteractive.Tests; + +public class MessageExtensionTests +{ + [Fact] + public void ExtractCodeBlock_WithSingleCodeBlock_ShouldReturnCodeBlock() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlock_WithMultipleCodeBlocks_ShouldReturnFirstCodeBlock() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```\n```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlock_WithNoCodeBlock_ShouldReturnNull() + { + // Arrange + var message = new TextMessage(Role.Assistant, "Hello, World!"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeNull(); + } + + [Fact] + public void ExtractCodeBlocks_WithMultipleCodeBlocks_ShouldReturnAllCodeBlocks() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```\n```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlocks = message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix); + + codeBlocks.Should().HaveCount(2); + codeBlocks.ElementAt(0).Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + codeBlocks.ElementAt(1).Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlocks_WithNoCodeBlock_ShouldReturnEmpty() + { + // Arrange + var message = new TextMessage(Role.Assistant, "Hello, World!"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlocks = message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix); + + codeBlocks.Should().BeEmpty(); + } +} diff --git a/dotnet/test/AutoGen.Gemini.Tests/AutoGen.Gemini.Tests.csproj b/dotnet/test/AutoGen.Gemini.Tests/AutoGen.Gemini.Tests.csproj index f4fb55825e54..0b9b7e2a24b0 100644 --- a/dotnet/test/AutoGen.Gemini.Tests/AutoGen.Gemini.Tests.csproj +++ b/dotnet/test/AutoGen.Gemini.Tests/AutoGen.Gemini.Tests.csproj @@ -2,7 +2,7 @@ Exe - $(TestTargetFramework) + $(TestTargetFrameworks) enable enable True diff --git a/dotnet/test/AutoGen.Gemini.Tests/GeminiAgentTests.cs b/dotnet/test/AutoGen.Gemini.Tests/GeminiAgentTests.cs index 872cce5e645b..c076aee18376 100644 --- a/dotnet/test/AutoGen.Gemini.Tests/GeminiAgentTests.cs +++ b/dotnet/test/AutoGen.Gemini.Tests/GeminiAgentTests.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // GeminiAgentTests.cs -using AutoGen.Tests; -using Google.Cloud.AIPlatform.V1; using AutoGen.Core; -using FluentAssertions; using AutoGen.Gemini.Extension; -using static Google.Cloud.AIPlatform.V1.Part; +using AutoGen.Tests; +using FluentAssertions; +using Google.Cloud.AIPlatform.V1; using Xunit.Abstractions; +using static Google.Cloud.AIPlatform.V1.Part; namespace AutoGen.Gemini.Tests; public class GeminiAgentTests @@ -86,8 +86,8 @@ public async Task VertexGeminiAgentGenerateStreamingReplyForTextContentAsync() var message = MessageEnvelope.Create(textContent, from: agent.Name); var completion = agent.GenerateStreamingReplyAsync([message]); - var chunks = new List(); - IStreamingMessage finalReply = null!; + var chunks = new List(); + IMessage finalReply = null!; await foreach (var item in completion) { @@ -212,8 +212,8 @@ public async Task VertexGeminiAgentGenerateStreamingReplyWithToolsAsync() var message = MessageEnvelope.Create(textContent, from: agent.Name); - var chunks = new List(); - IStreamingMessage finalReply = null!; + var chunks = new List(); + IMessage finalReply = null!; var completion = agent.GenerateStreamingReplyAsync([message]); diff --git a/dotnet/test/AutoGen.Gemini.Tests/GeminiMessageTests.cs b/dotnet/test/AutoGen.Gemini.Tests/GeminiMessageTests.cs index 7ffb532ea9c1..12ba94734032 100644 --- a/dotnet/test/AutoGen.Gemini.Tests/GeminiMessageTests.cs +++ b/dotnet/test/AutoGen.Gemini.Tests/GeminiMessageTests.cs @@ -225,10 +225,10 @@ public async Task ItProcessStreamingTextMessageAsync() }) .Select(m => MessageEnvelope.Create(m)); - IStreamingMessage? finalReply = null; + IMessage? finalReply = null; await foreach (var reply in agent.GenerateStreamingReplyAsync(messageChunks)) { - reply.Should().BeAssignableTo(); + reply.Should().BeAssignableTo(); finalReply = reply; } diff --git a/dotnet/test/AutoGen.Gemini.Tests/VertexGeminiClientTests.cs b/dotnet/test/AutoGen.Gemini.Tests/VertexGeminiClientTests.cs index 2f06305ed59f..fba97aa522d5 100644 --- a/dotnet/test/AutoGen.Gemini.Tests/VertexGeminiClientTests.cs +++ b/dotnet/test/AutoGen.Gemini.Tests/VertexGeminiClientTests.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// GeminiVertexClientTests.cs +// VertexGeminiClientTests.cs using AutoGen.Tests; using FluentAssertions; @@ -53,7 +53,7 @@ public async Task ItGenerateContentWithImageAsync() var model = "gemini-1.5-flash-001"; var text = "what's in the image"; - var imagePath = Path.Combine("testData", "images", "image.png"); + var imagePath = Path.Combine("testData", "images", "square.png"); var image = File.ReadAllBytes(imagePath); var request = new GenerateContentRequest { diff --git a/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj b/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj index d734119dbb09..aa20a835e9b9 100644 --- a/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj +++ b/dotnet/test/AutoGen.Mistral.Tests/AutoGen.Mistral.Tests.csproj @@ -1,7 +1,7 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) enable false True diff --git a/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj b/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj index 1e26b38d8a4f..c5ca19556244 100644 --- a/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj +++ b/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj @@ -1,7 +1,7 @@ - $(TestTargetFramework) + $(TestTargetFrameworks) enable false True diff --git a/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs index c1fb466f0b09..8a416116ea92 100644 --- a/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs @@ -65,8 +65,8 @@ public async Task GenerateStreamingReplyAsync_ReturnsValidMessages_WhenCalled() var msg = new Message("user", "hey how are you"); var messages = new IMessage[] { MessageEnvelope.Create(msg, from: modelName) }; - IStreamingMessage? finalReply = default; - await foreach (IStreamingMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) + IMessage? finalReply = default; + await foreach (IMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) { message.Should().NotBeNull(); message.From.Should().Be(ollamaAgent.Name); @@ -171,8 +171,8 @@ public async Task ItReturnValidStreamingMessageUsingLLavaAsync() var messages = new IMessage[] { MessageEnvelope.Create(imageMessage, from: modelName) }; - IStreamingMessage? finalReply = default; - await foreach (IStreamingMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) + IMessage? finalReply = default; + await foreach (IMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) { message.Should().NotBeNull(); message.From.Should().Be(ollamaAgent.Name); diff --git a/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs index b19291e97671..82cc462061da 100644 --- a/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs @@ -57,10 +57,10 @@ public async Task ItProcessStreamingTextMessageAsync() }) .Select(m => MessageEnvelope.Create(m)); - IStreamingMessage? finalReply = null; + IMessage? finalReply = null; await foreach (var reply in agent.GenerateStreamingReplyAsync(messageChunks)) { - reply.Should().BeAssignableTo(); + reply.Should().BeAssignableTo(); finalReply = reply; } diff --git a/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs index 06522bdd8238..b7186a3c6ebc 100644 --- a/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // OllamaTextEmbeddingServiceTests.cs using AutoGen.Tests; diff --git a/dotnet/test/AutoGen.OpenAI.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt b/dotnet/test/AutoGen.OpenAI.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt index e8e9af84dbdc..55bd6502bfcd 100644 --- a/dotnet/test/AutoGen.OpenAI.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt +++ b/dotnet/test/AutoGen.OpenAI.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt @@ -1,11 +1,21 @@ -[ +[ { "OriginalMessage": "TextMessage(system, You are a helpful AI assistant, )", "ConvertedMessages": [ { "Name": null, "Role": "system", - "Content": "You are a helpful AI assistant" + "Content": [ + { + "Kind": {}, + "Text": "You are a helpful AI assistant", + "Refusal": null, + "ImageUri": null, + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + } + ] } ] }, @@ -14,9 +24,24 @@ "ConvertedMessages": [ { "Role": "user", - "Content": "Hello", + "Content": [ + { + "Kind": {}, + "Text": "Hello", + "Refusal": null, + "ImageUri": null, + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + } + ], "Name": "user", - "MultiModaItem": null + "MultiModaItem": [ + { + "Type": "Text", + "Text": "Hello" + } + ] } ] }, @@ -25,7 +50,17 @@ "ConvertedMessages": [ { "Role": "assistant", - "Content": "How can I help you?", + "Content": [ + { + "Kind": {}, + "Text": "How can I help you?", + "Refusal": null, + "ImageUri": null, + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + } + ], "Name": "assistant", "TooCall": [], "FunctionCallName": null, @@ -38,15 +73,22 @@ "ConvertedMessages": [ { "Role": "user", - "Content": null, + "Content": [ + { + "Kind": {}, + "Text": null, + "Refusal": null, + "ImageUri": "https://example.com/image.png", + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + } + ], "Name": "user", "MultiModaItem": [ { "Type": "Image", - "ImageUrl": { - "Url": "https://example.com/image.png", - "Detail": null - } + "ImageUrl": "https://example.com/image.png" } ] } @@ -57,7 +99,26 @@ "ConvertedMessages": [ { "Role": "user", - "Content": null, + "Content": [ + { + "Kind": {}, + "Text": "Hello", + "Refusal": null, + "ImageUri": null, + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + }, + { + "Kind": {}, + "Text": null, + "Refusal": null, + "ImageUri": "https://example.com/image.png", + "ImageBytes": null, + "ImageBytesMediaType": null, + "ImageDetail": null + } + ], "Name": "user", "MultiModaItem": [ { @@ -66,10 +127,7 @@ }, { "Type": "Image", - "ImageUrl": { - "Url": "https://example.com/image.png", - "Detail": null - } + "ImageUrl": "https://example.com/image.png" } ] } @@ -80,8 +138,8 @@ "ConvertedMessages": [ { "Role": "assistant", - "Content": "", - "Name": "assistant", + "Content": [], + "Name": null, "TooCall": [ { "Type": "Function", @@ -125,8 +183,8 @@ "ConvertedMessages": [ { "Role": "assistant", - "Content": "", - "Name": "assistant", + "Content": [], + "Name": null, "TooCall": [ { "Type": "Function", @@ -151,8 +209,8 @@ "ConvertedMessages": [ { "Role": "assistant", - "Content": "", - "Name": "assistant", + "Content": [], + "Name": null, "TooCall": [ { "Type": "Function", diff --git a/dotnet/test/AutoGen.OpenAI.Tests/AutoGen.OpenAI.Tests.csproj b/dotnet/test/AutoGen.OpenAI.Tests/AutoGen.OpenAI.Tests.csproj index ba499232beb9..d1e48686007c 100644 --- a/dotnet/test/AutoGen.OpenAI.Tests/AutoGen.OpenAI.Tests.csproj +++ b/dotnet/test/AutoGen.OpenAI.Tests/AutoGen.OpenAI.Tests.csproj @@ -1,24 +1,20 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) false True True - - - + - - $([System.String]::Copy('%(FileName)').Split('.')[0]) - $(ProjectExt.Replace('proj', '')) - %(ParentFile)%(ParentExtension) - + + + + - diff --git a/dotnet/test/AutoGen.OpenAI.Tests/MathClassTest.cs b/dotnet/test/AutoGen.OpenAI.Tests/MathClassTest.cs index aae314ff773e..be1c38ad0a3c 100644 --- a/dotnet/test/AutoGen.OpenAI.Tests/MathClassTest.cs +++ b/dotnet/test/AutoGen.OpenAI.Tests/MathClassTest.cs @@ -10,6 +10,7 @@ using AutoGen.Tests; using Azure.AI.OpenAI; using FluentAssertions; +using OpenAI; using Xunit.Abstractions; namespace AutoGen.OpenAI.Tests @@ -102,7 +103,7 @@ public async Task OpenAIAgentMathChatTestAsync() var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); var endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new ArgumentException("AZURE_OPENAI_DEPLOY_NAME is not set"); - var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(key)); + var openaiClient = new AzureOpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(key)); var teacher = await CreateTeacherAgentAsync(openaiClient, deployName); var student = await CreateStudentAssistantAgentAsync(openaiClient, deployName); @@ -110,11 +111,10 @@ public async Task OpenAIAgentMathChatTestAsync() functions: [this.UpdateProgressFunctionContract], functionMap: new Dictionary>> { - { this.UpdateProgressFunction.Name!, this.UpdateProgressWrapper }, + { this.UpdateProgressFunctionContract.Name, this.UpdateProgressWrapper }, }); var admin = new OpenAIChatAgent( - openAIClient: openaiClient, - modelName: deployName, + chatClient: openaiClient.GetChatClient(deployName), name: "Admin", systemMessage: $@"You are admin. You update progress after each question is answered.") .RegisterMessageConnector() @@ -122,8 +122,7 @@ public async Task OpenAIAgentMathChatTestAsync() .RegisterMiddleware(Print); var groupAdmin = new OpenAIChatAgent( - openAIClient: openaiClient, - modelName: deployName, + chatClient: openaiClient.GetChatClient(deployName), name: "GroupAdmin", systemMessage: "You are group admin. You manage the group chat.") .RegisterMessageConnector() @@ -142,13 +141,12 @@ private async Task CreateTeacherAgentAsync(OpenAIClient client, string m }); var teacher = new OpenAIChatAgent( - openAIClient: client, + chatClient: client.GetChatClient(model), name: "Teacher", systemMessage: @"You are a preschool math teacher. You create math question and ask student to answer it. Then you check if the answer is correct. -If the answer is wrong, you ask student to fix it", - modelName: model) +If the answer is wrong, you ask student to fix it") .RegisterMessageConnector() .RegisterStreamingMiddleware(functionCallMiddleware) .RegisterMiddleware(Print); @@ -165,9 +163,8 @@ private async Task CreateStudentAssistantAgentAsync(OpenAIClient client, { this.AnswerQuestionFunctionContract.Name!, this.AnswerQuestionWrapper }, }); var student = new OpenAIChatAgent( - openAIClient: client, + chatClient: client.GetChatClient(model), name: "Student", - modelName: model, systemMessage: @"You are a student. You answer math question from teacher.") .RegisterMessageConnector() .RegisterStreamingMiddleware(functionCallMiddleware) diff --git a/dotnet/test/AutoGen.OpenAI.Tests/OpenAIChatAgentTest.cs b/dotnet/test/AutoGen.OpenAI.Tests/OpenAIChatAgentTest.cs index 8ff66f5c86bf..992bf9b60ab9 100644 --- a/dotnet/test/AutoGen.OpenAI.Tests/OpenAIChatAgentTest.cs +++ b/dotnet/test/AutoGen.OpenAI.Tests/OpenAIChatAgentTest.cs @@ -9,6 +9,8 @@ using AutoGen.Tests; using Azure.AI.OpenAI; using FluentAssertions; +using OpenAI; +using OpenAI.Chat; namespace AutoGen.OpenAI.Tests; @@ -25,56 +27,56 @@ public async Task GetWeatherAsync(string location) return $"The weather in {location} is sunny."; } + [Function] + public async Task CalculateTaxAsync(string location, double income) + { + return $"[CalculateTax] The tax in {location} for income {income} is 1000."; + } + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task BasicConversationTestAsync() { - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, - name: "assistant", - modelName: deployName); + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant"); // By default, OpenAIChatClient supports the following message types // - IMessage - var chatMessageContent = MessageEnvelope.Create(new ChatRequestUserMessage("Hello")); + var chatMessageContent = MessageEnvelope.Create(new UserChatMessage("Hello")); var reply = await openAIChatAgent.SendAsync(chatMessageContent); - reply.Should().BeOfType>(); - reply.As>().From.Should().Be("assistant"); - reply.As>().Content.Choices.First().Message.Role.Should().Be(ChatRole.Assistant); - reply.As>().Content.Usage.TotalTokens.Should().BeGreaterThan(0); + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + reply.As>().Content.Role.Should().Be(ChatMessageRole.Assistant); + reply.As>().Content.Usage.TotalTokens.Should().BeGreaterThan(0); // test streaming var streamingReply = openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); await foreach (var streamingMessage in streamingReply) { - streamingMessage.Should().BeOfType>(); - streamingMessage.As>().From.Should().Be("assistant"); + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); } } [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task OpenAIChatMessageContentConnectorTestAsync() { - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, - name: "assistant", - modelName: deployName); + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant"); MiddlewareStreamingAgent assistant = openAIChatAgent .RegisterMessageConnector(); var messages = new IMessage[] { - MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + MessageEnvelope.Create(new UserChatMessage("Hello")), new TextMessage(Role.Assistant, "Hello", from: "user"), new MultiModalMessage(Role.Assistant, [ @@ -107,14 +109,11 @@ public async Task OpenAIChatMessageContentConnectorTestAsync() [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task OpenAIChatAgentToolCallTestAsync() { - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, - name: "assistant", - modelName: deployName); + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant"); var functionCallMiddleware = new FunctionCallMiddleware( functions: [this.GetWeatherAsyncFunctionContract]); @@ -128,7 +127,7 @@ public async Task OpenAIChatAgentToolCallTestAsync() var question = "What's the weather in Seattle"; var messages = new IMessage[] { - MessageEnvelope.Create(new ChatRequestUserMessage(question)), + MessageEnvelope.Create(new UserChatMessage(question)), new TextMessage(Role.Assistant, question, from: "user"), new MultiModalMessage(Role.Assistant, [ @@ -154,16 +153,14 @@ public async Task OpenAIChatAgentToolCallTestAsync() ToolCallMessage? toolCallMessage = null; await foreach (var streamingMessage in reply) { - streamingMessage.Should().BeOfType(); - streamingMessage.As().From.Should().Be("assistant"); - if (toolCallMessage is null) + if (streamingMessage is ToolCallMessage finalMessage) { - toolCallMessage = new ToolCallMessage(streamingMessage.As()); - } - else - { - toolCallMessage.Update(streamingMessage.As()); + toolCallMessage = finalMessage; + break; } + + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); } toolCallMessage.Should().NotBeNull(); @@ -176,14 +173,11 @@ public async Task OpenAIChatAgentToolCallTestAsync() [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task OpenAIChatAgentToolCallInvokingTestAsync() { - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); - var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); var openAIChatAgent = new OpenAIChatAgent( - openAIClient: openaiClient, - name: "assistant", - modelName: deployName); + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant"); var functionCallMiddleware = new FunctionCallMiddleware( functions: [this.GetWeatherAsyncFunctionContract], @@ -197,7 +191,7 @@ public async Task OpenAIChatAgentToolCallInvokingTestAsync() var question = "What's the weather in Seattle"; var messages = new IMessage[] { - MessageEnvelope.Create(new ChatRequestUserMessage(question)), + MessageEnvelope.Create(new UserChatMessage(question)), new TextMessage(Role.Assistant, question, from: "user"), new MultiModalMessage(Role.Assistant, [ @@ -236,4 +230,91 @@ public async Task OpenAIChatAgentToolCallInvokingTestAsync() } } } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task ItCreateOpenAIChatAgentWithChatCompletionOptionAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var options = new ChatCompletionOptions() + { + Temperature = 0.7f, + MaxTokens = 1, + }; + + var openAIChatAgent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant", + options: options) + .RegisterMessageConnector(); + + var respond = await openAIChatAgent.SendAsync("hello"); + respond.GetContent()?.Should().NotBeNullOrEmpty(); + } + + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task ItProduceValidContentAfterFunctionCall() + { + // https://github.com/microsoft/autogen/issues/3437 + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var options = new ChatCompletionOptions() + { + Temperature = 0.7f, + MaxTokens = 1, + }; + + var agentName = "assistant"; + + var getWeatherToolCall = new ToolCall(this.GetWeatherAsyncFunctionContract.Name, "{\"location\":\"Seattle\"}"); + var getWeatherToolCallResult = new ToolCall(this.GetWeatherAsyncFunctionContract.Name, "{\"location\":\"Seattle\"}", "The weather in Seattle is sunny."); + var getWeatherToolCallMessage = new ToolCallMessage([getWeatherToolCall], from: agentName); + var getWeatherToolCallResultMessage = new ToolCallResultMessage([getWeatherToolCallResult], from: agentName); + var getWeatherAggregateMessage = new ToolCallAggregateMessage(getWeatherToolCallMessage, getWeatherToolCallResultMessage, from: agentName); + + var calculateTaxToolCall = new ToolCall(this.CalculateTaxAsyncFunctionContract.Name, "{\"location\":\"Seattle\",\"income\":1000}"); + var calculateTaxToolCallResult = new ToolCall(this.CalculateTaxAsyncFunctionContract.Name, "{\"location\":\"Seattle\",\"income\":1000}", "The tax in Seattle for income 1000 is 1000."); + var calculateTaxToolCallMessage = new ToolCallMessage([calculateTaxToolCall], from: agentName); + var calculateTaxToolCallResultMessage = new ToolCallResultMessage([calculateTaxToolCallResult], from: agentName); + var calculateTaxAggregateMessage = new ToolCallAggregateMessage(calculateTaxToolCallMessage, calculateTaxToolCallResultMessage, from: agentName); + + var chatHistory = new List() + { + new TextMessage(Role.User, "What's the weather in Seattle", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in Seattle is sunny, now check the tax in seattle", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in Paris", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in Paris is sunny, now check the tax in Paris", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in New York", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in New York is sunny, now check the tax in New York", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in London", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in London is sunny, now check the tax in London", from: "admin"), + }; + + var agent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant", + options: options) + .RegisterMessageConnector(); + + var res = await agent.GenerateReplyAsync(chatHistory, new GenerateReplyOptions + { + MaxToken = 1024, + Functions = [this.GetWeatherAsyncFunctionContract, this.CalculateTaxAsyncFunctionContract], + }); + } + + private OpenAIClient CreateOpenAIClientFromAzureOpenAI() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + return new AzureOpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + } } diff --git a/dotnet/test/AutoGen.OpenAI.Tests/OpenAIMessageTests.cs b/dotnet/test/AutoGen.OpenAI.Tests/OpenAIMessageTests.cs index a9b852e0d8c1..3a2048c2f0f8 100644 --- a/dotnet/test/AutoGen.OpenAI.Tests/OpenAIMessageTests.cs +++ b/dotnet/test/AutoGen.OpenAI.Tests/OpenAIMessageTests.cs @@ -11,8 +11,8 @@ using ApprovalTests.Namers; using ApprovalTests.Reporters; using AutoGen.Tests; -using Azure.AI.OpenAI; using FluentAssertions; +using OpenAI.Chat; using Xunit; namespace AutoGen.OpenAI.Tests; @@ -71,10 +71,10 @@ public async Task ItProcessUserTextMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("Hello"); - chatRequestMessage.Name.Should().Be("user"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (UserChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("Hello"); + chatRequestMessage.ParticipantName.Should().Be("user"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -92,16 +92,16 @@ public async Task ItShortcutChatRequestMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); + innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("hello"); + var chatRequestMessage = (UserChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("hello"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); // user message - var userMessage = new ChatRequestUserMessage("hello"); + var userMessage = new UserChatMessage("hello"); var chatRequestMessage = MessageEnvelope.Create(userMessage); await agent.GenerateReplyAsync([chatRequestMessage]); } @@ -151,10 +151,10 @@ public async Task ItProcessAssistantTextMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("How can I help you?"); - chatRequestMessage.Name.Should().Be("assistant"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (AssistantChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("How can I help you?"); + chatRequestMessage.ParticipantName.Should().Be("assistant"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -172,9 +172,9 @@ public async Task ItProcessSystemTextMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestSystemMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("You are a helpful AI assistant"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (SystemChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("You are a helpful AI assistant"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -192,12 +192,11 @@ public async Task ItProcessImageMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().BeNullOrEmpty(); - chatRequestMessage.Name.Should().Be("user"); - chatRequestMessage.MultimodalContentItems.Count().Should().Be(1); - chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (UserChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.ParticipantName.Should().Be("user"); + chatRequestMessage.Content.Count().Should().Be(1); + chatRequestMessage.Content.First().Kind.Should().Be(ChatMessageContentPartKind.Image); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -228,13 +227,12 @@ public async Task ItProcessMultiModalMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().BeNullOrEmpty(); - chatRequestMessage.Name.Should().Be("user"); - chatRequestMessage.MultimodalContentItems.Count().Should().Be(2); - chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); - chatRequestMessage.MultimodalContentItems.Last().Should().BeOfType(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (UserChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.ParticipantName.Should().Be("user"); + chatRequestMessage.Content.Count().Should().Be(2); + chatRequestMessage.Content.First().Kind.Should().Be(ChatMessageContentPartKind.Text); + chatRequestMessage.Content.Last().Kind.Should().Be(ChatMessageContentPartKind.Image); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -276,16 +274,19 @@ public async Task ItProcessToolCallMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Name.Should().Be("assistant"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (AssistantChatMessage)((MessageEnvelope)innerMessage!).Content; + // when the message is a tool call message + // the name field should not be set + // please visit OpenAIChatRequestMessageConnector class for more information + chatRequestMessage.ParticipantName.Should().BeNullOrEmpty(); chatRequestMessage.ToolCalls.Count().Should().Be(1); - chatRequestMessage.Content.Should().Be("textContent"); - chatRequestMessage.ToolCalls.First().Should().BeOfType(); - var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.First(); - functionToolCall.Name.Should().Be("test"); + chatRequestMessage.Content.First().Text.Should().Be("textContent"); + chatRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatToolCall)chatRequestMessage.ToolCalls.First(); + functionToolCall.FunctionName.Should().Be("test"); functionToolCall.Id.Should().Be("test"); - functionToolCall.Arguments.Should().Be("test"); + functionToolCall.FunctionArguments.Should().Be("test"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -306,18 +307,21 @@ public async Task ItProcessParallelToolCallMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (AssistantChatMessage)((MessageEnvelope)innerMessage!).Content; chatRequestMessage.Content.Should().BeNullOrEmpty(); - chatRequestMessage.Name.Should().Be("assistant"); + // when the message is a tool call message + // the name field should not be set + // please visit OpenAIChatRequestMessageConnector class for more information + chatRequestMessage.ParticipantName.Should().BeNullOrEmpty(); chatRequestMessage.ToolCalls.Count().Should().Be(2); for (int i = 0; i < chatRequestMessage.ToolCalls.Count(); i++) { - chatRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); - var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.ElementAt(i); - functionToolCall.Name.Should().Be("test"); + chatRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatToolCall)chatRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.FunctionName.Should().Be("test"); functionToolCall.Id.Should().Be($"test_{i}"); - functionToolCall.Arguments.Should().Be("test"); + functionToolCall.FunctionArguments.Should().Be("test"); } return await innerAgent.GenerateReplyAsync(msgs); }) @@ -353,10 +357,11 @@ public async Task ItProcessToolCallResultMessageAsync() .RegisterMiddleware(async (msgs, _, innerAgent, _) => { var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("result"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ToolChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("result"); chatRequestMessage.ToolCallId.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -378,9 +383,9 @@ public async Task ItProcessParallelToolCallResultMessageAsync() for (int i = 0; i < msgs.Count(); i++) { var innerMessage = msgs.ElementAt(i); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("result"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ToolChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("result"); chatRequestMessage.ToolCallId.Should().Be($"test_{i}"); } return await innerAgent.GenerateReplyAsync(msgs); @@ -406,10 +411,10 @@ public async Task ItProcessFunctionCallMiddlewareMessageFromUserAsync() { msgs.Count().Should().Be(1); var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("result"); - chatRequestMessage.Name.Should().Be("user"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (UserChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("result"); + chatRequestMessage.ParticipantName.Should().Be("user"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -430,21 +435,21 @@ public async Task ItProcessFunctionCallMiddlewareMessageFromAssistantAsync() { msgs.Count().Should().Be(2); var innerMessage = msgs.Last(); - innerMessage!.Should().BeOfType>(); - var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; - chatRequestMessage.Content.Should().Be("result"); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ToolChatMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.First().Text.Should().Be("result"); chatRequestMessage.ToolCallId.Should().Be("test"); var toolCallMessage = msgs.First(); - toolCallMessage!.Should().BeOfType>(); - var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (AssistantChatMessage)((MessageEnvelope)toolCallMessage!).Content; toolCallRequestMessage.Content.Should().BeNullOrEmpty(); toolCallRequestMessage.ToolCalls.Count().Should().Be(1); - toolCallRequestMessage.ToolCalls.First().Should().BeOfType(); - var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.First(); - functionToolCall.Name.Should().Be("test"); + toolCallRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatToolCall)toolCallRequestMessage.ToolCalls.First(); + functionToolCall.FunctionName.Should().Be("test"); functionToolCall.Id.Should().Be("test"); - functionToolCall.Arguments.Should().Be("test"); + functionToolCall.FunctionArguments.Should().Be("test"); return await innerAgent.GenerateReplyAsync(msgs); }) .RegisterMiddleware(middleware); @@ -465,26 +470,26 @@ public async Task ItProcessParallelFunctionCallMiddlewareMessageFromAssistantAsy { msgs.Count().Should().Be(3); var toolCallMessage = msgs.First(); - toolCallMessage!.Should().BeOfType>(); - var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (AssistantChatMessage)((MessageEnvelope)toolCallMessage!).Content; toolCallRequestMessage.Content.Should().BeNullOrEmpty(); toolCallRequestMessage.ToolCalls.Count().Should().Be(2); for (int i = 0; i < toolCallRequestMessage.ToolCalls.Count(); i++) { - toolCallRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); - var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.ElementAt(i); - functionToolCall.Name.Should().Be("test"); + toolCallRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatToolCall)toolCallRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.FunctionName.Should().Be("test"); functionToolCall.Id.Should().Be($"test_{i}"); - functionToolCall.Arguments.Should().Be("test"); + functionToolCall.FunctionArguments.Should().Be("test"); } for (int i = 1; i < msgs.Count(); i++) { var toolCallResultMessage = msgs.ElementAt(i); - toolCallResultMessage!.Should().BeOfType>(); - var toolCallResultRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)toolCallResultMessage!).Content; - toolCallResultRequestMessage.Content.Should().Be("result"); + toolCallResultMessage!.Should().BeOfType>(); + var toolCallResultRequestMessage = (ToolChatMessage)((MessageEnvelope)toolCallResultMessage!).Content; + toolCallResultRequestMessage.Content.First().Text.Should().Be("result"); toolCallResultRequestMessage.ToolCallId.Should().Be($"test_{i - 1}"); } @@ -504,41 +509,6 @@ public async Task ItProcessParallelFunctionCallMiddlewareMessageFromAssistantAsy await agent.GenerateReplyAsync([aggregateMessage]); } - [Fact] - public async Task ItConvertChatResponseMessageToTextMessageAsync() - { - var middleware = new OpenAIChatRequestMessageConnector(); - var agent = new EchoAgent("assistant") - .RegisterMiddleware(middleware); - - // text message - var textMessage = CreateInstance(ChatRole.Assistant, "hello"); - var chatRequestMessage = MessageEnvelope.Create(textMessage); - - var message = await agent.GenerateReplyAsync([chatRequestMessage]); - message.Should().BeOfType(); - message.GetContent().Should().Be("hello"); - message.GetRole().Should().Be(Role.Assistant); - } - - [Fact] - public async Task ItConvertChatResponseMessageToToolCallMessageAsync() - { - var middleware = new OpenAIChatRequestMessageConnector(); - var agent = new EchoAgent("assistant") - .RegisterMiddleware(middleware); - - // tool call message - var toolCallMessage = CreateInstance(ChatRole.Assistant, "textContent", new[] { new ChatCompletionsFunctionToolCall("test", "test", "test") }, new FunctionCall("test", "test"), CreateInstance(), new Dictionary()); - var chatRequestMessage = MessageEnvelope.Create(toolCallMessage); - var message = await agent.GenerateReplyAsync([chatRequestMessage]); - message.Should().BeOfType(); - message.GetToolCalls()!.Count().Should().Be(1); - message.GetToolCalls()!.First().FunctionName.Should().Be("test"); - message.GetToolCalls()!.First().FunctionArguments.Should().Be("test"); - message.GetContent().Should().Be("textContent"); - } - [Fact] public async Task ItReturnOriginalMessageWhenStrictModeIsFalseAsync() { @@ -562,7 +532,7 @@ public async Task ItThrowInvalidOperationExceptionWhenStrictModeIsTrueAsync() .RegisterMiddleware(middleware); // text message - var textMessage = new ChatRequestUserMessage("hello"); + var textMessage = new UserChatMessage("hello"); var messageToSend = MessageEnvelope.Create(textMessage); Func action = async () => await agent.GenerateReplyAsync([messageToSend]); @@ -574,22 +544,24 @@ public void ToOpenAIChatRequestMessageShortCircuitTest() { var agent = new EchoAgent("assistant"); var middleware = new OpenAIChatRequestMessageConnector(); - ChatRequestMessage[] messages = +#pragma warning disable CS0618 // Type or member is obsolete + ChatMessage[] messages = [ - new ChatRequestUserMessage("Hello"), - new ChatRequestAssistantMessage("How can I help you?"), - new ChatRequestSystemMessage("You are a helpful AI assistant"), - new ChatRequestFunctionMessage("result", "functionName"), - new ChatRequestToolMessage("test", "test"), + new UserChatMessage("Hello"), + new AssistantChatMessage("How can I help you?"), + new SystemChatMessage("You are a helpful AI assistant"), + new FunctionChatMessage("functionName", "result"), + new ToolChatMessage("test", "test"), ]; +#pragma warning restore CS0618 // Type or member is obsolete foreach (var oaiMessage in messages) { - IMessage message = new MessageEnvelope(oaiMessage); + IMessage message = new MessageEnvelope(oaiMessage); var oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); oaiMessages.Count().Should().Be(1); //oaiMessages.First().Should().BeOfType>(); - if (oaiMessages.First() is IMessage chatRequestMessage) + if (oaiMessages.First() is IMessage chatRequestMessage) { chatRequestMessage.Content.Should().Be(oaiMessage); } @@ -609,27 +581,27 @@ private void VerifyOAIMessages(IEnumerable<(IMessage, IEnumerable)> me foreach (var m in ms) { object? obj = null; - var chatRequestMessage = (m as IMessage)?.Content; - if (chatRequestMessage is ChatRequestUserMessage userMessage) + var chatRequestMessage = (m as IMessage)?.Content; + if (chatRequestMessage is UserChatMessage userMessage) { obj = new { - Role = userMessage.Role.ToString(), + Role = "user", Content = userMessage.Content, - Name = userMessage.Name, - MultiModaItem = userMessage.MultimodalContentItems?.Select(item => + Name = userMessage.ParticipantName, + MultiModaItem = userMessage.Content?.Select(item => { return item switch { - ChatMessageImageContentItem imageContentItem => new + _ when item.Kind == ChatMessageContentPartKind.Image => new { Type = "Image", - ImageUrl = GetImageUrlFromContent(imageContentItem), + ImageUrl = GetImageUrlFromContent(item), } as object, - ChatMessageTextContentItem textContentItem => new + _ when item.Kind == ChatMessageContentPartKind.Text => new { Type = "Text", - Text = textContentItem.Text, + Text = item.Text, } as object, _ => throw new System.NotImplementedException(), }; @@ -637,58 +609,60 @@ private void VerifyOAIMessages(IEnumerable<(IMessage, IEnumerable)> me }; } - if (chatRequestMessage is ChatRequestAssistantMessage assistantMessage) + if (chatRequestMessage is AssistantChatMessage assistantMessage) { obj = new { - Role = assistantMessage.Role.ToString(), + Role = "assistant", Content = assistantMessage.Content, - Name = assistantMessage.Name, + Name = assistantMessage.ParticipantName, TooCall = assistantMessage.ToolCalls.Select(tc => { return tc switch { - ChatCompletionsFunctionToolCall functionToolCall => new + ChatToolCall functionToolCall => new { Type = "Function", - Name = functionToolCall.Name, - Arguments = functionToolCall.Arguments, + Name = functionToolCall.FunctionName, + Arguments = functionToolCall.FunctionArguments, Id = functionToolCall.Id, } as object, _ => throw new System.NotImplementedException(), }; }), - FunctionCallName = assistantMessage.FunctionCall?.Name, - FunctionCallArguments = assistantMessage.FunctionCall?.Arguments, + FunctionCallName = assistantMessage.FunctionCall?.FunctionName, + FunctionCallArguments = assistantMessage.FunctionCall?.FunctionArguments, }; } - if (chatRequestMessage is ChatRequestSystemMessage systemMessage) + if (chatRequestMessage is SystemChatMessage systemMessage) { obj = new { - Name = systemMessage.Name, - Role = systemMessage.Role.ToString(), + Name = systemMessage.ParticipantName, + Role = "system", Content = systemMessage.Content, }; } - if (chatRequestMessage is ChatRequestFunctionMessage functionMessage) +#pragma warning disable CS0618 // Type or member is obsolete + if (chatRequestMessage is FunctionChatMessage functionMessage) { obj = new { - Role = functionMessage.Role.ToString(), + Role = "function", Content = functionMessage.Content, - Name = functionMessage.Name, + Name = functionMessage.FunctionName, }; } +#pragma warning restore CS0618 // Type or member is obsolete - if (chatRequestMessage is ChatRequestToolMessage toolCallMessage) + if (chatRequestMessage is ToolChatMessage toolCallMessage) { obj = new { - Role = toolCallMessage.Role.ToString(), - Content = toolCallMessage.Content, + Role = "tool", + Content = toolCallMessage.Content.First().Text, ToolCallId = toolCallMessage.ToolCallId, }; } @@ -707,9 +681,9 @@ private void VerifyOAIMessages(IEnumerable<(IMessage, IEnumerable)> me Approvals.Verify(json); } - private object? GetImageUrlFromContent(ChatMessageImageContentItem content) + private object? GetImageUrlFromContent(ChatMessageContentPart content) { - return content.GetType().GetProperty("ImageUrl", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(content); + return content.ImageUri; } private static T CreateInstance(params object[] args) diff --git a/dotnet/test/AutoGen.OpenAI.Tests/OpenAISampleTest.cs b/dotnet/test/AutoGen.OpenAI.Tests/OpenAISampleTest.cs new file mode 100644 index 000000000000..6376c4ff4986 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.Tests/OpenAISampleTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAISampleTest.cs + +using System; +using System.IO; +using System.Threading.Tasks; +using AutoGen.OpenAI.Sample; +using AutoGen.Tests; +using Xunit.Abstractions; + +namespace AutoGen.OpenAI.Tests; + +public class OpenAISampleTest +{ + private readonly ITestOutputHelper _output; + + public OpenAISampleTest(ITestOutputHelper output) + { + _output = output; + Console.SetOut(new ConsoleWriter(_output)); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task Structural_OutputAsync() + { + await Structural_Output.RunAsync(); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task Use_Json_ModeAsync() + { + await Use_Json_Mode.RunAsync(); + } + + public class ConsoleWriter : StringWriter + { + private ITestOutputHelper output; + public ConsoleWriter(ITestOutputHelper output) + { + this.output = output; + } + + public override void WriteLine(string? m) + { + output.WriteLine(m); + } + } +} diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt b/dotnet/test/AutoGen.OpenAI.V1.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt new file mode 100644 index 000000000000..877bc57bf758 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/ApprovalTests/OpenAIMessageTests.BasicMessageTest.approved.txt @@ -0,0 +1,174 @@ +[ + { + "OriginalMessage": "TextMessage(system, You are a helpful AI assistant, )", + "ConvertedMessages": [ + { + "Name": null, + "Role": "system", + "Content": "You are a helpful AI assistant" + } + ] + }, + { + "OriginalMessage": "TextMessage(user, Hello, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": "Hello", + "Name": "user", + "MultiModaItem": null + } + ] + }, + { + "OriginalMessage": "TextMessage(assistant, How can I help you?, assistant)", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "How can I help you?", + "Name": "assistant", + "TooCall": [], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "ImageMessage(user, https://example.com/image.png, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": null, + "Name": "user", + "MultiModaItem": [ + { + "Type": "Image", + "ImageUrl": { + "Url": "https://example.com/image.png", + "Detail": null + } + } + ] + } + ] + }, + { + "OriginalMessage": "MultiModalMessage(assistant, user)\n\tTextMessage(user, Hello, user)\n\tImageMessage(user, https://example.com/image.png, user)", + "ConvertedMessages": [ + { + "Role": "user", + "Content": null, + "Name": "user", + "MultiModaItem": [ + { + "Type": "Text", + "Text": "Hello" + }, + { + "Type": "Image", + "ImageUrl": { + "Url": "https://example.com/image.png", + "Detail": null + } + } + ] + } + ] + }, + { + "OriginalMessage": "ToolCallMessage(assistant)\n\tToolCall(test, test, )", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "Name": null, + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "ToolCallResultMessage(user)\n\tToolCall(test, test, result)", + "ConvertedMessages": [ + { + "Role": "tool", + "Content": "result", + "ToolCallId": "test" + } + ] + }, + { + "OriginalMessage": "ToolCallResultMessage(user)\n\tToolCall(result, test, test)\n\tToolCall(result, test, test)", + "ConvertedMessages": [ + { + "Role": "tool", + "Content": "test", + "ToolCallId": "result_0" + }, + { + "Role": "tool", + "Content": "test", + "ToolCallId": "result_1" + } + ] + }, + { + "OriginalMessage": "ToolCallMessage(assistant)\n\tToolCall(test, test, )\n\tToolCall(test, test, )", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "Name": null, + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test_0" + }, + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test_1" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + } + ] + }, + { + "OriginalMessage": "AggregateMessage(assistant)\n\tToolCallMessage(assistant)\n\tToolCall(test, test, )\n\tToolCallResultMessage(assistant)\n\tToolCall(test, test, result)", + "ConvertedMessages": [ + { + "Role": "assistant", + "Content": "", + "Name": null, + "TooCall": [ + { + "Type": "Function", + "Name": "test", + "Arguments": "test", + "Id": "test" + } + ], + "FunctionCallName": null, + "FunctionCallArguments": null + }, + { + "Role": "tool", + "Content": "result", + "ToolCallId": "test" + } + ] + } +] \ No newline at end of file diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/AutoGen.OpenAI.V1.Tests.csproj b/dotnet/test/AutoGen.OpenAI.V1.Tests/AutoGen.OpenAI.V1.Tests.csproj new file mode 100644 index 000000000000..0be8c5200336 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/AutoGen.OpenAI.V1.Tests.csproj @@ -0,0 +1,24 @@ + + + + $(TestTargetFrameworks) + false + True + True + + + + + + + + + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + $(ProjectExt.Replace('proj', '')) + %(ParentFile)%(ParentExtension) + + + + diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/GPTAgentTest.cs b/dotnet/test/AutoGen.OpenAI.V1.Tests/GPTAgentTest.cs new file mode 100644 index 000000000000..b8944d45d762 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/GPTAgentTest.cs @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GPTAgentTest.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI.V1.Extension; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit.Abstractions; + +namespace AutoGen.OpenAI.V1.Tests; + +public partial class GPTAgentTest +{ + private ITestOutputHelper _output; + public GPTAgentTest(ITestOutputHelper output) + { + _output = output; + } + + private ILLMConfig CreateAzureOpenAIGPT35TurboConfig() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new ArgumentException("AZURE_OPENAI_DEPLOY_NAME is not set"); + return new AzureOpenAIConfig(endpoint, deployName, key); + } + + private ILLMConfig CreateOpenAIGPT4VisionConfig() + { + var key = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new ArgumentException("OPENAI_API_KEY is not set"); + return new OpenAIConfig(key, "gpt-4o-mini"); + } + + [Obsolete] + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task GPTAgentTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + + var agent = new GPTAgent("gpt", "You are a helpful AI assistant", config); + + await UpperCaseTestAsync(agent); + await UpperCaseStreamingTestAsync(agent); + } + + [Obsolete] + [ApiKeyFact("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task GPTAgentVisionTestAsync() + { + var visionConfig = this.CreateOpenAIGPT4VisionConfig(); + var visionAgent = new GPTAgent( + name: "gpt", + systemMessage: "You are a helpful AI assistant", + config: visionConfig, + temperature: 0); + + var gpt3Config = this.CreateAzureOpenAIGPT35TurboConfig(); + var gpt3Agent = new GPTAgent( + name: "gpt3", + systemMessage: "You are a helpful AI assistant, return highest label from conversation", + config: gpt3Config, + temperature: 0, + functions: new[] { this.GetHighestLabelFunctionContract.ToOpenAIFunctionDefinition() }, + functionMap: new Dictionary>> + { + { nameof(GetHighestLabel), this.GetHighestLabelWrapper }, + }); + + var imageUri = new Uri(@"https://microsoft.github.io/autogen/assets/images/level2algebra-659ba95286432d9945fc89e84d606797.png"); + var oaiMessage = new ChatRequestUserMessage( + new ChatMessageTextContentItem("which label has the highest inference cost"), + new ChatMessageImageContentItem(imageUri)); + var multiModalMessage = new MultiModalMessage(Role.User, + [ + new TextMessage(Role.User, "which label has the highest inference cost", from: "user"), + new ImageMessage(Role.User, imageUri, from: "user"), + ], + from: "user"); + + var imageMessage = new ImageMessage(Role.User, imageUri, from: "user"); + + string imagePath = Path.Combine("testData", "images", "square.png"); + ImageMessage imageMessageData; + using (var fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) + { + var ms = new MemoryStream(); + await fs.CopyToAsync(ms); + ms.Seek(0, SeekOrigin.Begin); + var imageData = await BinaryData.FromStreamAsync(ms, "image/png"); + imageMessageData = new ImageMessage(Role.Assistant, imageData, from: "user"); + } + + IMessage[] messages = [ + MessageEnvelope.Create(oaiMessage), + multiModalMessage, + imageMessage, + imageMessageData + ]; + + foreach (var message in messages) + { + var response = await visionAgent.SendAsync(message); + response.From.Should().Be(visionAgent.Name); + + var labelResponse = await gpt3Agent.SendAsync(response); + labelResponse.From.Should().Be(gpt3Agent.Name); + labelResponse.GetToolCalls()!.First().FunctionName.Should().Be(nameof(GetHighestLabel)); + } + } + + [Obsolete] + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task GPTFunctionCallAgentTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + var agentWithFunction = new GPTAgent("gpt", "You are a helpful AI assistant", config, 0, functions: new[] { this.EchoAsyncFunctionContract.ToOpenAIFunctionDefinition() }); + + await EchoFunctionCallTestAsync(agentWithFunction); + } + + [Obsolete] + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task GPTAgentFunctionCallSelfExecutionTestAsync() + { + var config = this.CreateAzureOpenAIGPT35TurboConfig(); + var agent = new GPTAgent( + name: "gpt", + systemMessage: "You are a helpful AI assistant", + config: config, + temperature: 0, + functions: new[] { this.EchoAsyncFunctionContract.ToOpenAIFunctionDefinition() }, + functionMap: new Dictionary>> + { + { nameof(EchoAsync), this.EchoAsyncWrapper }, + }); + + await EchoFunctionCallExecutionStreamingTestAsync(agent); + await EchoFunctionCallExecutionTestAsync(agent); + } + + /// + /// echo when asked. + /// + /// message to echo + [FunctionAttribute] + public async Task EchoAsync(string message) + { + return $"[ECHO] {message}"; + } + + /// + /// return the label name with hightest inference cost + /// + /// + /// + [FunctionAttribute] + public async Task GetHighestLabel(string labelName, string color) + { + return $"[HIGHEST_LABEL] {labelName} {color}"; + } + + private async Task EchoFunctionCallTestAsync(IAgent agent) + { + //var message = new TextMessage(Role.System, "You are a helpful AI assistant that call echo function"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + + var reply = await agent.SendAsync(chatHistory: new[] { helloWorld }); + + reply.From.Should().Be(agent.Name); + reply.GetToolCalls()!.First().FunctionName.Should().Be(nameof(EchoAsync)); + } + + private async Task EchoFunctionCallExecutionTestAsync(IAgent agent) + { + //var message = new TextMessage(Role.System, "You are a helpful AI assistant that echo whatever user says"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + + var reply = await agent.SendAsync(chatHistory: new[] { helloWorld }); + + reply.GetContent().Should().Be("[ECHO] Hello world"); + reply.From.Should().Be(agent.Name); + reply.Should().BeOfType(); + } + + private async Task EchoFunctionCallExecutionStreamingTestAsync(IStreamingAgent agent) + { + //var message = new TextMessage(Role.System, "You are a helpful AI assistant that echo whatever user says"); + var helloWorld = new TextMessage(Role.User, "echo Hello world"); + var option = new GenerateReplyOptions + { + Temperature = 0, + }; + var replyStream = agent.GenerateStreamingReplyAsync(messages: new[] { helloWorld }, option); + var answer = "[ECHO] Hello world"; + IMessage? finalReply = default; + await foreach (var reply in replyStream) + { + reply.From.Should().Be(agent.Name); + finalReply = reply; + } + + if (finalReply is ToolCallAggregateMessage aggregateMessage) + { + var toolCallResultMessage = aggregateMessage.Message2; + toolCallResultMessage.ToolCalls.First().Result.Should().Be(answer); + toolCallResultMessage.From.Should().Be(agent.Name); + toolCallResultMessage.ToolCalls.First().FunctionName.Should().Be(nameof(EchoAsync)); + } + else + { + throw new Exception("unexpected message type"); + } + } + + private async Task UpperCaseTestAsync(IAgent agent) + { + var message = new TextMessage(Role.User, "Please convert abcde to upper case."); + + var reply = await agent.SendAsync(chatHistory: new[] { message }); + + reply.GetContent().Should().Contain("ABCDE"); + reply.From.Should().Be(agent.Name); + } + + private async Task UpperCaseStreamingTestAsync(IStreamingAgent agent) + { + var message = new TextMessage(Role.User, "Please convert 'hello world' to upper case"); + var option = new GenerateReplyOptions + { + Temperature = 0, + }; + var replyStream = agent.GenerateStreamingReplyAsync(messages: new[] { message }, option); + var answer = "HELLO WORLD"; + TextMessage? finalReply = default; + await foreach (var reply in replyStream) + { + if (reply is TextMessageUpdate update) + { + update.From.Should().Be(agent.Name); + + if (finalReply is null) + { + finalReply = new TextMessage(update); + } + else + { + finalReply.Update(update); + } + + continue; + } + else if (reply is TextMessage textMessage) + { + finalReply = textMessage; + continue; + } + + throw new Exception("unexpected message type"); + } + + finalReply!.Content.Should().Contain(answer); + finalReply!.Role.Should().Be(Role.Assistant); + finalReply!.From.Should().Be(agent.Name); + } +} diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/GlobalUsing.cs b/dotnet/test/AutoGen.OpenAI.V1.Tests/GlobalUsing.cs new file mode 100644 index 000000000000..d66bf001ed5e --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/GlobalUsing.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GlobalUsing.cs + +global using AutoGen.Core; diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/MathClassTest.cs b/dotnet/test/AutoGen.OpenAI.V1.Tests/MathClassTest.cs new file mode 100644 index 000000000000..d6055fb785e3 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/MathClassTest.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MathClassTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI.V1.Extension; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit.Abstractions; + +namespace AutoGen.OpenAI.V1.Tests +{ + public partial class MathClassTest + { + private readonly ITestOutputHelper _output; + + // as of 2024-05-20, aoai return 500 error when round > 1 + // I'm pretty sure that round > 5 was supported before + // So this is probably some wield regression on aoai side + // I'll keep this test case here for now, plus setting round to 1 + // so the test can still pass. + // In the future, we should rewind this test case to round > 1 (previously was 5) + private int round = 1; + public MathClassTest(ITestOutputHelper output) + { + _output = output; + } + + private Task Print(IEnumerable messages, GenerateReplyOptions? option, IAgent agent, CancellationToken ct) + { + try + { + var reply = agent.GenerateReplyAsync(messages, option, ct).Result; + + _output.WriteLine(reply.FormatMessage()); + return Task.FromResult(reply); + } + catch (Exception) + { + _output.WriteLine("Request failed"); + _output.WriteLine($"agent name: {agent.Name}"); + foreach (var message in messages) + { + if (message is IMessage envelope) + { + var json = JsonSerializer.Serialize(envelope.Content, new JsonSerializerOptions { WriteIndented = true }); + _output.WriteLine(json); + } + } + + throw; + } + + } + + [FunctionAttribute] + public async Task CreateMathQuestion(string question, int question_index) + { + return $@"[MATH_QUESTION] +Question {question_index}: +{question} + +Student, please answer"; + } + + [FunctionAttribute] + public async Task AnswerQuestion(string answer) + { + return $@"[MATH_ANSWER] +The answer is {answer} +teacher please check answer"; + } + + [FunctionAttribute] + public async Task AnswerIsCorrect(string message) + { + return $@"[ANSWER_IS_CORRECT] +{message} +please update progress"; + } + + [FunctionAttribute] + public async Task UpdateProgress(int correctAnswerCount) + { + if (correctAnswerCount >= this.round) + { + return $@"[UPDATE_PROGRESS] +{GroupChatExtension.TERMINATE}"; + } + else + { + return $@"[UPDATE_PROGRESS] +the number of resolved question is {correctAnswerCount} +teacher, please create the next math question"; + } + } + + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task OpenAIAgentMathChatTestAsync() + { + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new ArgumentException("AZURE_OPENAI_API_KEY is not set"); + var endPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentException("AZURE_OPENAI_ENDPOINT is not set"); + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new ArgumentException("AZURE_OPENAI_DEPLOY_NAME is not set"); + var openaiClient = new OpenAIClient(new Uri(endPoint), new Azure.AzureKeyCredential(key)); + var teacher = await CreateTeacherAgentAsync(openaiClient, deployName); + var student = await CreateStudentAssistantAgentAsync(openaiClient, deployName); + + var adminFunctionMiddleware = new FunctionCallMiddleware( + functions: [this.UpdateProgressFunctionContract], + functionMap: new Dictionary>> + { + { this.UpdateProgressFunctionContract.Name, this.UpdateProgressWrapper }, + }); + var admin = new OpenAIChatAgent( + openAIClient: openaiClient, + modelName: deployName, + name: "Admin", + systemMessage: $@"You are admin. You update progress after each question is answered.") + .RegisterMessageConnector() + .RegisterStreamingMiddleware(adminFunctionMiddleware) + .RegisterMiddleware(Print); + + var groupAdmin = new OpenAIChatAgent( + openAIClient: openaiClient, + modelName: deployName, + name: "GroupAdmin", + systemMessage: "You are group admin. You manage the group chat.") + .RegisterMessageConnector() + .RegisterMiddleware(Print); + await RunMathChatAsync(teacher, student, admin, groupAdmin); + } + + private async Task CreateTeacherAgentAsync(OpenAIClient client, string model) + { + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.CreateMathQuestionFunctionContract, this.AnswerIsCorrectFunctionContract], + functionMap: new Dictionary>> + { + { this.CreateMathQuestionFunctionContract.Name!, this.CreateMathQuestionWrapper }, + { this.AnswerIsCorrectFunctionContract.Name!, this.AnswerIsCorrectWrapper }, + }); + + var teacher = new OpenAIChatAgent( + openAIClient: client, + name: "Teacher", + systemMessage: @"You are a preschool math teacher. +You create math question and ask student to answer it. +Then you check if the answer is correct. +If the answer is wrong, you ask student to fix it", + modelName: model) + .RegisterMiddleware(Print) + .RegisterMiddleware(new OpenAIChatRequestMessageConnector()) + .RegisterMiddleware(functionCallMiddleware); + + return teacher; + } + + private async Task CreateStudentAssistantAgentAsync(OpenAIClient client, string model) + { + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.AnswerQuestionFunctionContract], + functionMap: new Dictionary>> + { + { this.AnswerQuestionFunctionContract.Name!, this.AnswerQuestionWrapper }, + }); + var student = new OpenAIChatAgent( + openAIClient: client, + name: "Student", + modelName: model, + systemMessage: @"You are a student. You answer math question from teacher.") + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware) + .RegisterMiddleware(Print); + + return student; + } + + private async Task RunMathChatAsync(IAgent teacher, IAgent student, IAgent admin, IAgent groupAdmin) + { + var teacher2Student = Transition.Create(teacher, student); + var student2Teacher = Transition.Create(student, teacher); + var teacher2Admin = Transition.Create(teacher, admin); + var admin2Teacher = Transition.Create(admin, teacher); + var workflow = new Graph( + [ + teacher2Student, + student2Teacher, + teacher2Admin, + admin2Teacher, + ]); + var group = new GroupChat( + workflow: workflow, + members: [ + admin, + teacher, + student, + ], + admin: groupAdmin); + + var groupChatManager = new GroupChatManager(group); + var chatHistory = await admin.InitiateChatAsync(groupChatManager, "teacher, create question", maxRound: 50); + + chatHistory.Where(msg => msg.From == teacher.Name && msg.GetContent()?.Contains("[MATH_QUESTION]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(this.round); + + chatHistory.Where(msg => msg.From == student.Name && msg.GetContent()?.Contains("[MATH_ANSWER]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(this.round); + + chatHistory.Where(msg => msg.From == teacher.Name && msg.GetContent()?.Contains("[ANSWER_IS_CORRECT]") is true) + .Count() + .Should().BeGreaterThanOrEqualTo(this.round); + + // check if there's terminate chat message from admin + chatHistory.Where(msg => msg.From == admin.Name && msg.IsGroupChatTerminateMessage()) + .Count() + .Should().Be(1); + } + } +} diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIChatAgentTest.cs b/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIChatAgentTest.cs new file mode 100644 index 000000000000..1000339c6886 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIChatAgentTest.cs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatAgentTest.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoGen.OpenAI.V1.Extension; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; + +namespace AutoGen.OpenAI.V1.Tests; + +public partial class OpenAIChatAgentTest +{ + /// + /// Get the weather for a location. + /// + /// location + /// + [Function] + public async Task GetWeatherAsync(string location) + { + return $"[GetWeather] The weather in {location} is sunny."; + } + + [Function] + public async Task CalculateTaxAsync(string location, double income) + { + return $"[CalculateTax] The tax in {location} for income {income} is 1000."; + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task BasicConversationTestAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: deployName); + + // By default, OpenAIChatClient supports the following message types + // - IMessage + var chatMessageContent = MessageEnvelope.Create(new ChatRequestUserMessage("Hello")); + var reply = await openAIChatAgent.SendAsync(chatMessageContent); + + reply.Should().BeOfType>(); + reply.As>().From.Should().Be("assistant"); + reply.As>().Content.Choices.First().Message.Role.Should().Be(ChatRole.Assistant); + reply.As>().Content.Usage.TotalTokens.Should().BeGreaterThan(0); + + // test streaming + var streamingReply = openAIChatAgent.GenerateStreamingReplyAsync(new[] { chatMessageContent }); + + await foreach (var streamingMessage in streamingReply) + { + streamingMessage.Should().BeOfType>(); + streamingMessage.As>().From.Should().Be("assistant"); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task OpenAIChatMessageContentConnectorTestAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: deployName); + + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage("Hello")), + new TextMessage(Role.Assistant, "Hello", from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, "Hello", from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await assistant.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + } + + // test streaming + foreach (var message in messages) + { + var reply = assistant.GenerateStreamingReplyAsync([message]); + + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task OpenAIChatAgentToolCallTestAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: deployName); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract]); + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + assistant.StreamingMiddlewares.Count().Should().Be(1); + var functionCallAgent = assistant + .RegisterStreamingMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.As().From.Should().Be("assistant"); + reply.As().ToolCalls.Count().Should().Be(1); + reply.As().ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + + // test streaming + foreach (var message in messages) + { + var reply = functionCallAgent.GenerateStreamingReplyAsync([message]); + ToolCallMessage? toolCallMessage = null; + await foreach (var streamingMessage in reply) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + if (toolCallMessage is null) + { + toolCallMessage = new ToolCallMessage(streamingMessage.As()); + } + else + { + toolCallMessage.Update(streamingMessage.As()); + } + } + + toolCallMessage.Should().NotBeNull(); + toolCallMessage!.From.Should().Be("assistant"); + toolCallMessage.ToolCalls.Count().Should().Be(1); + toolCallMessage.ToolCalls.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task OpenAIChatAgentToolCallInvokingTestAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + modelName: deployName); + + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [this.GetWeatherAsyncFunctionContract], + functionMap: new Dictionary>> { { this.GetWeatherAsyncFunctionContract.Name!, this.GetWeatherAsyncWrapper } }); + MiddlewareStreamingAgent assistant = openAIChatAgent + .RegisterMessageConnector(); + + var functionCallAgent = assistant + .RegisterStreamingMiddleware(functionCallMiddleware); + + var question = "What's the weather in Seattle"; + var messages = new IMessage[] + { + MessageEnvelope.Create(new ChatRequestUserMessage(question)), + new TextMessage(Role.Assistant, question, from: "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.Assistant, question, from: "user"), + ], + from: "user"), + }; + + foreach (var message in messages) + { + var reply = await functionCallAgent.SendAsync(message); + + reply.Should().BeOfType(); + reply.From.Should().Be("assistant"); + reply.GetToolCalls()!.Count().Should().Be(1); + reply.GetToolCalls()!.First().FunctionName.Should().Be(this.GetWeatherAsyncFunctionContract.Name); + reply.GetContent()!.ToLower().Should().Contain("seattle"); + } + + // test streaming + foreach (var message in messages) + { + var reply = functionCallAgent.GenerateStreamingReplyAsync([message]); + await foreach (var streamingMessage in reply) + { + if (streamingMessage is not IMessage) + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().From.Should().Be("assistant"); + } + else + { + streamingMessage.Should().BeOfType(); + streamingMessage.As().GetContent()!.ToLower().Should().Contain("seattle"); + } + } + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task ItCreateOpenAIChatAgentWithChatCompletionOptionAsync() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var options = new ChatCompletionsOptions(deployName, []) + { + Temperature = 0.7f, + MaxTokens = 1, + }; + + var openAIChatAgent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + options: options) + .RegisterMessageConnector(); + + var respond = await openAIChatAgent.SendAsync("hello"); + respond.GetContent()?.Should().NotBeNullOrEmpty(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task ItThrowExceptionWhenChatCompletionOptionContainsMessages() + { + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var options = new ChatCompletionsOptions(deployName, [new ChatRequestUserMessage("hi")]) + { + Temperature = 0.7f, + MaxTokens = 1, + }; + + var action = () => new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + options: options) + .RegisterMessageConnector(); + + action.Should().ThrowExactly().WithMessage("Messages should not be provided in options"); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task ItProduceValidContentAfterFunctionCall() + { + // https://github.com/microsoft/autogen/issues/3437 + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = CreateOpenAIClientFromAzureOpenAI(); + var options = new ChatCompletionsOptions(deployName, []) + { + Temperature = 0.7f, + MaxTokens = 1, + }; + + var agentName = "assistant"; + + var getWeatherToolCall = new ToolCall(this.GetWeatherAsyncFunctionContract.Name, "{\"location\":\"Seattle\"}"); + var getWeatherToolCallResult = new ToolCall(this.GetWeatherAsyncFunctionContract.Name, "{\"location\":\"Seattle\"}", "The weather in Seattle is sunny."); + var getWeatherToolCallMessage = new ToolCallMessage([getWeatherToolCall], from: agentName); + var getWeatherToolCallResultMessage = new ToolCallResultMessage([getWeatherToolCallResult], from: agentName); + var getWeatherAggregateMessage = new ToolCallAggregateMessage(getWeatherToolCallMessage, getWeatherToolCallResultMessage, from: agentName); + + var calculateTaxToolCall = new ToolCall(this.CalculateTaxAsyncFunctionContract.Name, "{\"location\":\"Seattle\",\"income\":1000}"); + var calculateTaxToolCallResult = new ToolCall(this.CalculateTaxAsyncFunctionContract.Name, "{\"location\":\"Seattle\",\"income\":1000}", "The tax in Seattle for income 1000 is 1000."); + var calculateTaxToolCallMessage = new ToolCallMessage([calculateTaxToolCall], from: agentName); + var calculateTaxToolCallResultMessage = new ToolCallResultMessage([calculateTaxToolCallResult], from: agentName); + var calculateTaxAggregateMessage = new ToolCallAggregateMessage(calculateTaxToolCallMessage, calculateTaxToolCallResultMessage, from: agentName); + + var chatHistory = new List() + { + new TextMessage(Role.User, "What's the weather in Seattle", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in Seattle is sunny, now check the tax in seattle", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in Paris", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in Paris is sunny, now check the tax in Paris", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in New York", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in New York is sunny, now check the tax in New York", from: "admin"), + calculateTaxAggregateMessage, + new TextMessage(Role.User, "what's the weather in London", from: "user"), + getWeatherAggregateMessage, + new TextMessage(Role.User, "The weather in London is sunny, now check the tax in London", from: "admin"), + }; + + var agent = new OpenAIChatAgent( + openAIClient: openaiClient, + name: "assistant", + options: options) + .RegisterMessageConnector(); + + var res = await agent.GenerateReplyAsync(chatHistory, new GenerateReplyOptions + { + MaxToken = 1024, + Functions = [this.GetWeatherAsyncFunctionContract, this.CalculateTaxAsyncFunctionContract], + }); + } + + private OpenAIClient CreateOpenAIClientFromAzureOpenAI() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + return new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + } +} diff --git a/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIMessageTests.cs b/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIMessageTests.cs new file mode 100644 index 000000000000..876416fdc333 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.V1.Tests/OpenAIMessageTests.cs @@ -0,0 +1,731 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIMessageTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; +using Xunit; + +namespace AutoGen.OpenAI.V1.Tests; + +public class OpenAIMessageTests +{ + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IgnoreReadOnlyProperties = false, + }; + + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + public void BasicMessageTest() + { + IMessage[] messages = [ + new TextMessage(Role.System, "You are a helpful AI assistant"), + new TextMessage(Role.User, "Hello", "user"), + new TextMessage(Role.Assistant, "How can I help you?", from: "assistant"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + new MultiModalMessage(Role.Assistant, + [ + new TextMessage(Role.User, "Hello", "user"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + ], "user"), + new ToolCallMessage("test", "test", "assistant"), + new ToolCallResultMessage("result", "test", "test", "user"), + new ToolCallResultMessage( + [ + new ToolCall("result", "test", "test"), + new ToolCall("result", "test", "test"), + ], "user"), + new ToolCallMessage( + [ + new ToolCall("test", "test"), + new ToolCall("test", "test"), + ], "assistant"), + new AggregateMessage( + message1: new ToolCallMessage("test", "test", "assistant"), + message2: new ToolCallResultMessage("result", "test", "test", "assistant"), "assistant"), + ]; + var openaiMessageConnectorMiddleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant"); + + var oaiMessages = messages.Select(m => (m, openaiMessageConnectorMiddleware.ProcessIncomingMessages(agent, [m]))); + VerifyOAIMessages(oaiMessages); + } + + [Fact] + public async Task ItProcessUserTextMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("Hello"); + chatRequestMessage.Name.Should().Be("user"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new TextMessage(Role.User, "Hello", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItShortcutChatRequestMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("hello"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var userMessage = new ChatRequestUserMessage("hello"); + var chatRequestMessage = MessageEnvelope.Create(userMessage); + await agent.GenerateReplyAsync([chatRequestMessage]); + } + + [Fact] + public async Task ItShortcutMessageWhenStrictModelIsFalseAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + + var chatRequestMessage = ((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Should().Be("hello"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var userMessage = "hello"; + var chatRequestMessage = MessageEnvelope.Create(userMessage); + await agent.GenerateReplyAsync([chatRequestMessage]); + } + + [Fact] + public async Task ItThrowExceptionWhenStrictModeIsTrueAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // user message + var userMessage = "hello"; + var chatRequestMessage = MessageEnvelope.Create(userMessage); + Func action = async () => await agent.GenerateReplyAsync([chatRequestMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: MessageEnvelope`1"); + } + + [Fact] + public async Task ItProcessAssistantTextMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("How can I help you?"); + chatRequestMessage.Name.Should().Be("assistant"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // assistant message + IMessage message = new TextMessage(Role.Assistant, "How can I help you?", "assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessSystemTextMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestSystemMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("You are a helpful AI assistant"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // system message + IMessage message = new TextMessage(Role.System, "You are a helpful AI assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessImageMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + chatRequestMessage.Name.Should().Be("user"); + chatRequestMessage.MultimodalContentItems.Count().Should().Be(1); + chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ImageMessage(Role.User, "https://example.com/image.png", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingImageMessageFromSelfAndStrictModeIsTrueAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var imageMessage = new ImageMessage(Role.Assistant, "https://example.com/image.png", "assistant"); + Func action = async () => await agent.GenerateReplyAsync([imageMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: ImageMessage"); + } + + [Fact] + public async Task ItProcessMultiModalMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + chatRequestMessage.Name.Should().Be("user"); + chatRequestMessage.MultimodalContentItems.Count().Should().Be(2); + chatRequestMessage.MultimodalContentItems.First().Should().BeOfType(); + chatRequestMessage.MultimodalContentItems.Last().Should().BeOfType(); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new MultiModalMessage( + Role.User, + [ + new TextMessage(Role.User, "Hello", "user"), + new ImageMessage(Role.User, "https://example.com/image.png", "user"), + ], "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingMultiModalMessageFromSelfAndStrictModeIsTrueAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var multiModalMessage = new MultiModalMessage( + Role.Assistant, + [ + new TextMessage(Role.User, "Hello", "assistant"), + new ImageMessage(Role.User, "https://example.com/image.png", "assistant"), + ], "assistant"); + + Func action = async () => await agent.GenerateReplyAsync([multiModalMessage]); + + await action.Should().ThrowAsync().WithMessage("Invalid message type: MultiModalMessage"); + } + + [Fact] + public async Task ItProcessToolCallMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + // when the message is a tool call message + // the name field should not be set + // please visit OpenAIChatRequestMessageConnector class for more information + chatRequestMessage.Name.Should().BeNullOrEmpty(); + chatRequestMessage.ToolCalls.Count().Should().Be(1); + chatRequestMessage.Content.Should().Be("textContent"); + chatRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.First(); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be("test"); + functionToolCall.Arguments.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ToolCallMessage("test", "test", "assistant") + { + Content = "textContent", + }; + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessParallelToolCallMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().BeNullOrEmpty(); + + // when the message is a tool call message + // the name field should not be set + // please visit OpenAIChatRequestMessageConnector class for more information + chatRequestMessage.Name.Should().BeNullOrEmpty(); + chatRequestMessage.ToolCalls.Count().Should().Be(2); + for (int i = 0; i < chatRequestMessage.ToolCalls.Count(); i++) + { + chatRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)chatRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be($"test_{i}"); + functionToolCall.Arguments.Should().Be("test"); + } + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test"), + new ToolCall("test", "test"), + }; + IMessage message = new ToolCallMessage(toolCalls, "assistant"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItThrowExceptionWhenProcessingToolCallMessageFromUserAndStrictModeIsTrueAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(strictMode: true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + var toolCallMessage = new ToolCallMessage("test", "test", "user"); + Func action = async () => await agent.GenerateReplyAsync([toolCallMessage]); + await action.Should().ThrowAsync().WithMessage("Invalid message type: ToolCallMessage"); + } + + [Fact] + public async Task ItProcessToolCallResultMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + IMessage message = new ToolCallResultMessage("result", "test", "test", "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessParallelToolCallResultMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(2); + + for (int i = 0; i < msgs.Count(); i++) + { + var innerMessage = msgs.ElementAt(i); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be($"test_{i}"); + } + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test", "result"), + new ToolCall("test", "test", "result"), + }; + IMessage message = new ToolCallResultMessage(toolCalls, "user"); + await agent.GenerateReplyAsync([message]); + } + + [Fact] + public async Task ItProcessFunctionCallMiddlewareMessageFromUserAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestUserMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.Name.Should().Be("user"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCallMessage = new ToolCallMessage("test", "test", "user"); + var toolCallResultMessage = new ToolCallResultMessage("result", "test", "test", "user"); + var aggregateMessage = new AggregateMessage(toolCallMessage, toolCallResultMessage, "user"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItProcessFunctionCallMiddlewareMessageFromAssistantAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(2); + var innerMessage = msgs.Last(); + innerMessage!.Should().BeOfType>(); + var chatRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)innerMessage!).Content; + chatRequestMessage.Content.Should().Be("result"); + chatRequestMessage.ToolCallId.Should().Be("test"); + + var toolCallMessage = msgs.First(); + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallRequestMessage.Content.Should().BeNullOrEmpty(); + toolCallRequestMessage.ToolCalls.Count().Should().Be(1); + toolCallRequestMessage.ToolCalls.First().Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.First(); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be("test"); + functionToolCall.Arguments.Should().Be("test"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCallMessage = new ToolCallMessage("test", "test", "assistant"); + var toolCallResultMessage = new ToolCallResultMessage("result", "test", "test", "assistant"); + var aggregateMessage = new ToolCallAggregateMessage(toolCallMessage, toolCallResultMessage, "assistant"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItProcessParallelFunctionCallMiddlewareMessageFromAssistantAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, _) => + { + msgs.Count().Should().Be(3); + var toolCallMessage = msgs.First(); + toolCallMessage!.Should().BeOfType>(); + var toolCallRequestMessage = (ChatRequestAssistantMessage)((MessageEnvelope)toolCallMessage!).Content; + toolCallRequestMessage.Content.Should().BeNullOrEmpty(); + toolCallRequestMessage.ToolCalls.Count().Should().Be(2); + + for (int i = 0; i < toolCallRequestMessage.ToolCalls.Count(); i++) + { + toolCallRequestMessage.ToolCalls.ElementAt(i).Should().BeOfType(); + var functionToolCall = (ChatCompletionsFunctionToolCall)toolCallRequestMessage.ToolCalls.ElementAt(i); + functionToolCall.Name.Should().Be("test"); + functionToolCall.Id.Should().Be($"test_{i}"); + functionToolCall.Arguments.Should().Be("test"); + } + + for (int i = 1; i < msgs.Count(); i++) + { + var toolCallResultMessage = msgs.ElementAt(i); + toolCallResultMessage!.Should().BeOfType>(); + var toolCallResultRequestMessage = (ChatRequestToolMessage)((MessageEnvelope)toolCallResultMessage!).Content; + toolCallResultRequestMessage.Content.Should().Be("result"); + toolCallResultRequestMessage.ToolCallId.Should().Be($"test_{i - 1}"); + } + + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(middleware); + + // user message + var toolCalls = new[] + { + new ToolCall("test", "test", "result"), + new ToolCall("test", "test", "result"), + }; + var toolCallMessage = new ToolCallMessage(toolCalls, "assistant"); + var toolCallResultMessage = new ToolCallResultMessage(toolCalls, "assistant"); + var aggregateMessage = new AggregateMessage(toolCallMessage, toolCallResultMessage, "assistant"); + await agent.GenerateReplyAsync([aggregateMessage]); + } + + [Fact] + public async Task ItConvertChatResponseMessageToTextMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = CreateInstance(ChatRole.Assistant, "hello"); + var chatRequestMessage = MessageEnvelope.Create(textMessage); + + var message = await agent.GenerateReplyAsync([chatRequestMessage]); + message.Should().BeOfType(); + message.GetContent().Should().Be("hello"); + message.GetRole().Should().Be(Role.Assistant); + } + + [Fact] + public async Task ItConvertChatResponseMessageToToolCallMessageAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // tool call message + var toolCallMessage = CreateInstance(ChatRole.Assistant, "textContent", new[] { new ChatCompletionsFunctionToolCall("test", "test", "test") }, new FunctionCall("test", "test"), CreateInstance(), new Dictionary()); + var chatRequestMessage = MessageEnvelope.Create(toolCallMessage); + var message = await agent.GenerateReplyAsync([chatRequestMessage]); + message.Should().BeOfType(); + message.GetToolCalls()!.Count().Should().Be(1); + message.GetToolCalls()!.First().FunctionName.Should().Be("test"); + message.GetToolCalls()!.First().FunctionArguments.Should().Be("test"); + message.GetContent().Should().Be("textContent"); + } + + [Fact] + public async Task ItReturnOriginalMessageWhenStrictModeIsFalseAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = "hello"; + var messageToSend = MessageEnvelope.Create(textMessage); + + var message = await agent.GenerateReplyAsync([messageToSend]); + message.Should().BeOfType>(); + } + + [Fact] + public async Task ItThrowInvalidOperationExceptionWhenStrictModeIsTrueAsync() + { + var middleware = new OpenAIChatRequestMessageConnector(true); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(middleware); + + // text message + var textMessage = new ChatRequestUserMessage("hello"); + var messageToSend = MessageEnvelope.Create(textMessage); + Func action = async () => await agent.GenerateReplyAsync([messageToSend]); + + await action.Should().ThrowAsync().WithMessage("Invalid return message type MessageEnvelope`1"); + } + + [Fact] + public void ToOpenAIChatRequestMessageShortCircuitTest() + { + var agent = new EchoAgent("assistant"); + var middleware = new OpenAIChatRequestMessageConnector(); + ChatRequestMessage[] messages = + [ + new ChatRequestUserMessage("Hello"), + new ChatRequestAssistantMessage("How can I help you?"), + new ChatRequestSystemMessage("You are a helpful AI assistant"), + new ChatRequestFunctionMessage("result", "functionName"), + new ChatRequestToolMessage("test", "test"), + ]; + + foreach (var oaiMessage in messages) + { + IMessage message = new MessageEnvelope(oaiMessage); + var oaiMessages = middleware.ProcessIncomingMessages(agent, [message]); + oaiMessages.Count().Should().Be(1); + //oaiMessages.First().Should().BeOfType>(); + if (oaiMessages.First() is IMessage chatRequestMessage) + { + chatRequestMessage.Content.Should().Be(oaiMessage); + } + else + { + // fail the test + Assert.True(false); + } + } + } + private void VerifyOAIMessages(IEnumerable<(IMessage, IEnumerable)> messages) + { + var jsonObjects = messages.Select(pair => + { + var (originalMessage, ms) = pair; + var objs = new List(); + foreach (var m in ms) + { + object? obj = null; + var chatRequestMessage = (m as IMessage)?.Content; + if (chatRequestMessage is ChatRequestUserMessage userMessage) + { + obj = new + { + Role = userMessage.Role.ToString(), + Content = userMessage.Content, + Name = userMessage.Name, + MultiModaItem = userMessage.MultimodalContentItems?.Select(item => + { + return item switch + { + ChatMessageImageContentItem imageContentItem => new + { + Type = "Image", + ImageUrl = GetImageUrlFromContent(imageContentItem), + } as object, + ChatMessageTextContentItem textContentItem => new + { + Type = "Text", + Text = textContentItem.Text, + } as object, + _ => throw new System.NotImplementedException(), + }; + }), + }; + } + + if (chatRequestMessage is ChatRequestAssistantMessage assistantMessage) + { + obj = new + { + Role = assistantMessage.Role.ToString(), + Content = assistantMessage.Content, + Name = assistantMessage.Name, + TooCall = assistantMessage.ToolCalls.Select(tc => + { + return tc switch + { + ChatCompletionsFunctionToolCall functionToolCall => new + { + Type = "Function", + Name = functionToolCall.Name, + Arguments = functionToolCall.Arguments, + Id = functionToolCall.Id, + } as object, + _ => throw new System.NotImplementedException(), + }; + }), + FunctionCallName = assistantMessage.FunctionCall?.Name, + FunctionCallArguments = assistantMessage.FunctionCall?.Arguments, + }; + } + + if (chatRequestMessage is ChatRequestSystemMessage systemMessage) + { + obj = new + { + Name = systemMessage.Name, + Role = systemMessage.Role.ToString(), + Content = systemMessage.Content, + }; + } + + if (chatRequestMessage is ChatRequestFunctionMessage functionMessage) + { + obj = new + { + Role = functionMessage.Role.ToString(), + Content = functionMessage.Content, + Name = functionMessage.Name, + }; + } + + if (chatRequestMessage is ChatRequestToolMessage toolCallMessage) + { + obj = new + { + Role = toolCallMessage.Role.ToString(), + Content = toolCallMessage.Content, + ToolCallId = toolCallMessage.ToolCallId, + }; + } + + objs.Add(obj ?? throw new System.NotImplementedException()); + } + + return new + { + OriginalMessage = originalMessage.ToString(), + ConvertedMessages = objs, + }; + }); + + var json = JsonSerializer.Serialize(jsonObjects, this.jsonSerializerOptions); + Approvals.Verify(json); + } + + private object? GetImageUrlFromContent(ChatMessageImageContentItem content) + { + return content.GetType().GetProperty("ImageUrl", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(content); + } + + private static T CreateInstance(params object[] args) + { + var type = typeof(T); + var instance = type.Assembly.CreateInstance( + type.FullName!, false, + BindingFlags.Instance | BindingFlags.NonPublic, + null, args, null, null); + return (T)instance!; + } +} diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt index 677831d412b7..eb346da3b313 100644 --- a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt @@ -14,8 +14,7 @@ "Name": "message", "Description": "", "ParameterType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", - "IsRequired": true, - "DefaultValue": "" + "IsRequired": true } ], "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt index ee835b1ba081..9ed3c675e4a0 100644 --- a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt @@ -16,8 +16,7 @@ "Name": "newState", "Description": "new state", "ParameterType": "System.Boolean, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", - "IsRequired": true, - "DefaultValue": "" + "IsRequired": true } ], "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj b/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj index 8be4b55b1722..6ff942ea3ba2 100644 --- a/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj @@ -1,7 +1,7 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) enable false $(NoWarn);SKEXP0110 @@ -10,10 +10,10 @@ + - - + diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs index 3e955c8ecbcc..0dc2ea215ddf 100644 --- a/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs @@ -5,6 +5,7 @@ using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; using AutoGen.Tests; +using Azure; using Azure.AI.OpenAI; using FluentAssertions; using Microsoft.SemanticKernel; @@ -19,13 +20,15 @@ public async Task ItRegisterKernelFunctionMiddlewareFromTestPluginTests() var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = new AzureOpenAIClient( + endpoint: new Uri(endpoint), + credential: new AzureKeyCredential(key)); var kernel = new Kernel(); var plugin = kernel.ImportPluginFromType(); var kernelFunctionMiddleware = new KernelPluginMiddleware(kernel, plugin); - var agent = new OpenAIChatAgent(openaiClient, "assistant", modelName: deployName) + var agent = new OpenAIChatAgent(openaiClient.GetChatClient(deployName), "assistant") .RegisterMessageConnector() .RegisterMiddleware(kernelFunctionMiddleware); @@ -63,7 +66,9 @@ public async Task ItRegisterKernelFunctionMiddlewareFromMethodTests() var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); - var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + var openaiClient = new AzureOpenAIClient( + endpoint: new Uri(endpoint), + credential: new AzureKeyCredential(key)); var kernel = new Kernel(); var getWeatherMethod = kernel.CreateFunctionFromMethod((string location) => $"The weather in {location} is sunny.", functionName: "GetWeather", description: "Get the weather for a location."); @@ -71,7 +76,7 @@ public async Task ItRegisterKernelFunctionMiddlewareFromMethodTests() var plugin = kernel.ImportPluginFromFunctions("plugin", [getWeatherMethod, createPersonObjectMethod]); var kernelFunctionMiddleware = new KernelPluginMiddleware(kernel, plugin); - var agent = new OpenAIChatAgent(openaiClient, "assistant", modelName: deployName) + var agent = new OpenAIChatAgent(chatClient: openaiClient.GetChatClient(deployName), "assistant") .RegisterMessageConnector() .RegisterMiddleware(kernelFunctionMiddleware); diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs b/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs index d9dde62f1bd0..dc1b655a7a4f 100644 --- a/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs @@ -34,8 +34,11 @@ public async Task BasicConversationTestAsync() var builder = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion(deploymentName, endpoint, key); + var kernel = builder.Build(); + kernel.GetRequiredService(); + var skAgent = new SemanticKernelAgent(kernel, "assistant"); var chatMessageContent = MessageEnvelope.Create(new ChatMessageContent(AuthorRole.Assistant, "Hello")); @@ -223,11 +226,10 @@ public async Task SkChatCompletionAgentPluginTestAsync() Kernel = kernel, Name = "assistant", Instructions = "You are a helpful AI assistant", - ExecutionSettings = - new OpenAIPromptExecutionSettings() - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - } + Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }) }; var skAgent = new SemanticKernelChatCompletionAgent(agent).RegisterMiddleware( diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionCallTemplateTests.TestFunctionCallTemplate.approved.txt b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionCallTemplateTests.TestFunctionCallTemplate.approved.txt index 0439febc52c7..ea5a8585cc2f 100644 --- a/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionCallTemplateTests.TestFunctionCallTemplate.approved.txt +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/ApprovalTests/FunctionCallTemplateTests.TestFunctionCallTemplate.approved.txt @@ -8,7 +8,6 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using System; using AutoGen.Core; -using AutoGen.OpenAI.Extension; namespace AutoGen.SourceGenerator.Tests { @@ -61,11 +60,6 @@ namespace AutoGen.SourceGenerator.Tests }, }; } - - public global::Azure.AI.OpenAI.FunctionDefinition AddAsyncFunction - { - get => this.AddAsyncFunctionContract.ToOpenAIFunctionDefinition(); - } } } diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj b/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj index 2e0ead045bef..f7d814a6cdef 100644 --- a/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/AutoGen.SourceGenerator.Tests.csproj @@ -1,7 +1,7 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) enable false True diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionCallTemplateEncodingTests.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionCallTemplateEncodingTests.cs new file mode 100644 index 000000000000..0b2e211c6386 --- /dev/null +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionCallTemplateEncodingTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// FunctionCallTemplateEncodingTests.cs + +using System.Text.Json; // Needed for JsonSerializer +using AutoGen.SourceGenerator.Template; // Needed for FunctionCallTemplate +using Xunit; // Needed for Fact and Assert + +namespace AutoGen.SourceGenerator.Tests +{ + public class FunctionCallTemplateEncodingTests + { + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + }; + + [Fact] + public void FunctionDescription_Should_Encode_DoubleQuotes() + { + // Arrange + var functionContracts = new List + { + new SourceGeneratorFunctionContract + { + Name = "TestFunction", + Description = "This is a \"test\" function", + Parameters = new SourceGeneratorParameterContract[] + { + new SourceGeneratorParameterContract + { + Name = "param1", + Description = "This is a \"parameter\" description", + Type = "string", + IsOptional = false + } + }, + ReturnType = "void" + } + }; + + var template = new FunctionCallTemplate + { + NameSpace = "TestNamespace", + ClassName = "TestClass", + FunctionContracts = functionContracts + }; + + // Act + var result = template.TransformText(); + + // Assert + Assert.Contains("Description = @\"This is a \"\"test\"\" function\"", result); + Assert.Contains("Description = @\"This is a \"\"parameter\"\" description\"", result); + } + + [Fact] + public void ParameterDescription_Should_Encode_DoubleQuotes() + { + // Arrange + var functionContracts = new List + { + new SourceGeneratorFunctionContract + { + Name = "TestFunction", + Description = "This is a test function", + Parameters = new SourceGeneratorParameterContract[] + { + new SourceGeneratorParameterContract + { + Name = "param1", + Description = "This is a \"parameter\" description", + Type = "string", + IsOptional = false + } + }, + ReturnType = "void" + } + }; + + var template = new FunctionCallTemplate + { + NameSpace = "TestNamespace", + ClassName = "TestClass", + FunctionContracts = functionContracts + }; + + // Act + var result = template.TransformText(); + + // Assert + Assert.Contains("Description = @\"This is a \"\"parameter\"\" description\"", result); + } + } +} diff --git a/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs index f7b90e0b96ff..8b477446d9f9 100644 --- a/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs +++ b/dotnet/test/AutoGen.SourceGenerator.Tests/FunctionExample.test.cs @@ -5,8 +5,9 @@ using ApprovalTests; using ApprovalTests.Namers; using ApprovalTests.Reporters; -using Azure.AI.OpenAI; +using AutoGen.OpenAI.Extension; using FluentAssertions; +using OpenAI.Chat; using Xunit; namespace AutoGen.SourceGenerator.Tests @@ -29,7 +30,7 @@ public void Add_Test() }; this.VerifyFunction(functionExamples.AddWrapper, args, 3); - this.VerifyFunctionDefinition(functionExamples.AddFunction); + this.VerifyFunctionDefinition(functionExamples.AddFunctionContract.ToChatTool()); } [Fact] @@ -41,7 +42,7 @@ public void Sum_Test() }; this.VerifyFunction(functionExamples.SumWrapper, args, 6.0); - this.VerifyFunctionDefinition(functionExamples.SumFunction); + this.VerifyFunctionDefinition(functionExamples.SumFunctionContract.ToChatTool()); } [Fact] @@ -57,7 +58,7 @@ public async Task DictionaryToString_Test() }; await this.VerifyAsyncFunction(functionExamples.DictionaryToStringAsyncWrapper, args, JsonSerializer.Serialize(args.xargs, jsonSerializerOptions)); - this.VerifyFunctionDefinition(functionExamples.DictionaryToStringAsyncFunction); + this.VerifyFunctionDefinition(functionExamples.DictionaryToStringAsyncFunctionContract.ToChatTool()); } [Fact] @@ -96,18 +97,18 @@ public void Query_Test() }; this.VerifyFunction(functionExamples.QueryWrapper, args, new[] { "hello", "hello", "hello" }); - this.VerifyFunctionDefinition(functionExamples.QueryFunction); + this.VerifyFunctionDefinition(functionExamples.QueryFunctionContract.ToChatTool()); } [UseReporter(typeof(DiffReporter))] [UseApprovalSubdirectory("ApprovalTests")] - private void VerifyFunctionDefinition(FunctionDefinition function) + private void VerifyFunctionDefinition(ChatTool function) { var func = new { - name = function.Name, - description = function.Description.Replace(Environment.NewLine, ","), - parameters = function.Parameters.ToObjectFromJson(options: jsonSerializerOptions), + name = function.FunctionName, + description = function.FunctionDescription.Replace(Environment.NewLine, ","), + parameters = function.FunctionParameters.ToObjectFromJson(options: jsonSerializerOptions), }; Approvals.Verify(JsonSerializer.Serialize(func, jsonSerializerOptions)); diff --git a/dotnet/test/AutoGen.Test.Share/Attribute/EnvironmentSpecificFactAttribute.cs b/dotnet/test/AutoGen.Test.Share/Attribute/EnvironmentSpecificFactAttribute.cs new file mode 100644 index 000000000000..1361531cc9ed --- /dev/null +++ b/dotnet/test/AutoGen.Test.Share/Attribute/EnvironmentSpecificFactAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// EnvironmentSpecificFactAttribute.cs + +using Xunit; + +namespace AutoGen.Tests; + +/// +/// A base class for environment-specific fact attributes. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public abstract class EnvironmentSpecificFactAttribute : FactAttribute +{ + private readonly string _skipMessage; + + /// + /// Creates a new instance of the class. + /// + /// The message to be used when skipping the test marked with this attribute. + protected EnvironmentSpecificFactAttribute(string skipMessage) + { + _skipMessage = skipMessage ?? throw new ArgumentNullException(nameof(skipMessage)); + } + + public sealed override string Skip => IsEnvironmentSupported() ? string.Empty : _skipMessage; + + /// + /// A method used to evaluate whether to skip a test marked with this attribute. Skips iff this method evaluates to false. + /// + protected abstract bool IsEnvironmentSupported(); +} diff --git a/dotnet/test/AutoGen.Test.Share/Attribute/OpenAIFact.cs b/dotnet/test/AutoGen.Test.Share/Attribute/OpenAIFact.cs new file mode 100644 index 000000000000..54d72cd61ab7 --- /dev/null +++ b/dotnet/test/AutoGen.Test.Share/Attribute/OpenAIFact.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIFact.cs + +namespace AutoGen.Tests; + +/// +/// A fact for tests requiring OPENAI_API_KEY env. +/// +public sealed class ApiKeyFactAttribute : EnvironmentSpecificFactAttribute +{ + private readonly string[] _envVariableNames; + public ApiKeyFactAttribute(params string[] envVariableNames) : base($"{envVariableNames} is not found in env") + { + _envVariableNames = envVariableNames; + } + + /// + protected override bool IsEnvironmentSupported() + { + return _envVariableNames.All(Environment.GetEnvironmentVariables().Contains); + } +} diff --git a/dotnet/test/AutoGen.Test.Share/AutoGen.Tests.Share.csproj b/dotnet/test/AutoGen.Test.Share/AutoGen.Tests.Share.csproj new file mode 100644 index 000000000000..21c71896ddc7 --- /dev/null +++ b/dotnet/test/AutoGen.Test.Share/AutoGen.Tests.Share.csproj @@ -0,0 +1,15 @@ + + + + $(TestTargetFrameworks) + enable + false + True + enable + + + + + + + diff --git a/dotnet/test/AutoGen.Test.Share/EchoAgent.cs b/dotnet/test/AutoGen.Test.Share/EchoAgent.cs new file mode 100644 index 000000000000..010b72d2add0 --- /dev/null +++ b/dotnet/test/AutoGen.Test.Share/EchoAgent.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// EchoAgent.cs + +using System.Runtime.CompilerServices; +using AutoGen.Core; + +namespace AutoGen.Tests; + +public class EchoAgent : IStreamingAgent +{ + public EchoAgent(string name) + { + Name = name; + } + public string Name { get; } + + public Task GenerateReplyAsync( + IEnumerable conversation, + GenerateReplyOptions? options = null, + CancellationToken ct = default) + { + // return the most recent message + var lastMessage = conversation.Last(); + lastMessage.From = this.Name; + + return Task.FromResult(lastMessage); + } + + public async IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var message in messages) + { + message.From = this.Name; + yield return message; + } + } +} diff --git a/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs b/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs deleted file mode 100644 index 1042dec6f271..000000000000 --- a/dotnet/test/AutoGen.Tests/Attribute/EnvironmentSpecificFactAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// EnvironmentSpecificFactAttribute.cs - -using System; -using Xunit; - -namespace AutoGen.Tests -{ - /// - /// A base class for environment-specific fact attributes. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public abstract class EnvironmentSpecificFactAttribute : FactAttribute - { - private readonly string _skipMessage; - - /// - /// Creates a new instance of the class. - /// - /// The message to be used when skipping the test marked with this attribute. - protected EnvironmentSpecificFactAttribute(string skipMessage) - { - _skipMessage = skipMessage ?? throw new ArgumentNullException(nameof(skipMessage)); - } - - public sealed override string Skip => IsEnvironmentSupported() ? string.Empty : _skipMessage; - - /// - /// A method used to evaluate whether to skip a test marked with this attribute. Skips iff this method evaluates to false. - /// - protected abstract bool IsEnvironmentSupported(); - } -} diff --git a/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs b/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs deleted file mode 100644 index 44457d8f571c..000000000000 --- a/dotnet/test/AutoGen.Tests/Attribute/OpenAIFact.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// OpenAIFact.cs - -using System; -using System.Linq; - -namespace AutoGen.Tests -{ - /// - /// A fact for tests requiring OPENAI_API_KEY env. - /// - public sealed class ApiKeyFactAttribute : EnvironmentSpecificFactAttribute - { - private readonly string[] _envVariableNames; - public ApiKeyFactAttribute(params string[] envVariableNames) : base($"{envVariableNames} is not found in env") - { - _envVariableNames = envVariableNames; - } - - /// - protected override bool IsEnvironmentSupported() - { - return _envVariableNames.All(Environment.GetEnvironmentVariables().Contains); - } - } -} diff --git a/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj index 4def281ed7b4..a0c3b815f22b 100644 --- a/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj +++ b/dotnet/test/AutoGen.Tests/AutoGen.Tests.csproj @@ -1,7 +1,7 @@  - $(TestTargetFramework) + $(TestTargetFrameworks) True True $(NoWarn);xUnit1013;SKEXP0110 @@ -9,8 +9,10 @@ + + diff --git a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs index 8f2b9b2de51b..317fdc36e019 100644 --- a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs +++ b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs @@ -31,17 +31,12 @@ public async Task TwoAgentMathClassTestAsync() await Example02_TwoAgent_MathChat.RunAsync(); } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + [ApiKeyFact("OPENAI_API_KEY")] public async Task AgentFunctionCallTestAsync() { await Example03_Agent_FunctionCall.RunAsync(); } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] - public async Task OpenAIAgent_JsonMode() - { - await Example13_OpenAIAgent_JsonMode.RunAsync(); - } [ApiKeyFact("MISTRAL_API_KEY")] public async Task MistralClientAgent_TokenCount() @@ -49,12 +44,6 @@ public async Task MistralClientAgent_TokenCount() await Example14_MistralClientAgent_TokenCount.RunAsync(); } - [ApiKeyFact("OPENAI_API_KEY")] - public async Task DynamicGroupChatGetMLNetPRTestAsync() - { - await Example04_Dynamic_GroupChat_Coding_Task.RunAsync(); - } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task DynamicGroupChatCalculateFibonacciAsync() { diff --git a/dotnet/test/AutoGen.Tests/EchoAgent.cs b/dotnet/test/AutoGen.Tests/EchoAgent.cs deleted file mode 100644 index 9cead5ad2516..000000000000 --- a/dotnet/test/AutoGen.Tests/EchoAgent.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// EchoAgent.cs - -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace AutoGen.Tests -{ - public class EchoAgent : IStreamingAgent - { - public EchoAgent(string name) - { - Name = name; - } - public string Name { get; } - - public Task GenerateReplyAsync( - IEnumerable conversation, - GenerateReplyOptions? options = null, - CancellationToken ct = default) - { - // return the most recent message - var lastMessage = conversation.Last(); - lastMessage.From = this.Name; - - return Task.FromResult(lastMessage); - } - - public async IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - foreach (var message in messages) - { - message.From = this.Name; - yield return message; - } - } - } -} diff --git a/dotnet/test/AutoGen.Tests/GroupChat/GraphTests.cs b/dotnet/test/AutoGen.Tests/GroupChat/GraphTests.cs index 77e2c99dcd13..7eeea6743f04 100644 --- a/dotnet/test/AutoGen.Tests/GroupChat/GraphTests.cs +++ b/dotnet/test/AutoGen.Tests/GroupChat/GraphTests.cs @@ -1,4 +1,6 @@ - +// Copyright (c) Microsoft Corporation. All rights reserved. +// GraphTests.cs + using Xunit; namespace AutoGen.Tests diff --git a/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs b/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs new file mode 100644 index 000000000000..9c2d2ce8197a --- /dev/null +++ b/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GroupChatTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Xunit; + +namespace AutoGen.Tests; + +public class GroupChatTests +{ + [Fact] + public async Task ItSendMessageTestAsync() + { + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + + var groupChat = new GroupChat([alice, bob]); + + var chatHistory = new List(); + + var maxRound = 10; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(10); + } + + [Fact] + public async Task ItTerminateConversationWhenAgentReturnTerminateKeyWord() + { + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + var cathy = new DefaultReplyAgent("Cathy", $"I am cathy, {GroupChatExtension.TERMINATE}"); + + var groupChat = new GroupChat([alice, bob, cathy]); + + var chatHistory = new List(); + + var maxRound = 10; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(3); + chatHistory.Last().From.Should().Be("Cathy"); + } + + [Fact] + public async Task ItSendAsyncDoesntAddDuplicateInitializeMessagesTest() + { + // fix #3268 + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + var cathy = new DefaultReplyAgent("Cathy", $"I am cathy, {GroupChatExtension.TERMINATE}"); + + var roundRobinOrchestrator = new RoundRobinOrchestrator(); + var orchestrator = Mock.Of(); + Mock.Get(orchestrator).Setup(x => x.GetNextSpeakerAsync(It.IsAny(), It.IsAny())) + .Returns((OrchestrationContext context, CancellationToken token) => + { + // determine if initialize message is already sent and not added twice + context.ChatHistory.Where(x => x.From == alice.Name).Count().Should().Be(1); + + return roundRobinOrchestrator.GetNextSpeakerAsync(context, token); + }); + + var groupChat = new GroupChat([alice, bob, cathy], orchestrator); + groupChat.AddInitializeMessage(new TextMessage(Role.User, "Hello", from: alice.Name)); + + var maxRound = 2; + var chatHistory = new List(); + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(2); + } + + [Fact] + public async Task ItTerminateConversationWhenNoSpeakerAvailable() + { + // fix #3306 + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + var cathy = new DefaultReplyAgent("Cathy", $"I am cathy, {GroupChatExtension.TERMINATE}"); + + var orchestrator = Mock.Of(); + Mock.Get(orchestrator).Setup(x => x.GetNextSpeakerAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((IAgent?)null); + + var groupChat = new GroupChat([alice, bob, cathy], orchestrator); + + var chatHistory = new List(); + + var maxRound = 10; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(0); + } +} diff --git a/dotnet/test/AutoGen.Tests/MiddlewareTest.cs b/dotnet/test/AutoGen.Tests/MiddlewareTest.cs index 6c1c89a33c1c..6398a24f5c57 100644 --- a/dotnet/test/AutoGen.Tests/MiddlewareTest.cs +++ b/dotnet/test/AutoGen.Tests/MiddlewareTest.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Azure.AI.OpenAI; using FluentAssertions; using Xunit; @@ -73,7 +72,7 @@ public async Task FunctionCallMiddlewareTestAsync() var agent = new EchoAgent("echo"); var args = new EchoSchema { message = "hello" }; var argsJson = JsonSerializer.Serialize(args) ?? throw new InvalidOperationException("Failed to serialize args"); - var functionCall = new FunctionCall("echo", argsJson); + var functionCall = new ToolCall("echo", argsJson); var functionCallAgent = agent.RegisterMiddleware(async (messages, options, agent, ct) => { if (options?.Functions is null) @@ -81,7 +80,7 @@ public async Task FunctionCallMiddlewareTestAsync() return await agent.GenerateReplyAsync(messages, options, ct); } - return new ToolCallMessage(functionCall.Name, functionCall.Arguments, from: agent.Name); + return new ToolCallMessage(functionCall.FunctionName, functionCall.FunctionArguments, from: agent.Name); }); // test 1 @@ -90,7 +89,7 @@ public async Task FunctionCallMiddlewareTestAsync() functionMap: new Dictionary>> { { "echo", EchoWrapper } }); var testAgent = agent.RegisterMiddleware(mw); - var functionCallMessage = new ToolCallMessage(functionCall.Name, functionCall.Arguments, from: "user"); + var functionCallMessage = new ToolCallMessage(functionCall.FunctionName, functionCall.FunctionArguments, from: "user"); var reply = await testAgent.SendAsync(functionCallMessage); reply.Should().BeOfType(); reply.GetContent()!.Should().Be("[FUNC] hello"); diff --git a/dotnet/test/AutoGen.Tests/Orchestrator/RolePlayOrchestratorTests.cs b/dotnet/test/AutoGen.Tests/Orchestrator/RolePlayOrchestratorTests.cs new file mode 100644 index 000000000000..d4d602d84914 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/Orchestrator/RolePlayOrchestratorTests.cs @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RolePlayOrchestratorTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Anthropic; +using AutoGen.Anthropic.Extensions; +using AutoGen.Anthropic.Utils; +using AutoGen.AzureAIInference; +using AutoGen.AzureAIInference.Extension; +using AutoGen.Gemini; +using AutoGen.Mistral; +using AutoGen.Mistral.Extension; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using Azure.AI.Inference; +using Azure.AI.OpenAI; +using FluentAssertions; +using Moq; +using OpenAI; +using Xunit; + +namespace AutoGen.Tests; + +public class RolePlayOrchestratorTests +{ + [Fact] + public async Task ItReturnNextSpeakerTestAsync() + { + var admin = Mock.Of(); + Mock.Get(admin).Setup(x => x.Name).Returns("Admin"); + Mock.Get(admin).Setup(x => x.GenerateReplyAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, GenerateReplyOptions, CancellationToken>((messages, option, _) => + { + // verify prompt + var rolePlayPrompt = messages.First().GetContent(); + rolePlayPrompt.Should().Contain("You are in a role play game. Carefully read the conversation history and carry on the conversation"); + rolePlayPrompt.Should().Contain("The available roles are:"); + rolePlayPrompt.Should().Contain("Alice,Bob"); + rolePlayPrompt.Should().Contain("From Alice:"); + option.StopSequence.Should().BeEquivalentTo([":"]); + option.Temperature.Should().Be(0); + option.MaxToken.Should().Be(128); + option.Functions.Should().BeNull(); + }) + .ReturnsAsync(new TextMessage(Role.Assistant, "From Alice")); + + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + + var orchestrator = new RolePlayOrchestrator(admin); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = [], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(alice); + } + + [Fact] + public async Task ItReturnNullWhenNoCandidateIsAvailableAsync() + { + var admin = Mock.Of(); + var orchestrator = new RolePlayOrchestrator(admin); + var context = new OrchestrationContext + { + Candidates = [], + ChatHistory = [], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().BeNull(); + } + + [Fact] + public async Task ItReturnCandidateWhenOnlyOneCandidateIsAvailableAsync() + { + var admin = Mock.Of(); + var alice = new EchoAgent("Alice"); + var orchestrator = new RolePlayOrchestrator(admin); + var context = new OrchestrationContext + { + Candidates = [alice], + ChatHistory = [], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(alice); + } + + [Fact] + public async Task ItThrowExceptionWhenAdminFailsToFollowPromptAsync() + { + var admin = Mock.Of(); + Mock.Get(admin).Setup(x => x.Name).Returns("Admin"); + Mock.Get(admin).Setup(x => x.GenerateReplyAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new TextMessage(Role.Assistant, "I don't know")); // admin fails to follow the prompt and returns an invalid message + + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + + var orchestrator = new RolePlayOrchestrator(admin); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = [], + }; + + var action = async () => await orchestrator.GetNextSpeakerAsync(context); + + await action.Should().ThrowAsync() + .WithMessage("The response from admin is 't know, which is either not in the candidates list or not in the correct format."); + } + + [Fact] + public async Task ItSelectNextSpeakerFromWorkflowIfProvided() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + var charlie = new EchoAgent("Charlie"); + workflow.AddTransition(Transition.Create(alice, bob)); + workflow.AddTransition(Transition.Create(bob, charlie)); + workflow.AddTransition(Transition.Create(charlie, alice)); + + var admin = Mock.Of(); + var orchestrator = new RolePlayOrchestrator(admin, workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob, charlie], + ChatHistory = + [ + new TextMessage(Role.User, "Hello, Bob", from: "Alice"), + ], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(bob); + } + + [Fact] + public async Task ItReturnNullIfNoAvailableAgentFromWorkflowAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + workflow.AddTransition(Transition.Create(alice, bob)); + + var admin = Mock.Of(); + var orchestrator = new RolePlayOrchestrator(admin, workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = + [ + new TextMessage(Role.User, "Hello, Alice", from: "Bob"), + ], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().BeNull(); + } + + [Fact] + public async Task ItUseCandidatesFromWorflowAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + var charlie = new EchoAgent("Charlie"); + workflow.AddTransition(Transition.Create(alice, bob)); + workflow.AddTransition(Transition.Create(alice, charlie)); + + var admin = Mock.Of(); + Mock.Get(admin).Setup(x => x.GenerateReplyAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, GenerateReplyOptions, CancellationToken>((messages, option, _) => + { + messages.First().IsSystemMessage().Should().BeTrue(); + + // verify prompt + var rolePlayPrompt = messages.First().GetContent(); + rolePlayPrompt.Should().Contain("Bob,Charlie"); + rolePlayPrompt.Should().Contain("From Bob:"); + option.StopSequence.Should().BeEquivalentTo([":"]); + option.Temperature.Should().Be(0); + option.MaxToken.Should().Be(128); + option.Functions.Should().BeEmpty(); + }) + .ReturnsAsync(new TextMessage(Role.Assistant, "From Bob")); + var orchestrator = new RolePlayOrchestrator(admin, workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = + [ + new TextMessage(Role.User, "Hello, Bob", from: "Alice"), + ], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(bob); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task GPT_3_5_CoderReviewerRunnerTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = new AzureOpenAIClient(new Uri(endpoint), new System.ClientModel.ApiKeyCredential(key)); + var openAIChatAgent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(deployName), + name: "assistant") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(openAIChatAgent); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task GPT_4o_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set"); + var model = "gpt-4o"; + var openaiClient = new OpenAIClient(apiKey); + var openAIChatAgent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(model), + name: "assistant") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(openAIChatAgent); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task GPT_4o_mini_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set"); + var model = "gpt-4o-mini"; + var openaiClient = new OpenAIClient(apiKey); + var openAIChatAgent = new OpenAIChatAgent( + chatClient: openaiClient.GetChatClient(model), + name: "assistant") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(openAIChatAgent); + } + + + [ApiKeyFact("GOOGLE_GEMINI_API_KEY")] + public async Task GoogleGemini_1_5_flash_001_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("GOOGLE_GEMINI_API_KEY") ?? throw new InvalidOperationException("GOOGLE_GEMINI_API_KEY is not set"); + var geminiAgent = new GeminiChatAgent( + name: "gemini", + model: "gemini-1.5-flash-001", + apiKey: apiKey) + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(geminiAgent); + } + + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task Claude3_Haiku_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new Exception("Please set ANTHROPIC_API_KEY environment variable."); + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, apiKey); + + var agent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku, + systemMessage: "You are a helpful AI assistant that convert user message to upper case") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(agent); + } + + [ApiKeyFact("MISTRAL_API_KEY")] + public async Task Mistra_7b_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY") ?? throw new InvalidOperationException("MISTRAL_API_KEY is not set."); + var client = new MistralClient(apiKey: apiKey); + + var agent = new MistralClientAgent( + client: client, + name: "MistralClientAgent", + model: "open-mistral-7b") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(agent); + } + + [ApiKeyFact("GH_API_KEY")] + public async Task LLaMA_3_1_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("GH_API_KEY") ?? throw new InvalidOperationException("GH_API_KEY is not set."); + var endPoint = "https://models.inference.ai.azure.com"; + + var chatCompletionClient = new ChatCompletionsClient(new Uri(endPoint), new Azure.AzureKeyCredential(apiKey)); + var agent = new ChatCompletionsClientAgent( + chatCompletionsClient: chatCompletionClient, + name: "assistant", + modelName: "Meta-Llama-3.1-70B-Instruct") + .RegisterMessageConnector(); + + await CoderReviewerRunnerTestAsync(agent); + } + + /// + /// This test is to mimic the conversation among coder, reviewer and runner. + /// The coder will write the code, the reviewer will review the code, and the runner will run the code. + /// + /// + /// + public async Task CoderReviewerRunnerTestAsync(IAgent admin) + { + var coder = new EchoAgent("Coder"); + var reviewer = new EchoAgent("Reviewer"); + var runner = new EchoAgent("Runner"); + var user = new EchoAgent("User"); + var initializeMessage = new List + { + new TextMessage(Role.User, "Hello, I am user, I will provide the coding task, please write the code first, then review and run it", from: "User"), + new TextMessage(Role.User, "Hello, I am coder, I will write the code", from: "Coder"), + new TextMessage(Role.User, "Hello, I am reviewer, I will review the code", from: "Reviewer"), + new TextMessage(Role.User, "Hello, I am runner, I will run the code", from: "Runner"), + new TextMessage(Role.User, "how to print 'hello world' using C#", from: user.Name), + }; + + var chatHistory = new List() + { + new TextMessage(Role.User, """ + ```csharp + Console.WriteLine("Hello World"); + ``` + """, from: coder.Name), + new TextMessage(Role.User, "The code looks good", from: reviewer.Name), + new TextMessage(Role.User, "The code runs successfully, the output is 'Hello World'", from: runner.Name), + }; + + var orchestrator = new RolePlayOrchestrator(admin); + foreach (var message in chatHistory) + { + var context = new OrchestrationContext + { + Candidates = [coder, reviewer, runner, user], + ChatHistory = initializeMessage, + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker!.Name.Should().Be(message.From); + initializeMessage.Add(message); + } + + // the last next speaker should be the user + var lastSpeaker = await orchestrator.GetNextSpeakerAsync(new OrchestrationContext + { + Candidates = [coder, reviewer, runner, user], + ChatHistory = initializeMessage, + }); + + lastSpeaker!.Name.Should().Be(user.Name); + } +} diff --git a/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs b/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs new file mode 100644 index 000000000000..17897860a144 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RoundRobinOrchestratorTests.cs + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class RoundRobinOrchestratorTests +{ + [Fact] + public async Task ItReturnNextAgentAsync() + { + var orchestrator = new RoundRobinOrchestrator(); + var context = new OrchestrationContext + { + Candidates = new List + { + new EchoAgent("Alice"), + new EchoAgent("Bob"), + new EchoAgent("Charlie"), + }, + }; + + var messages = new List + { + new TextMessage(Role.User, "Hello, Alice", from: "Alice"), + new TextMessage(Role.User, "Hello, Bob", from: "Bob"), + new TextMessage(Role.User, "Hello, Charlie", from: "Charlie"), + }; + + var expected = new List { "Bob", "Charlie", "Alice" }; + + var zip = messages.Zip(expected); + + foreach (var (msg, expect) in zip) + { + context.ChatHistory = [msg]; + var nextSpeaker = await orchestrator.GetNextSpeakerAsync(context); + Assert.Equal(expect, nextSpeaker!.Name); + } + } + + [Fact] + public async Task ItReturnNullIfNoCandidates() + { + var orchestrator = new RoundRobinOrchestrator(); + var context = new OrchestrationContext + { + Candidates = new List(), + ChatHistory = new List + { + new TextMessage(Role.User, "Hello, Alice", from: "Alice"), + }, + }; + + var result = await orchestrator.GetNextSpeakerAsync(context); + Assert.Null(result); + } + + [Fact] + public async Task ItReturnNullIfLastMessageIsNotFromCandidates() + { + var orchestrator = new RoundRobinOrchestrator(); + var context = new OrchestrationContext + { + Candidates = new List + { + new EchoAgent("Alice"), + new EchoAgent("Bob"), + new EchoAgent("Charlie"), + }, + ChatHistory = new List + { + new TextMessage(Role.User, "Hello, David", from: "David"), + }, + }; + + var result = await orchestrator.GetNextSpeakerAsync(context); + result.Should().BeNull(); + } + + [Fact] + public async Task ItReturnTheFirstAgentInTheListIfNoChatHistory() + { + var orchestrator = new RoundRobinOrchestrator(); + var context = new OrchestrationContext + { + Candidates = new List + { + new EchoAgent("Alice"), + new EchoAgent("Bob"), + new EchoAgent("Charlie"), + }, + }; + + var result = await orchestrator.GetNextSpeakerAsync(context); + result!.Name.Should().Be("Alice"); + } +} diff --git a/dotnet/test/AutoGen.Tests/Orchestrator/WorkflowOrchestratorTests.cs b/dotnet/test/AutoGen.Tests/Orchestrator/WorkflowOrchestratorTests.cs new file mode 100644 index 000000000000..6599566a4466 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/Orchestrator/WorkflowOrchestratorTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// WorkflowOrchestratorTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class WorkflowOrchestratorTests +{ + [Fact] + public async Task ItReturnNextAgentAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + var charlie = new EchoAgent("Charlie"); + workflow.AddTransition(Transition.Create(alice, bob)); + workflow.AddTransition(Transition.Create(bob, charlie)); + workflow.AddTransition(Transition.Create(charlie, alice)); + var orchestrator = new WorkflowOrchestrator(workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob, charlie] + }; + + var messages = new List + { + new TextMessage(Role.User, "Hello, Alice", from: "Alice"), + new TextMessage(Role.User, "Hello, Bob", from: "Bob"), + new TextMessage(Role.User, "Hello, Charlie", from: "Charlie"), + }; + + var expected = new List { "Bob", "Charlie", "Alice" }; + + var zip = messages.Zip(expected); + + foreach (var (msg, expect) in zip) + { + context.ChatHistory = [msg]; + var result = await orchestrator.GetNextSpeakerAsync(context); + Assert.Equal(expect, result!.Name); + } + } + + [Fact] + public async Task ItReturnNullIfNoCandidates() + { + var workflow = new Graph(); + var orchestrator = new WorkflowOrchestrator(workflow); + var context = new OrchestrationContext + { + Candidates = new List(), + ChatHistory = new List + { + new TextMessage(Role.User, "Hello, Alice", from: "Alice"), + }, + }; + + var nextAgent = await orchestrator.GetNextSpeakerAsync(context); + nextAgent.Should().BeNull(); + } + + [Fact] + public async Task ItReturnNullIfNoAgentIsAvailableFromWorkflowAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + workflow.AddTransition(Transition.Create(alice, bob)); + var orchestrator = new WorkflowOrchestrator(workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = new List + { + new TextMessage(Role.User, "Hello, Bob", from: "Bob"), + }, + }; + + var nextSpeaker = await orchestrator.GetNextSpeakerAsync(context); + nextSpeaker.Should().BeNull(); + } + + [Fact] + public async Task ItThrowExceptionWhenMoreThanOneAvailableAgentsFromWorkflowAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + var charlie = new EchoAgent("Charlie"); + workflow.AddTransition(Transition.Create(alice, bob)); + workflow.AddTransition(Transition.Create(alice, charlie)); + var orchestrator = new WorkflowOrchestrator(workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob, charlie], + ChatHistory = new List + { + new TextMessage(Role.User, "Hello, Bob", from: "Alice"), + }, + }; + + var action = async () => await orchestrator.GetNextSpeakerAsync(context); + + await action.Should().ThrowExactlyAsync().WithMessage("There are more than one available agents from the workflow for the next speaker."); + } +} diff --git a/dotnet/test/AutoGen.Tests/SingleAgentTest.cs b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs index 5a3a9734cd1a..fb28f48e12db 100644 --- a/dotnet/test/AutoGen.Tests/SingleAgentTest.cs +++ b/dotnet/test/AutoGen.Tests/SingleAgentTest.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; -using AutoGen.LMStudio; -using AutoGen.OpenAI; -using Azure.AI.OpenAI; using FluentAssertions; using Xunit; using Xunit.Abstractions; @@ -37,90 +33,6 @@ private ILLMConfig CreateOpenAIGPT4VisionConfig() return new OpenAIConfig(key, "gpt-4-vision-preview"); } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] - public async Task GPTAgentTestAsync() - { - var config = this.CreateAzureOpenAIGPT35TurboConfig(); - - var agent = new GPTAgent("gpt", "You are a helpful AI assistant", config); - - await UpperCaseTestAsync(agent); - await UpperCaseStreamingTestAsync(agent); - } - - [ApiKeyFact("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] - public async Task GPTAgentVisionTestAsync() - { - var visionConfig = this.CreateOpenAIGPT4VisionConfig(); - var visionAgent = new GPTAgent( - name: "gpt", - systemMessage: "You are a helpful AI assistant", - config: visionConfig, - temperature: 0); - - var gpt3Config = this.CreateAzureOpenAIGPT35TurboConfig(); - var gpt3Agent = new GPTAgent( - name: "gpt3", - systemMessage: "You are a helpful AI assistant, return highest label from conversation", - config: gpt3Config, - temperature: 0, - functions: new[] { this.GetHighestLabelFunction }, - functionMap: new Dictionary>> - { - { nameof(GetHighestLabel), this.GetHighestLabelWrapper }, - }); - - var imageUri = new Uri(@"https://microsoft.github.io/autogen/assets/images/level2algebra-659ba95286432d9945fc89e84d606797.png"); - var oaiMessage = new ChatRequestUserMessage( - new ChatMessageTextContentItem("which label has the highest inference cost"), - new ChatMessageImageContentItem(imageUri)); - var multiModalMessage = new MultiModalMessage(Role.User, - [ - new TextMessage(Role.User, "which label has the highest inference cost", from: "user"), - new ImageMessage(Role.User, imageUri, from: "user"), - ], - from: "user"); - - var imageMessage = new ImageMessage(Role.User, imageUri, from: "user"); - - string imagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ApprovalTests", "square.png"); - ImageMessage imageMessageData; - using (var fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) - { - var ms = new MemoryStream(); - await fs.CopyToAsync(ms); - ms.Seek(0, SeekOrigin.Begin); - var imageData = await BinaryData.FromStreamAsync(ms, "image/png"); - imageMessageData = new ImageMessage(Role.Assistant, imageData, from: "user"); - } - - IMessage[] messages = [ - MessageEnvelope.Create(oaiMessage), - multiModalMessage, - imageMessage, - imageMessageData - ]; - - foreach (var message in messages) - { - var response = await visionAgent.SendAsync(message); - response.From.Should().Be(visionAgent.Name); - - var labelResponse = await gpt3Agent.SendAsync(response); - labelResponse.From.Should().Be(gpt3Agent.Name); - labelResponse.GetToolCalls()!.First().FunctionName.Should().Be(nameof(GetHighestLabel)); - } - } - - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] - public async Task GPTFunctionCallAgentTestAsync() - { - var config = this.CreateAzureOpenAIGPT35TurboConfig(); - var agentWithFunction = new GPTAgent("gpt", "You are a helpful AI assistant", config, 0, functions: new[] { this.EchoAsyncFunction }); - - await EchoFunctionCallTestAsync(agentWithFunction); - } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] public async Task AssistantAgentFunctionCallTestAsync() { @@ -146,43 +58,6 @@ public async Task AssistantAgentFunctionCallTestAsync() await EchoFunctionCallTestAsync(assistantAgent); } - [Fact] - public async Task ItCreateAssistantAgentFromLMStudioConfigAsync() - { - var host = "http://localhost"; - var port = 8080; - var lmStudioConfig = new LMStudioConfig(host, port); - - var assistantAgent = new AssistantAgent( - name: "assistant", - llmConfig: new ConversableAgentConfig() - { - ConfigList = [lmStudioConfig], - }); - - assistantAgent.Name.Should().Be("assistant"); - assistantAgent.InnerAgent.Should().BeOfType(); - } - - [ApiKeyFact("LMStudio_ENDPOINT")] - public async Task ItTestAssistantAgentFromLMStudioConfigAsync() - { - var Uri = Environment.GetEnvironmentVariable("LMStudio_ENDPOINT") ?? throw new ArgumentException("LMStudio_ENDPOINT is not set"); - var lmStudioConfig = new LMStudioConfig(new Uri(Uri)); - - var assistantAgent = new AssistantAgent( - name: "assistant", - llmConfig: new ConversableAgentConfig() - { - ConfigList = [lmStudioConfig], - }); - - assistantAgent.Name.Should().Be("assistant"); - assistantAgent.InnerAgent.Should().BeOfType(); - await this.UpperCaseTestAsync(assistantAgent); - } - - [Fact] public async Task AssistantAgentDefaultReplyTestAsync() { @@ -224,25 +99,6 @@ public async Task AssistantAgentFunctionCallSelfExecutionTestAsync() await EchoFunctionCallExecutionTestAsync(assistantAgent); } - [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] - public async Task GPTAgentFunctionCallSelfExecutionTestAsync() - { - var config = this.CreateAzureOpenAIGPT35TurboConfig(); - var agent = new GPTAgent( - name: "gpt", - systemMessage: "You are a helpful AI assistant", - config: config, - temperature: 0, - functions: new[] { this.EchoAsyncFunction }, - functionMap: new Dictionary>> - { - { nameof(EchoAsync), this.EchoAsyncWrapper }, - }); - - await EchoFunctionCallExecutionStreamingTestAsync(agent); - await EchoFunctionCallExecutionTestAsync(agent); - } - /// /// echo when asked. /// @@ -297,7 +153,7 @@ public async Task EchoFunctionCallExecutionStreamingTestAsync(IStreamingAgent ag }; var replyStream = agent.GenerateStreamingReplyAsync(messages: new[] { helloWorld }, option); var answer = "[ECHO] Hello world"; - IStreamingMessage? finalReply = default; + IMessage? finalReply = default; await foreach (var reply in replyStream) { reply.From.Should().Be(agent.Name); diff --git a/dotnet/test/AutoGen.Tests/TwoAgentTest.cs b/dotnet/test/AutoGen.Tests/TwoAgentTest.cs index 90c1bfa9a148..335f4aaa57c6 100644 --- a/dotnet/test/AutoGen.Tests/TwoAgentTest.cs +++ b/dotnet/test/AutoGen.Tests/TwoAgentTest.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using AutoGen.OpenAI; using FluentAssertions; using Xunit.Abstractions; @@ -56,7 +55,7 @@ public async Task TwoAgentWeatherChatTestAsync() name: "user", functionMap: new Dictionary>> { - { this.GetWeatherFunction.Name, this.GetWeatherWrapper }, + { this.GetWeatherFunctionContract.Name, this.GetWeatherWrapper }, }) .RegisterMiddleware(async (msgs, option, agent, ct) => { diff --git a/dotnet/test/AutoGen.Tests/WorkflowTest.cs b/dotnet/test/AutoGen.Tests/WorkflowTest.cs index d1d12010e39f..1079ec95515a 100644 --- a/dotnet/test/AutoGen.Tests/WorkflowTest.cs +++ b/dotnet/test/AutoGen.Tests/WorkflowTest.cs @@ -17,7 +17,7 @@ public async Task TransitionTestAsync() var alice = new EchoAgent("alice"); var bob = new EchoAgent("bob"); - var aliceToBob = Transition.Create(alice, bob, async (from, to, messages) => + var aliceToBob = Transition.Create(alice, bob, async (from, to, messages, _) => { if (messages.Any(m => m.GetContent() == "Hello")) { @@ -30,7 +30,7 @@ public async Task TransitionTestAsync() var canTransit = await aliceToBob.CanTransitionAsync([]); canTransit.Should().BeFalse(); - canTransit = await aliceToBob.CanTransitionAsync(new[] { new TextMessage(Role.Assistant, "Hello") }); + canTransit = await aliceToBob.CanTransitionAsync([new TextMessage(Role.Assistant, "Hello")]); canTransit.Should().BeTrue(); // if no function is provided, it should always return true diff --git a/dotnet/test/AutoGen.WebAPI.Tests/AutoGen.WebAPI.Tests.csproj b/dotnet/test/AutoGen.WebAPI.Tests/AutoGen.WebAPI.Tests.csproj new file mode 100644 index 000000000000..7ec6c408cfe8 --- /dev/null +++ b/dotnet/test/AutoGen.WebAPI.Tests/AutoGen.WebAPI.Tests.csproj @@ -0,0 +1,28 @@ + + + + $(TestTargetFrameworks) + enable + enable + false + true + True + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/AutoGen.WebAPI.Tests/EchoAgent.cs b/dotnet/test/AutoGen.WebAPI.Tests/EchoAgent.cs new file mode 100644 index 000000000000..957f8d1d799b --- /dev/null +++ b/dotnet/test/AutoGen.WebAPI.Tests/EchoAgent.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// EchoAgent.cs + +using System.Runtime.CompilerServices; +using AutoGen.Core; + +namespace AutoGen.WebAPI.Tests; + +public class EchoAgent : IStreamingAgent +{ + public EchoAgent(string name) + { + Name = name; + } + public string Name { get; } + + public async Task GenerateReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + return messages.Last(); + } + + public async IAsyncEnumerable GenerateStreamingReplyAsync( + IEnumerable messages, + GenerateReplyOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var lastMessage = messages.LastOrDefault(); + if (lastMessage == null) + { + yield break; + } + + // return each character of the last message as a separate message + if (lastMessage.GetContent() is string content) + { + foreach (var c in content) + { + yield return new TextMessageUpdate(Role.Assistant, c.ToString(), this.Name); + } + } + } +} diff --git a/dotnet/test/AutoGen.WebAPI.Tests/OpenAIChatCompletionMiddlewareTests.cs b/dotnet/test/AutoGen.WebAPI.Tests/OpenAIChatCompletionMiddlewareTests.cs new file mode 100644 index 000000000000..c56bbf983504 --- /dev/null +++ b/dotnet/test/AutoGen.WebAPI.Tests/OpenAIChatCompletionMiddlewareTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OpenAIChatCompletionMiddlewareTests.cs + +using System.ClientModel.Primitives; +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenAI; + +namespace AutoGen.WebAPI.Tests; + +public class OpenAIChatCompletionMiddlewareTests +{ + [Fact] + public async Task ItReturnTextMessageWhenSendTextMessage() + { + var agent = new EchoAgent("test"); + var hostBuilder = CreateHostBuilder(agent); + using var host = await hostBuilder.StartAsync(); + var client = host.GetTestClient(); + var openaiClient = CreateOpenAIClient(client); + var openAIAgent = new OpenAIChatAgent(openaiClient.GetChatClient("test"), "test") + .RegisterMessageConnector(); + + var response = await openAIAgent.SendAsync("Hey"); + + response.GetContent().Should().Be("Hey"); + response.Should().BeOfType(); + response.From.Should().Be("test"); + } + + [Fact] + public async Task ItReturnTextMessageWhenSendTextMessageUseStreaming() + { + var agent = new EchoAgent("test"); + var hostBuilder = CreateHostBuilder(agent); + using var host = await hostBuilder.StartAsync(); + var client = host.GetTestClient(); + var openaiClient = CreateOpenAIClient(client); + var openAIAgent = new OpenAIChatAgent(openaiClient.GetChatClient("test"), "test") + .RegisterMessageConnector(); + + var message = new TextMessage(Role.User, "ABCDEFGHIJKLMN"); + var chunks = new List(); + await foreach (var chunk in openAIAgent.GenerateStreamingReplyAsync([message])) + { + chunk.Should().BeOfType(); + chunks.Add(chunk); + } + + var mergedChunks = string.Join("", chunks.Select(c => c.GetContent())); + mergedChunks.Should().Be("ABCDEFGHIJKLMN"); + chunks.Count.Should().Be(14); + } + + private IHostBuilder CreateHostBuilder(IAgent agent) + { + return new HostBuilder() + .ConfigureWebHost(webHost => + { + webHost.UseTestServer(); + webHost.Configure(app => + { + app.UseAgentAsOpenAIChatCompletionEndpoint(agent); + }); + }); + } + + private OpenAIClient CreateOpenAIClient(HttpClient client) + { + return new OpenAIClient("api-key", new OpenAIClientOptions + { + Transport = new HttpClientPipelineTransport(client), + }); + } +} diff --git a/dotnet/website/articles/Agent-overview.md b/dotnet/website/articles/Agent-overview.md index 0b84cdc49ac7..586d231a6e7d 100644 --- a/dotnet/website/articles/Agent-overview.md +++ b/dotnet/website/articles/Agent-overview.md @@ -8,7 +8,6 @@ - Create an @AutoGen.OpenAI.OpenAIChatAgent: [Create an OpenAI chat agent](./OpenAIChatAgent-simple-chat.md) - Create a @AutoGen.SemanticKernel.SemanticKernelAgent: [Create a semantic kernel agent](./AutoGen.SemanticKernel/SemanticKernelAgent-simple-chat.md) - Create a @AutoGen.LMStudio.LMStudioAgent: [Connect to LM Studio](./Consume-LLM-server-from-LM-Studio.md) -- Create your own agent: [Create your own agent](./Create-your-own-agent.md) ## Chat with an agent To chat with an agent, typically you can invoke @AutoGen.Core.IAgent.GenerateReplyAsync*. On top of that, you can also use one of the extension methods like @AutoGen.Core.AgentExtension.SendAsync* as shortcuts. diff --git a/dotnet/website/articles/Function-call-with-ollama-and-litellm.md b/dotnet/website/articles/Function-call-with-ollama-and-litellm.md new file mode 100644 index 000000000000..2dc595ba3adb --- /dev/null +++ b/dotnet/website/articles/Function-call-with-ollama-and-litellm.md @@ -0,0 +1,93 @@ +This example shows how to use function call with local LLM models where [Ollama](https://ollama.com/) as local model provider and [LiteLLM](https://docs.litellm.ai/docs/) proxy server which provides an openai-api compatible interface. + +[![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs) + +To run this example, the following prerequisites are required: +- Install [Ollama](https://ollama.com/) and [LiteLLM](https://docs.litellm.ai/docs/) on your local machine. +- A local model that supports function call. In this example `dolphincoder:latest` is used. + +## Install Ollama and pull `dolphincoder:latest` model +First, install Ollama by following the instructions on the [Ollama website](https://ollama.com/). + +After installing Ollama, pull the `dolphincoder:latest` model by running the following command: +```bash +ollama pull dolphincoder:latest +``` + +## Install LiteLLM and start the proxy server + +You can install LiteLLM by following the instructions on the [LiteLLM website](https://docs.litellm.ai/docs/). +```bash +pip install 'litellm[proxy]' +``` + +Then, start the proxy server by running the following command: + +```bash +litellm --model ollama_chat/dolphincoder --port 4000 +``` + +This will start an openai-api compatible proxy server at `http://localhost:4000`. You can verify if the server is running by observing the following output in the terminal: + +```bash +#------------------------------------------------------------# +# # +# 'The worst thing about this product is...' # +# https://github.com/BerriAI/litellm/issues/new # +# # +#------------------------------------------------------------# + +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:4000 (Press CTRL+C to quit) +``` + +## Install AutoGen and AutoGen.SourceGenerator +In your project, install the AutoGen and AutoGen.SourceGenerator package using the following command: + +```bash +dotnet add package AutoGen +dotnet add package AutoGen.SourceGenerator +``` + +The `AutoGen.SourceGenerator` package is used to automatically generate type-safe `FunctionContract` instead of manually defining them. For more information, please check out [Create type-safe function](Create-type-safe-function-call.md). + +And in your project file, enable structural xml document support by setting the `GenerateDocumentationFile` property to `true`: + +```xml + + + true + +``` + +## Define `WeatherReport` function and create @AutoGen.Core.FunctionCallMiddleware + +Create a `public partial` class to host the methods you want to use in AutoGen agents. The method has to be a `public` instance method and its return type must be `Task`. After the methods are defined, mark them with `AutoGen.Core.FunctionAttribute` attribute. + +[!code-csharp[Define WeatherReport function](../../sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs?name=Function)] + +Then create a @AutoGen.Core.FunctionCallMiddleware and add the `WeatherReport` function to the middleware. The middleware will pass the `FunctionContract` to the agent when generating a response, and process the tool call response when receiving a `ToolCallMessage`. +[!code-csharp[Define WeatherReport function](../../sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs?name=Create_tools)] + +## Create @AutoGen.OpenAI.OpenAIChatAgent with `GetWeatherReport` tool and chat with it + +Because LiteLLM proxy server is openai-api compatible, we can use @AutoGen.OpenAI.OpenAIChatAgent to connect to it as a third-party openai-api provider. The agent is also registered with a @AutoGen.Core.FunctionCallMiddleware which contains the `WeatherReport` tool. Therefore, the agent can call the `WeatherReport` tool when generating a response. + +[!code-csharp[Create an agent with tools](../../sample/AutoGen.OpenAI.Sample/Tool_Call_With_Ollama_And_LiteLLM.cs?name=Create_Agent)] + +The reply from the agent will similar to the following: +```bash +AggregateMessage from assistant +-------------------- +ToolCallMessage: +ToolCallMessage from assistant +-------------------- +- GetWeatherAsync: {"city": "new york"} +-------------------- + +ToolCallResultMessage: +ToolCallResultMessage from assistant +-------------------- +- GetWeatherAsync: The weather in new york is 72 degrees and sunny. +-------------------- +``` \ No newline at end of file diff --git a/dotnet/website/articles/Installation.md b/dotnet/website/articles/Installation.md index 3ec5d3a470f4..30b55442d246 100644 --- a/dotnet/website/articles/Installation.md +++ b/dotnet/website/articles/Installation.md @@ -13,8 +13,9 @@ AutoGen.Net provides the following packages, you can choose to install one or mo - `AutoGen.LMStudio`: This package provides the integration agents from LM Studio. - `AutoGen.SemanticKernel`: This package provides the integration agents over semantic kernel. - `AutoGen.Gemini`: This package provides the integration agents from [Google Gemini](https://gemini.google.com/). +- `AutoGen.AzureAIInference`: This package provides the integration agents for [Azure AI Inference](https://www.nuget.org/packages/Azure.AI.Inference). - `AutoGen.SourceGenerator`: This package carries a source generator that adds support for type-safe function definition generation. -- `AutoGen.DotnetInteractive`: This packages carries dotnet interactive support to execute dotnet code snippet. +- `AutoGen.DotnetInteractive`: This packages carries dotnet interactive support to execute code snippets. The current supported language is C#, F#, powershell and python. >[!Note] > Help me choose diff --git a/dotnet/website/articles/OpenAIChatAgent-connect-to-third-party-api.md b/dotnet/website/articles/OpenAIChatAgent-connect-to-third-party-api.md index 8321fc87a5c2..0873765b1a6c 100644 --- a/dotnet/website/articles/OpenAIChatAgent-connect-to-third-party-api.md +++ b/dotnet/website/articles/OpenAIChatAgent-connect-to-third-party-api.md @@ -1,7 +1,6 @@ The following example shows how to connect to third-party OpenAI API using @AutoGen.OpenAI.OpenAIChatAgent. -> [!NOTE] -> You can find the complete code of this example in [Example16_OpenAIChatAgent_ConnectToThirdPartyBackend](https://github.com/microsoft/autogen/tree/main/dotnet/sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs). +[![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs) ## Overview A lot of LLM applications/platforms support spinning up a chat server that is compatible with OpenAI API, such as LM Studio, Ollama, Mistral etc. This means that you can connect to these servers using the @AutoGen.OpenAI.OpenAIChatAgent. @@ -25,24 +24,24 @@ ollama serve ## Steps - Import the required namespaces: -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs?name=using_statement)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs?name=using_statement)] - Create a `CustomHttpClientHandler` class. The `CustomHttpClientHandler` class is used to customize the HttpClientHandler. In this example, we override the `SendAsync` method to redirect the request to local Ollama server, which is running on `http://localhost:11434`. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs?name=CustomHttpClientHandler)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs?name=CustomHttpClientHandler)] - Create an `OpenAIChatAgent` instance and connect to the third-party API. Then create an @AutoGen.OpenAI.OpenAIChatAgent instance and connect to the OpenAI API from Ollama. You can customize the transport behavior of `OpenAIClient` by passing a customized `HttpClientTransport` instance. In the customized `HttpClientTransport` instance, we pass the `CustomHttpClientHandler` we just created which redirects all openai chat requests to the local Ollama server. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs?name=create_agent)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs?name=create_agent)] - Chat with the `OpenAIChatAgent`. Finally, you can start chatting with the agent. In this example, we send a coding question to the agent and get the response. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example16_OpenAIChatAgent_ConnectToThirdPartyBackend.cs?name=send_message)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Connect_To_Ollama.cs?name=send_message)] ## Sample Output The following is the sample output of the code snippet above: diff --git a/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md b/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md index a822cb046334..22f0ced00469 100644 --- a/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md +++ b/dotnet/website/articles/OpenAIChatAgent-use-json-mode.md @@ -1,5 +1,7 @@ The following example shows how to enable JSON mode in @AutoGen.OpenAI.OpenAIChatAgent. +[![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs) + ## What is JSON mode? JSON mode is a new feature in OpenAI which allows you to instruct model to always respond with a valid JSON object. This is useful when you want to constrain the model output to JSON format only. @@ -8,20 +10,17 @@ JSON mode is a new feature in OpenAI which allows you to instruct model to alway ## How to enable JSON mode in OpenAIChatAgent. -> [!NOTE] -> You can find the complete example in the [Example13_OpenAIAgent_JsonMode](https://github.com/microsoft/autogen/tree/main/dotnet/sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs). - To enable JSON mode for @AutoGen.OpenAI.OpenAIChatAgent, set `responseFormat` to `ChatCompletionsResponseFormat.JsonObject` when creating the agent. Note that when enabling JSON mode, you also need to instruct the agent to output JSON format in its system message. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=create_agent)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs?name=create_agent)] After enabling JSON mode, the `openAIClientAgent` will always respond in JSON format when it receives a message. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=chat_with_agent)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs?name=chat_with_agent)] When running the example, the output from `openAIClientAgent` will be a valid JSON object which can be parsed as `Person` class defined below. Note that in the output, the `address` field is missing because the address information is not provided in user input. -[!code-csharp[](../../sample/AutoGen.BasicSamples/Example13_OpenAIAgent_JsonMode.cs?name=person_class)] +[!code-csharp[](../../sample/AutoGen.OpenAI.Sample/Use_Json_Mode.cs?name=person_class)] The output will be: ```bash diff --git a/dotnet/website/articles/Run-dotnet-code.md b/dotnet/website/articles/Run-dotnet-code.md index e3d8fa78a0b3..bee7e1aa3bbb 100644 --- a/dotnet/website/articles/Run-dotnet-code.md +++ b/dotnet/website/articles/Run-dotnet-code.md @@ -16,17 +16,46 @@ For example, in data analysis scenario, agent can resolve tasks like "What is th > [!WARNING] > Running arbitrary code snippet from agent response could bring risks to your system. Using this feature with caution. -## How to run dotnet code snippet? +## Use dotnet interactive kernel to execute code snippet? The built-in feature of running dotnet code snippet is provided by [dotnet-interactive](https://github.com/dotnet/interactive). To run dotnet code snippet, you need to install the following package to your project, which provides the intergraion with dotnet-interactive: ```xml ``` -Then you can use @AutoGen.DotnetInteractive.AgentExtension.RegisterDotnetCodeBlockExectionHook(AutoGen.IAgent,InteractiveService,System.String,System.String) to register a `reply hook` to run dotnet code snippet. The hook will check if a csharp code snippet is present in the most recent message from history, and run the code snippet if it is present. - -The following code snippet shows how to register a dotnet code snippet execution hook: - -[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_0_1)] +Then you can use @AutoGen.DotnetInteractive.DotnetInteractiveKernelBuilder* to create a in-process dotnet-interactive composite kernel with C# and F# kernels. [!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_1)] + +After that, use @AutoGen.DotnetInteractive.Extension.RunSubmitCodeCommandAsync* method to run code snippet. The method will return the result of the code snippet. [!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_2)] + +## Run python code snippet +To run python code, firstly you need to have python installed on your machine, then you need to set up ipykernel and jupyter in your environment. + +```bash +pip install ipykernel +pip install jupyter +``` + +After `ipykernel` and `jupyter` are installed, you can confirm the ipykernel is installed correctly by running the following command: + +```bash +jupyter kernelspec list +``` + +The output should contain all available kernels, including `python3`. + +```bash +Available kernels: + python3 /usr/local/share/jupyter/kernels/python3 + ... +``` + +Then you can add the python kernel to the dotnet-interactive composite kernel by calling `AddPythonKernel` method. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_4)] + +## Further reading +You can refer to the following examples for running code snippet in agentic workflow: +- Dynamic_GroupChat_Coding_Task: [![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.BasicSample/Example04_Dynamic_GroupChat_Coding_Task.cs) +- Dynamic_GroupChat_Calculate_Fibonacci: [![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.BasicSample/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs) diff --git a/dotnet/website/articles/function-comparison-page-between-python-AutoGen-and-autogen.net.md b/dotnet/website/articles/function-comparison-page-between-python-AutoGen-and-autogen.net.md new file mode 100644 index 000000000000..e81b96f11bed --- /dev/null +++ b/dotnet/website/articles/function-comparison-page-between-python-AutoGen-and-autogen.net.md @@ -0,0 +1,37 @@ +### Function comparison between Python AutoGen and AutoGen\.Net + + +#### Agentic pattern + +| Feature | AutoGen | AutoGen\.Net | +| :---------------- | :------ | :---- | +| Code interpreter | run python code in local/docker/notebook executor | run csharp code in dotnet interactive executor | +| Single agent chat pattern | ✔️ | ✔️ | +| Two agent chat pattern | ✔️ | ✔️ | +| group chat (include FSM)| ✔️ | ✔️ (using workflow for FSM groupchat) | +| Nest chat| ✔️ | ✔️ (using middleware pattern)| +|Sequential chat | ✔️ | ❌ (need to manually create task in code) | +| Tool | ✔️ | ✔️ | + + +#### LLM platform support + +ℹ️ Note + +``` Other than the platforms list below, AutoGen.Net also supports all the platforms that semantic kernel supports via AutoGen.SemanticKernel as a bridge ``` + +| Feature | AutoGen | AutoGen\.Net | +| :---------------- | :------ | :---- | +| OpenAI (include third-party) | ✔️ | ✔️ | +| Mistral | ✔️| ✔️| +| Ollama | ✔️| ✔️| +|Claude |✔️ |✔️| +|Gemini (Include Vertex) | ✔️ | ✔️ | + +#### Popular Contrib Agent support + + +| Feature | AutoGen | AutoGen\.Net | +| :---------------- | :------ | :---- | +| Rag Agent | ✔️| ❌ | +| Web surfer | ✔️| ❌ | diff --git a/dotnet/website/articles/toc.yml b/dotnet/website/articles/toc.yml index 6d3787ae3d0b..2335ebf092b5 100644 --- a/dotnet/website/articles/toc.yml +++ b/dotnet/website/articles/toc.yml @@ -26,6 +26,8 @@ href: Create-type-safe-function-call.md - name: Use function call in an agent href: Use-function-call.md + - name: Function call with local model + href: Function-call-with-ollama-and-litellm.md - name: middleware items: - name: middleware overview diff --git a/dotnet/website/docfx.json b/dotnet/website/docfx.json index 224ef9065ca8..221cd4721e3d 100644 --- a/dotnet/website/docfx.json +++ b/dotnet/website/docfx.json @@ -32,6 +32,8 @@ "articles/**/toc.yml", "tutorial/**.md", "tutorial/**/toc.yml", + "release_note/**.md", + "release_note/**/toc.yml", "toc.yml", "*.md" ] diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsA.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsA.png new file mode 100644 index 000000000000..0403a8cf9742 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsA.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:491f8f538c55ce8768179cabfd3789c71c4a07b7d809f85deba9b8f4b759c00e +size 42329 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsB.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsB.png new file mode 100644 index 000000000000..03a68735c082 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e319fad11682c46c3dc511e2fc63e033f3f99efb06d4530e7f72d1f4af23848f +size 31528 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsC.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsC.png new file mode 100644 index 000000000000..7326ad14d040 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/FinalStepsC.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8024b5336615e8c2c3497df7a5890a331bd5bdc7b15dd06abd7ec528ffe0932 +size 70169 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.2OpenAIModel.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.2OpenAIModel.png new file mode 100644 index 000000000000..b2b7481bbe78 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.2OpenAIModel.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:911f2f7c1ab4f9403386298d9769243c0aa8cc22c6f119342cc107a654d1463a +size 44041 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.3ModelNameAndURL.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.3ModelNameAndURL.png new file mode 100644 index 000000000000..d1c19f300806 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step5.3ModelNameAndURL.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec10a48ed3f0a6d8448e0ce425658f3857c2cf89e2badef8a8d3a8c3744fc3bf +size 51944 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6.png new file mode 100644 index 000000000000..67c734454427 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f016faea51f64af3970fde41ac95249c4e0423b02573f058c36dc1e6ba15562d +size 50669 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6b.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6b.png new file mode 100644 index 000000000000..ebd19bff045a --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Step6b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a23cbbf5d3d24eaf1da9370e0914f186815f2ecbf46131d2fd6eb5ff3264d96 +size 22569 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Terminal.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Terminal.png new file mode 100644 index 000000000000..9edefc3aebf3 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/Terminal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97328776c25fd0a61c76065db379406d8d3c96bd8773490c34c168cd7c69a855 +size 58527 diff --git a/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/TheModelTab.png b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/TheModelTab.png new file mode 100644 index 000000000000..55e7bd862613 --- /dev/null +++ b/dotnet/website/images/articles/UseAutoGenAsModelinAGStudio/TheModelTab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d7f4f3a772278e6de320a3601a76f8a9862cab4a9c0da03fad3058b86fcfaf7 +size 45260 diff --git a/dotnet/website/release_note/0.0.16.md b/dotnet/website/release_note/0.0.16.md new file mode 100644 index 000000000000..b9a190c5f793 --- /dev/null +++ b/dotnet/website/release_note/0.0.16.md @@ -0,0 +1,32 @@ +# AutoGen.Net 0.0.16 Release Notes + +We are excited to announce the release of **AutoGen.Net 0.0.16**. This release includes several new features, bug fixes, improvements, and important updates. Below are the detailed release notes: + +**[Milestone: AutoGen.Net 0.0.16](https://github.com/microsoft/autogen/milestone/4)** + +## 📦 New Features +1. **Deprecate `IStreamingMessage`** ([#3045](https://github.com/microsoft/autogen/issues/3045)) - Replaced `IStreamingMessage` and `IStreamingMessage` with `IMessage` and `IMessage`. +2. **Add example for using ollama + LiteLLM for function call** ([#3014](https://github.com/microsoft/autogen/issues/3014)) - Added a new tutorial to the website for integrating ollama with LiteLLM for function calls. +3. **Add ReAct sample** ([#2978](https://github.com/microsoft/autogen/issues/2978)) - Added a new sample demonstrating the ReAct pattern. +4. **Support tools Anthropic Models** ([#2771](https://github.com/microsoft/autogen/issues/2771)) - Introduced support for tools like `AnthropicClient`, `AnthropicClientAgent`, and `AnthropicMessageConnector`. +5. **Propose Orchestrator for managing group chat/agentic workflow** ([#2695](https://github.com/microsoft/autogen/issues/2695)) - Introduced a customizable orchestrator interface for managing group chats and agent workflows. +6. **Run Agent as Web API** ([#2519](https://github.com/microsoft/autogen/issues/2519)) - Introduced the ability to start an OpenAI-chat-compatible web API from an arbitrary agent. + +## 🐛 Bug Fixes +1. **SourceGenerator doesn't work when function's arguments are empty** ([#2976](https://github.com/microsoft/autogen/issues/2976)) - Fixed an issue where the SourceGenerator failed when function arguments were empty. +2. **Add content field in ToolCallMessage** ([#2975](https://github.com/microsoft/autogen/issues/2975)) - Added a content property in `ToolCallMessage` to handle text content returned by the OpenAI model during tool calls. +3. **AutoGen.SourceGenerator doesn’t encode `"` in structural comments** ([#2872](https://github.com/microsoft/autogen/issues/2872)) - Fixed an issue where structural comments containing `"` were not properly encoded, leading to compilation errors. + +## 🚀 Improvements +1. **Sample update - Add getting-start samples for BasicSample project** ([#2859](https://github.com/microsoft/autogen/issues/2859)) - Re-organized the `AutoGen.BasicSample` project to include only essential getting-started examples, simplifying complex examples. +2. **Graph constructor should consider null transitions** ([#2708](https://github.com/microsoft/autogen/issues/2708)) - Updated the Graph constructor to handle cases where transitions’ values are null. + +## ⚠️ API-Breakchange +1. **Deprecate `IStreamingMessage`** ([#3045](https://github.com/microsoft/autogen/issues/3045)) - **Migration guide:** Deprecating `IStreamingMessage` will introduce breaking changes, particularly for `IStreamingAgent` and `IStreamingMiddleware`. Replace all `IStreamingMessage` and `IStreamingMessage` with `IMessage` and `IMessage`. + +## 📚 Document Update +1. **Add example for using ollama + LiteLLM for function call** ([#3014](https://github.com/microsoft/autogen/issues/3014)) - Added a tutorial to the website for using ollama with LiteLLM. + +Thank you to all the contributors for making this release possible. We encourage everyone to upgrade to AutoGen.Net 0.0.16 to take advantage of these new features and improvements. If you encounter any issues or have any feedback, please let us know. + +Happy coding! 🚀 \ No newline at end of file diff --git a/dotnet/website/release_note/0.0.17.md b/dotnet/website/release_note/0.0.17.md new file mode 100644 index 000000000000..ad245191e7d0 --- /dev/null +++ b/dotnet/website/release_note/0.0.17.md @@ -0,0 +1,45 @@ +# AutoGen.Net 0.0.17 Release Notes + +## 🌟 What's New + +1. **.NET Core Target Framework Support** ([#3203](https://github.com/microsoft/autogen/issues/3203)) + - 🚀 Added support for .NET Core to ensure compatibility and enhanced performance of AutoGen packages across different platforms. + +2. **Kernel Support in Interactive Service Constructor** ([#3181](https://github.com/microsoft/autogen/issues/3181)) + - 🧠 Enhanced the Interactive Service to accept a kernel in its constructor, facilitating usage in notebook environments. + +3. **Constructor Options for OpenAIChatAgent** ([#3126](https://github.com/microsoft/autogen/issues/3126)) + - ⚙️ Added new constructor options for `OpenAIChatAgent` to allow full control over chat completion flags/options. + +4. **Step-by-Step Execution for Group Chat** ([#3075](https://github.com/microsoft/autogen/issues/3075)) + - 🛠️ Introduced an `IAsyncEnumerable` extension API to run group chat step-by-step, enabling developers to observe internal processes or implement early stopping mechanisms. + +## 🚀 Improvements + +1. **Cancellation Token Addition in Graph APIs** ([#3111](https://github.com/microsoft/autogen/issues/3111)) + - 🔄 Added cancellation tokens to async APIs in the `AutoGen.Core.Graph` class to follow best practices and enhance the control flow. + +## ⚠️ API Breaking Changes + +1. **FunctionDefinition Generation Stopped in Source Generator** ([#3133](https://github.com/microsoft/autogen/issues/3133)) + - 🛑 Stopped generating `FunctionDefinition` from `Azure.AI.OpenAI` in the source generator to eliminate unnecessary package dependencies. Migration guide: + - ➡️ Use `ToOpenAIFunctionDefinition()` extension from `AutoGen.OpenAI` for generating `FunctionDefinition` from `AutoGen.Core.FunctionContract`. + - ➡️ Use `FunctionContract` for metadata such as function name or parameters. + +2. **Namespace Renaming for AutoGen.WebAPI** ([#3152](https://github.com/microsoft/autogen/issues/3152)) + - ✏️ Renamed the namespace of `AutoGen.WebAPI` from `AutoGen.Service` to `AutoGen.WebAPI` to maintain consistency with the project name. + +3. **Semantic Kernel Version Update** ([#3118](https://github.com/microsoft/autogen/issues/3118)) + - 📈 Upgraded the Semantic Kernel version to 1.15.1 for enhanced functionality and performance improvements. This might introduce break change for those who use a lower-version semantic kernel. + +## 📚 Documentation + +1. **Consume AutoGen.Net Agent in AG Studio** ([#3142](https://github.com/microsoft/autogen/issues/3142)) + - Added detailed documentation on using AutoGen.Net Agent as a model in AG Studio, including examples of starting an OpenAI chat backend and integrating third-party OpenAI models. + +2. **Middleware Overview Documentation Errors Fixed** ([#3129](https://github.com/microsoft/autogen/issues/3129)) + - Corrected logic and compile errors in the example code provided in the Middleware Overview documentation to ensure it runs without issues. + +--- + +We hope you enjoy the new features and improvements in AutoGen.Net 0.0.17! If you encounter any issues or have feedback, please open a new issue on our [GitHub repository](https://github.com/microsoft/autogen/issues). \ No newline at end of file diff --git a/dotnet/website/release_note/0.1.0.md b/dotnet/website/release_note/0.1.0.md new file mode 100644 index 000000000000..dc844087758c --- /dev/null +++ b/dotnet/website/release_note/0.1.0.md @@ -0,0 +1,41 @@ +# 🎉 Release Notes: AutoGen.Net 0.1.0 🎉 + +## 📦 New Packages + +1. **Add AutoGen.AzureAIInference Package** + - **Issue**: [.Net][Feature Request] [#3323](https://github.com/microsoft/autogen/issues/3323) + - **Description**: The new `AutoGen.AzureAIInference` package includes the `ChatCompletionClientAgent`. + +## ✨ New Features + +1. **Enable Step-by-Step Execution for Two Agent Chat API** + - **Issue**: [.Net][Feature Request] [#3339](https://github.com/microsoft/autogen/issues/3339) + - **Description**: The `AgentExtension.SendAsync` now returns an `IAsyncEnumerable`, allowing conversations to be driven step by step, similar to how `GroupChatExtension.SendAsync` works. + +2. **Support Python Code Execution in AutoGen.DotnetInteractive** + - **Issue**: [.Net][Feature Request] [#3316](https://github.com/microsoft/autogen/issues/3316) + - **Description**: `dotnet-interactive` now supports Jupyter kernel connection, allowing Python code execution in `AutoGen.DotnetInteractive`. + +3. **Support Prompt Cache in Claude** + - **Issue**: [.Net][Feature Request] [#3359](https://github.com/microsoft/autogen/issues/3359) + - **Description**: Claude now supports prompt caching, which dramatically lowers the bill if the cache is hit. Added the corresponding option in the Claude client. + +## 🐛 Bug Fixes + +1. **GroupChatExtension.SendAsync Doesn’t Terminate Chat When `IOrchestrator` Returns Null as Next Agent** + - **Issue**: [.Net][Bug] [#3306](https://github.com/microsoft/autogen/issues/3306) + - **Description**: Fixed an issue where `GroupChatExtension.SendAsync` would continue until the max_round is reached even when `IOrchestrator` returns null as the next speaker. + +2. **InitializedMessages Are Added Repeatedly in GroupChatExtension.SendAsync Method** + - **Issue**: [.Net][Bug] [#3268](https://github.com/microsoft/autogen/issues/3268) + - **Description**: Fixed an issue where initialized messages from group chat were being added repeatedly in every iteration of the `GroupChatExtension.SendAsync` API. + +3. **Remove `Azure.AI.OpenAI` Dependency from `AutoGen.DotnetInteractive`** + - **Issue**: [.Net][Feature Request] [#3273](https://github.com/microsoft/autogen/issues/3273) + - **Description**: Fixed an issue by removing the `Azure.AI.OpenAI` dependency from `AutoGen.DotnetInteractive`, simplifying the package and reducing dependencies. + +## 📄 Documentation Updates + +1. **Add Function Comparison Page Between Python AutoGen and AutoGen.Net** + - **Issue**: [.Net][Document] [#3184](https://github.com/microsoft/autogen/issues/3184) + - **Description**: Added comparative documentation for features between AutoGen and AutoGen.Net across various functionalities and platform supports. \ No newline at end of file diff --git a/dotnet/website/release_note/0.2.0.md b/dotnet/website/release_note/0.2.0.md new file mode 100644 index 000000000000..ceff532b6056 --- /dev/null +++ b/dotnet/website/release_note/0.2.0.md @@ -0,0 +1,48 @@ +# Release Notes for AutoGen.Net v0.2.0 🚀 + +## New Features 🌟 +- **OpenAI Structural Format Output**: Added support for structural output format in the OpenAI integration. You can check out the example [here](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Structural_Output.cs) ([#3482](https://github.com/microsoft/autogen/issues/3482)). +- **Structural Output Configuration**: Introduced a property for overriding the structural output schema when generating replies with `GenerateReplyOption` ([#3436](https://github.com/microsoft/autogen/issues/3436)). + +## Bug Fixes 🐛 +- **Fixed Error Code 500**: Resolved an issue where an error occurred when the message history contained multiple different tool calls with the `name` field ([#3437](https://github.com/microsoft/autogen/issues/3437)). + +## Improvements 🔧 +- **Leverage OpenAI V2.0 in AutoGen.OpenAI package**: The `AutoGen.OpenAI` package now uses OpenAI v2.0, providing improved functionality and performance. In the meantime, the original `AutoGen.OpenAI` is still available and can be accessed by `AutoGen.OpenAI.V1`. This allows users who prefer to continue to use `Azure.AI.OpenAI v1` package in their project. ([#3193](https://github.com/microsoft/autogen/issues/3193)). +- **Deprecation of GPTAgent**: `GPTAgent` has been deprecated in favor of `OpenAIChatAgent` and `OpenAIMessageConnector` ([#3404](https://github.com/microsoft/autogen/issues/3404)). + +## Documentation 📚 +- **Tool Call Instructions**: Added detailed documentation on using tool calls with `ollama` and `OpenAIChatAgent` ([#3248](https://github.com/microsoft/autogen/issues/3248)). + +### Migration Guides 🔄 + +#### For the Deprecation of `GPTAgent` ([#3404](https://github.com/microsoft/autogen/issues/3404)): +**Before:** +```csharp +var agent = new GPTAgent(...); +``` +**After:** +```csharp +var agent = new OpenAIChatAgent(...) + .RegisterMessageConnector(); +``` + +#### For Using Azure.AI.OpenAI v2.0 ([#3193](https://github.com/microsoft/autogen/issues/3193)): +**Previous way of creating `OpenAIChatAgent`:** +```csharp +var openAIClient = new OpenAIClient(apiKey); +var openAIClientAgent = new OpenAIChatAgent( + openAIClient: openAIClient, + model: "gpt-4o-mini", + // Other parameters... + ); +``` + +**New way of creating `OpenAIChatAgent`:** +```csharp +var openAIClient = new OpenAIClient(apiKey); +var openAIClientAgent = new OpenAIChatAgent( + chatClient: openAIClient.GetChatClient("gpt-4o-mini"), + // Other parameters... + ); +``` \ No newline at end of file diff --git a/dotnet/website/release_note/0.2.1.md b/dotnet/website/release_note/0.2.1.md new file mode 100644 index 000000000000..353cdd080de9 --- /dev/null +++ b/dotnet/website/release_note/0.2.1.md @@ -0,0 +1,7 @@ +# Release Notes for AutoGen.Net v0.2.1 🚀 + +## New Features 🌟 +- **Support for OpenAi o1-preview** : Added support for OpenAI o1-preview model ([#3522](https://github.com/microsoft/autogen/issues/3522)) + +## Example 📚 +- **OpenAI o1-preview**: [Connect_To_OpenAI_o1_preview](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.OpenAI.Sample/Connect_To_OpenAI_o1_preview.cs) \ No newline at end of file diff --git a/dotnet/website/release_note/toc.yml b/dotnet/website/release_note/toc.yml new file mode 100644 index 000000000000..133656687d8e --- /dev/null +++ b/dotnet/website/release_note/toc.yml @@ -0,0 +1,17 @@ +- name: 0.2.1 + href: 0.2.1.md + +- name: 0.2.0 + href: 0.2.0.md + +- name: 0.1.0 + href: 0.1.0.md + +- name: 0.0.17 + href: 0.0.17.md + +- name: 0.0.16 + href: 0.0.16.md + +- name: 0.0.0 - 0.0.15 + href: update.md \ No newline at end of file diff --git a/dotnet/website/update.md b/dotnet/website/release_note/update.md similarity index 100% rename from dotnet/website/update.md rename to dotnet/website/release_note/update.md diff --git a/dotnet/website/toc.yml b/dotnet/website/toc.yml index b92e92d2c363..18a7eae08a83 100644 --- a/dotnet/website/toc.yml +++ b/dotnet/website/toc.yml @@ -3,12 +3,15 @@ - name: Tutorial href: tutorial/ - + - name: API Reference href: api/ - name: Release Notes - href: update.md + href: release_note/ + +- name: Comparison between Python AutoGen and AutoGen.Net + href: articles/function-comparison-page-between-python-AutoGen-and-autogen.net.md - name: Other Languages dropdown: true diff --git a/dotnet/website/tutorial/Create-agent-with-tools.md b/dotnet/website/tutorial/Create-agent-with-tools.md index d3af7bff6f67..5d631890308a 100644 --- a/dotnet/website/tutorial/Create-agent-with-tools.md +++ b/dotnet/website/tutorial/Create-agent-with-tools.md @@ -98,7 +98,8 @@ Some LLM models support parallel tool call, which returns multiple tool calls in [!code-csharp[Generate Response](../../sample/AutoGen.BasicSamples/GettingStart/Use_Tools_With_Agent.cs?name=parallel_tool_call)] ## Further Reading -- [Function-call-with-openai](../articles/OpenAIChatAgent-use-function-call.md) -- [Function-call-with-gemini](../articles/AutoGen.Gemini/Function-call-with-gemini.md) +- [Function call with openai](../articles/OpenAIChatAgent-use-function-call.md) +- [Function call with gemini](../articles/AutoGen.Gemini/Function-call-with-gemini.md) +- [Function call with local model](../articles/Function-call-with-ollama-and-litellm.md) - [Use kernel plugin in other agents](../articles/AutoGen.SemanticKernel/Use-kernel-plugin-in-other-agents.md) - [function call in mistral](../articles/MistralChatAgent-use-function-call.md) \ No newline at end of file diff --git a/dotnet/website/tutorial/Use-AutoGen.Net-agent-as-model-in-AG-Studio.md b/dotnet/website/tutorial/Use-AutoGen.Net-agent-as-model-in-AG-Studio.md new file mode 100644 index 000000000000..a47cb01f649e --- /dev/null +++ b/dotnet/website/tutorial/Use-AutoGen.Net-agent-as-model-in-AG-Studio.md @@ -0,0 +1,84 @@ +This tutorial shows how to use AutoGen.Net agent as model in AG Studio + +## Step 1. Create Dotnet empty web app and install AutoGen and AutoGen.WebAPI package + +```bash +dotnet new web +dotnet add package AutoGen +dotnet add package AutoGen.WebAPI +``` + +## Step 2. Replace the Program.cs with following code + +```bash +using AutoGen.Core; +using AutoGen.Service; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +var helloWorldAgent = new HelloWorldAgent(); +app.UseAgentAsOpenAIChatCompletionEndpoint(helloWorldAgent); + +app.Run(); + +class HelloWorldAgent : IAgent +{ + public string Name => "HelloWorld"; + + public Task GenerateReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new TextMessage(Role.Assistant, "Hello World!", from: this.Name)); + } +} +``` + +## Step 3: Start the web app + +Run the following command to start web api + +```bash +dotnet RUN +``` + +The web api will listen at `http://localhost:5264/v1/chat/completion + +![terminal](../images/articles/UseAutoGenAsModelinAGStudio/Terminal.png) + +## Step 4: In another terminal, start autogen-studio + +```bash +autogenstudio ui +``` + +## Step 5: Navigate to AutoGen Studio UI and add hello world agent as openai Model + +### Step 5.1: Go to model tab + +![The Model Tab](../images/articles/UseAutoGenAsModelinAGStudio/TheModelTab.png) + +### Step 5.2: Select "OpenAI model" card + +![Open AI model Card](../images/articles/UseAutoGenAsModelinAGStudio/Step5.2OpenAIModel.png) + +### Step 5.3: Fill the model name and url + +The model name needs to be same with agent name + +![Fill the model name and url](../images/articles/UseAutoGenAsModelinAGStudio/Step5.3ModelNameAndURL.png) + +## Step 6: Create a hello world agent that uses the hello world model + +![Create a hello world agent that uses the hello world model](../images/articles/UseAutoGenAsModelinAGStudio/Step6.png) + +![Agent Configuration](../images/articles/UseAutoGenAsModelinAGStudio/Step6b.png) + +## Final Step: Use the hello world agent in workflow + +![Use the hello world agent in workflow](../images/articles/UseAutoGenAsModelinAGStudio/FinalStepsA.png) + +![Use the hello world agent in workflow](../images/articles/UseAutoGenAsModelinAGStudio/FinalStepsA.png) + +![Use the hello world agent in workflow](../images/articles/UseAutoGenAsModelinAGStudio/FinalStepsB.png) + +![Use the hello world agent in workflow](../images/articles/UseAutoGenAsModelinAGStudio/FinalStepsC.png) diff --git a/dotnet/website/tutorial/toc.yml b/dotnet/website/tutorial/toc.yml index f624ec4af289..167baa70e4fd 100644 --- a/dotnet/website/tutorial/toc.yml +++ b/dotnet/website/tutorial/toc.yml @@ -5,4 +5,7 @@ href: Image-chat-with-agent.md - name: Create agent with tools - href: Create-agent-with-tools.md \ No newline at end of file + href: Create-agent-with-tools.md + +- name: Use AutoGen.Net agent as model in AG Studio + href: Use-AutoGen.Net-agent-as-model-in-AG-Studio.md \ No newline at end of file diff --git a/notebook/JSON_mode_example.ipynb b/notebook/JSON_mode_example.ipynb index f3adce9dec35..c4b65c4d9f4d 100644 --- a/notebook/JSON_mode_example.ipynb +++ b/notebook/JSON_mode_example.ipynb @@ -26,7 +26,7 @@ "\n", "\n", "## Requirements\n", - "JSON mode is a feature of OpenAI API, however strong models (such as claude 3 Opus), can generate appropriate json as well.\n", + "JSON mode is a feature of OpenAI API, however strong models (such as Claude 3 Opus), can generate appropriate json as well.\n", "AutoGen requires `Python>=3.8`. To run this notebook example, please install:\n", "```bash\n", "pip install pyautogen\n", @@ -71,7 +71,7 @@ "source": [ "## Model Configuration\n", "\n", - "we Need to set two different Configs for this to work. \n", + "We need to set two different Configs for this to work. \n", "One for JSON mode\n", "One for Text mode. \n", "This is because the group chat manager requires text mode. " diff --git a/notebook/agentchat_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat.ipynb index adb13ac47bd5..0b829835a0a3 100644 --- a/notebook/agentchat_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat.ipynb @@ -10,7 +10,7 @@ "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framework allows tool use and human participation through multi-agent conversation.\n", "Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n", "\n", - "RetrieveChat is a conversational system for retrieval-augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `RetrieveAssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)). Essentially, `RetrieveAssistantAgent` and `RetrieveUserProxyAgent` implement a different auto-reply mechanism corresponding to the RetrieveChat prompts.\n", + "RetrieveChat is a conversational system for retrieval-augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `AssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)). Essentially, `RetrieveUserProxyAgent` implement a different auto-reply mechanism corresponding to the RetrieveChat prompts.\n", "\n", "## Table of Contents\n", "We'll demonstrate six examples of using RetrieveChat for code generation and question answering:\n", @@ -31,6 +31,8 @@ "pip install pyautogen[retrievechat] flaml[automl]\n", "```\n", "\n", + "*You'll need to install `chromadb<=0.5.0` if you see issue like [#3551](https://github.com/microsoft/autogen/issues/3551).*\n", + "\n", "For more information, please refer to the [installation guide](/docs/installation/).\n", ":::\n", "````" @@ -48,14 +50,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "models to use: ['gpt-3.5-turbo-0125']\n" + "models to use: ['gpt-35-turbo', 'gpt4-1106-preview', 'gpt-4o']\n" ] } ], @@ -66,16 +68,14 @@ "import chromadb\n", "\n", "import autogen\n", - "from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent\n", + "from autogen import AssistantAgent\n", "from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent\n", "\n", "# Accepted file formats for that can be stored in\n", "# a vector database instance\n", "from autogen.retrieve_utils import TEXT_FORMATS\n", "\n", - "config_list = [\n", - " {\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": \"\", \"api_type\": \"openai\"},\n", - "]\n", + "config_list = autogen.config_list_from_json(\"OAI_CONFIG_LIST\")\n", "\n", "assert len(config_list) > 0\n", "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" @@ -94,7 +94,7 @@ "\n", "## Construct agents for RetrieveChat\n", "\n", - "We start by initializing the `RetrieveAssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for RetrieveAssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.message_generator` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant." + "We start by initializing the `AssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for AssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.message_generator` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant." ] }, { @@ -107,7 +107,7 @@ "output_type": "stream", "text": [ "Accepted file formats for `docs_path`:\n", - "['odt', 'xml', 'pdf', 'docx', 'html', 'md', 'htm', 'csv', 'rst', 'org', 'ppt', 'doc', 'log', 'json', 'epub', 'jsonl', 'pptx', 'yml', 'xlsx', 'tsv', 'txt', 'yaml', 'msg', 'rtf']\n" + "['txt', 'json', 'csv', 'tsv', 'md', 'html', 'htm', 'rtf', 'rst', 'jsonl', 'log', 'xml', 'yaml', 'yml', 'pdf']\n" ] } ], @@ -120,10 +120,19 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspace/anaconda3/envs/autogen312/lib/python3.12/site-packages/sentence_transformers/cross_encoder/CrossEncoder.py:11: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", + " from tqdm.autonotebook import tqdm, trange\n" + ] + } + ], "source": [ - "# 1. create an RetrieveAssistantAgent instance named \"assistant\"\n", - "assistant = RetrieveAssistantAgent(\n", + "# 1. create an AssistantAgent instance named \"assistant\"\n", + "assistant = AssistantAgent(\n", " name=\"assistant\",\n", " system_message=\"You are a helpful assistant.\",\n", " llm_config={\n", @@ -134,15 +143,9 @@ ")\n", "\n", "# 2. create the RetrieveUserProxyAgent instance named \"ragproxyagent\"\n", - "# By default, the human_input_mode is \"ALWAYS\", which means the agent will ask for human input at every step. We set it to \"NEVER\" here.\n", - "# `docs_path` is the path to the docs directory. It can also be the path to a single file, or the url to a single file. By default,\n", - "# it is set to None, which works only if the collection is already created.\n", - "# `task` indicates the kind of task we're working on. In this example, it's a `code` task.\n", - "# `chunk_token_size` is the chunk token size for the retrieve chat. By default, it is set to `max_tokens * 0.6`, here we set it to 2000.\n", - "# `custom_text_types` is a list of file types to be processed. Default is `autogen.retrieve_utils.TEXT_FORMATS`.\n", - "# This only applies to files under the directories in `docs_path`. Explicitly included files and urls will be chunked regardless of their types.\n", - "# In this example, we set it to [\"non-existent-type\"] to only process markdown files. Since no \"non-existent-type\" files are included in the `websit/docs`,\n", - "# no files there will be processed. However, the explicitly included urls will still be processed.\n", + "# Refer to https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/retrieve_user_proxy_agent\n", + "# and https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/vectordb/chromadb\n", + "# for more information on the RetrieveUserProxyAgent and ChromaVectorDB\n", "ragproxyagent = RetrieveUserProxyAgent(\n", " name=\"ragproxyagent\",\n", " human_input_mode=\"NEVER\",\n", @@ -152,14 +155,12 @@ " \"docs_path\": [\n", " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Examples/Integrate%20-%20Spark.md\",\n", " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Research.md\",\n", - " os.path.join(os.path.abspath(\"\"), \"..\", \"website\", \"docs\"),\n", " ],\n", - " \"custom_text_types\": [\"non-existent-type\"],\n", " \"chunk_token_size\": 2000,\n", " \"model\": config_list[0][\"model\"],\n", - " # \"client\": chromadb.PersistentClient(path=\"/tmp/chromadb\"), # deprecated, use \"vector_db\" instead\n", - " \"vector_db\": \"chroma\", # to use the deprecated `client` parameter, set to None and uncomment the line above\n", + " \"vector_db\": \"chroma\",\n", " \"overwrite\": False, # set to True if you want to overwrite an existing collection\n", + " \"get_or_create\": True, # set to False if don't want to reuse an existing collection\n", " },\n", " code_execution_config=False, # set to False if you don't want to execute the code\n", ")" @@ -188,7 +189,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-04-07 17:30:56,955 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `autogen-docs`.\u001b[0m\n" + "2024-08-14 06:22:06,884 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `autogen-docs`.\u001b[0m\n" ] }, { @@ -202,7 +203,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-04-07 17:30:59,609 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n", + "2024-08-14 06:22:07,353 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n", "Number of requested results 20 is greater than number of elements in index 2, updating n_results = 2\n" ] }, @@ -361,65 +362,53 @@ "--------------------------------------------------------------------------------\n", "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", "\n", - "To perform a classification task using FLAML and use Spark to do parallel training for 30 seconds and force cancel jobs if the time limit is reached, you can follow these steps:\n", - "\n", - "1. First, convert your data into Spark dataframe format using `to_pandas_on_spark` function from `flaml.automl.spark.utils` module.\n", - "2. Then, format your data for use SparkML models by using `VectorAssembler`.\n", - "3. Define your AutoML settings, including the `metric`, `time_budget`, and `task`.\n", - "4. Use `AutoML` from `flaml` to run AutoML with SparkML models by setting `use_spark` to `true`, and `estimator_list` to a list of spark-based estimators, like `[\"lgbm_spark\"]`.\n", - "5. Set `n_concurrent_trials` to the desired number of parallel jobs and `force_cancel` to `True` to cancel the jobs if the time limit is reached.\n", - "\n", - "Here's an example code snippet for performing classification using FLAML and Spark:\n", - "\n", "```python\n", - "import pandas as pd\n", + "import flaml\n", "from flaml.automl.spark.utils import to_pandas_on_spark\n", "from pyspark.ml.feature import VectorAssembler\n", - "import flaml\n", + "import pandas as pd\n", "\n", - "# Creating a dictionary\n", + "# Example Data (Please provide real data in practice)\n", "data = {\n", - " \"sepal_length\": [5.1, 4.9, 4.7, 4.6, 5.0],\n", - " \"sepal_width\": [3.5, 3.0, 3.2, 3.1, 3.6],\n", - " \"petal_length\": [1.4, 1.4, 1.3, 1.5, 1.4],\n", - " \"petal_width\": [0.2, 0.2, 0.2, 0.2, 0.2],\n", - " \"species\": [\"setosa\", \"setosa\", \"setosa\", \"setosa\", \"setosa\"]\n", + " \"feature1\": [0, 1, 2, 3, 4],\n", + " \"feature2\": [1, 2, 3, 4, 5],\n", + " # ... add all features you need for your classification\n", + " \"label\": ['a', 'b', 'a', 'a', 'b'], # assuming binary classification with labels 'a' and 'b'\n", "}\n", "\n", - "# Creating a pandas DataFrame\n", - "dataframe = pd.DataFrame(data)\n", - "label = \"species\"\n", + "# Convert to Pandas DataFrame\n", + "pdf = pd.DataFrame(data)\n", "\n", - "# Convert to pandas-on-spark dataframe\n", - "psdf = to_pandas_on_spark(dataframe)\n", + "# Generate pandas-on-spark dataframe\n", + "psdf = to_pandas_on_spark(pdf)\n", "\n", - "# Format data for SparkML models\n", - "columns = psdf.columns\n", - "feature_cols = [col for col in columns if col != label]\n", + "# Organize data into feature vectors and labels\n", + "label_col = \"label\"\n", + "feature_cols = [col for col in psdf.columns if col != label_col]\n", "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", "\n", - "# Define AutoML settings\n", - "settings = {\n", + "# Apply the transformation\n", + "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\", label_col]\n", + "\n", + "# Prepare AutoML settings\n", + "automl_settings = {\n", " \"time_budget\": 30,\n", - " \"metric\": \"accuracy\",\n", + " \"metric\": \"accuracy\", # Change this to a classification metric you prefer\n", " \"task\": \"classification\",\n", + " \"n_concurrent_trials\": 2, # Or other number that fits your Spark cluster configuration\n", + " \"use_spark\": True,\n", + " \"force_cancel\": True, # Enable force cancel to obey the time constraint\n", + " \"estimator_list\": [\"lgbm_spark\"], # Specify SparkML estimators you want to try\n", "}\n", "\n", - "# Use AutoML with SparkML models and parallel jobs\n", + "# Create an AutoML instance\n", "automl = flaml.AutoML()\n", - "automl.fit(\n", - " dataframe=psdf,\n", - " label=label,\n", - " estimator_list=[\"lgbm_spark\"],\n", - " use_spark=True,\n", - " n_concurrent_trials=2,\n", - " force_cancel=True,\n", - " **settings,\n", - ")\n", - "```\n", "\n", - "Note that the above code assumes the data is small enough to train within 30 seconds. If you have a larger dataset, you may need to increase the `time_budget` and adjust the number of parallel jobs accordingly.\n", + "# Run the AutoML search\n", + "automl.fit(dataframe=psdf, label=label_col, **automl_settings)\n", + "``` \n", + "\n", + "Remember to replace the example data with your real dataset and choose an appropriate metric for your classification task. You'll also need a configured and running Spark environment to utilize the \"use_spark\" feature.\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", @@ -439,25 +428,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Number of requested results 60 is greater than number of elements in index 2, updating n_results = 2\n", - "Number of requested results 100 is greater than number of elements in index 2, updating n_results = 2\n", - "Number of requested results 140 is greater than number of elements in index 2, updating n_results = 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", - "VectorDB returns doc_ids: [['bdfbc921']]\n", - "VectorDB returns doc_ids: [['bdfbc921']]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Number of requested results 180 is greater than number of elements in index 2, updating n_results = 2\n" + "Number of requested results 60 is greater than number of elements in index 2, updating n_results = 2\n" ] }, { @@ -470,18 +441,13 @@ "\n", "TERMINATE\n", "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "TERMINATE\n", + "\n", "--------------------------------------------------------------------------------\n" ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'TERMINATE', 'role': 'assistant'}], summary='', cost=({'total_cost': 0.007691, 'gpt-35-turbo': {'cost': 0.007691, 'prompt_tokens': 4242, 'completion_tokens': 664, 'total_tokens': 4906}}, {'total_cost': 0}), human_input=[])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ @@ -791,23 +757,22 @@ "\n", "\n", "\n", - "--------------------------------------------------------------------------------\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", "\n", - "The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.\n", + "The authors of FLAML (Fast and Lightweight AutoML) as mentioned in the provided context are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu. They are listed as the authors of the publication titled \"FLAML: A Fast and Lightweight AutoML Library\" which appeared in MLSys 2021.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "The authors of FLAML (Fast and Lightweight AutoML) as mentioned in the provided context are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu. They are listed as the authors of the publication titled \"FLAML: A Fast and Lightweight AutoML Library\" which appeared in MLSys 2021.\n", "\n", "--------------------------------------------------------------------------------\n" ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'You\\'re a retrieve augmented coding assistant. You answer user\\'s questions based on your own knowledge and the\\ncontext provided by the user.\\nIf you can\\'t answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\\nFor code generation, you must obey the following rules:\\nRule 1. You MUST NOT install any packages because all the packages needed are already installed.\\nRule 2. You must follow the formats below to write your code:\\n```language\\n# your code\\n```\\n\\nUser\\'s question is: Who is the author of FLAML?\\n\\nContext is: # Research\\n\\nFor technical details, please check our research publications.\\n\\n- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\\n\\n```bibtex\\n@inproceedings{wang2021flaml,\\n title={FLAML: A Fast and Lightweight AutoML Library},\\n author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\\n year={2021},\\n booktitle={MLSys},\\n}\\n```\\n\\n- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\\n\\n```bibtex\\n@inproceedings{wu2021cfo,\\n title={Frugal Optimization for Cost-related Hyperparameters},\\n author={Qingyun Wu and Chi Wang and Silu Huang},\\n year={2021},\\n booktitle={AAAI},\\n}\\n```\\n\\n- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\\n\\n```bibtex\\n@inproceedings{wang2021blendsearch,\\n title={Economical Hyperparameter Optimization With Blended Search Strategy},\\n author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\\n year={2021},\\n booktitle={ICLR},\\n}\\n```\\n\\n- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\\n\\n```bibtex\\n@inproceedings{liuwang2021hpolm,\\n title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\\n author={Susan Xueqing Liu and Chi Wang},\\n year={2021},\\n booktitle={ACL},\\n}\\n```\\n\\n- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\\n\\n```bibtex\\n@inproceedings{wu2021chacha,\\n title={ChaCha for Online AutoML},\\n author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\\n year={2021},\\n booktitle={ICML},\\n}\\n```\\n\\n- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\\n\\n```bibtex\\n@inproceedings{wuwang2021fairautoml,\\n title={Fair AutoML},\\n author={Qingyun Wu and Chi Wang},\\n year={2021},\\n booktitle={ArXiv preprint arXiv:2111.06495},\\n}\\n```\\n\\n- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\\n\\n```bibtex\\n@inproceedings{kayaliwang2022default,\\n title={Mining Robust Default Configurations for Resource-constrained AutoML},\\n author={Moe Kayali and Chi Wang},\\n year={2022},\\n booktitle={ArXiv preprint arXiv:2202.09927},\\n}\\n```\\n\\n- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\\n\\n```bibtex\\n@inproceedings{zhang2023targeted,\\n title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\\n author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\\n booktitle={International Conference on Learning Representations},\\n year={2023},\\n url={https://openreview.net/forum?id=0Ij9_q567Ma},\\n}\\n```\\n\\n- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\\n\\n```bibtex\\n@inproceedings{wang2023EcoOptiGen,\\n title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\\n author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2303.04673},\\n}\\n```\\n\\n- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\\n\\n```bibtex\\n@inproceedings{wu2023empirical,\\n title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\\n author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2306.01337},\\n}\\n```\\n# Integrate - Spark\\n\\nFLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\\n\\n- Use Spark ML estimators for AutoML.\\n- Use Spark to run training in parallel spark jobs.\\n\\n## Spark ML Estimators\\n\\nFLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\\n\\n### Data\\n\\nFor Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\\n\\nThis utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\\n\\nThis function also accepts optional arguments `index_col` and `default_index_type`.\\n\\n- `index_col` is the column name to use as the index, default is None.\\n- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\\n\\nHere is an example code snippet for Spark Data:\\n\\n```python\\nimport pandas as pd\\nfrom flaml.automl.spark.utils import to_pandas_on_spark\\n\\n# Creating a dictionary\\ndata = {\\n \"Square_Feet\": [800, 1200, 1800, 1500, 850],\\n \"Age_Years\": [20, 15, 10, 7, 25],\\n \"Price\": [100000, 200000, 300000, 240000, 120000],\\n}\\n\\n# Creating a pandas DataFrame\\ndataframe = pd.DataFrame(data)\\nlabel = \"Price\"\\n\\n# Convert to pandas-on-spark dataframe\\npsdf = to_pandas_on_spark(dataframe)\\n```\\n\\nTo use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\\n\\nHere is an example of how to use it:\\n\\n```python\\nfrom pyspark.ml.feature import VectorAssembler\\n\\ncolumns = psdf.columns\\nfeature_cols = [col for col in columns if col != label]\\nfeaturizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\\npsdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\\n```\\n\\nLater in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\\n\\n### Estimators\\n\\n#### Model List\\n\\n- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\\n\\n#### Usage\\n\\nFirst, prepare your data in the required format as described in the previous section.\\n\\nBy including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven\\'t specified them.\\n\\nHere is an example code snippet using SparkML models in AutoML:\\n\\n```python\\nimport flaml\\n\\n# prepare your data in pandas-on-spark format as we previously mentioned\\n\\nautoml = flaml.AutoML()\\nsettings = {\\n \"time_budget\": 30,\\n \"metric\": \"r2\",\\n \"estimator_list\": [\"lgbm_spark\"], # this setting is optional\\n \"task\": \"regression\",\\n}\\n\\nautoml.fit(\\n dataframe=psdf,\\n label=label,\\n **settings,\\n)\\n```\\n\\n[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\\n\\n## Parallel Spark Jobs\\n\\nYou can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\\n\\nPlease note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\\n\\nAll the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\\n\\n- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\\n- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performes parallel tuning.\\n- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\\n\\nAn example code snippet for using parallel Spark jobs:\\n\\n```python\\nimport flaml\\n\\nautoml_experiment = flaml.AutoML()\\nautoml_settings = {\\n \"time_budget\": 30,\\n \"metric\": \"r2\",\\n \"task\": \"regression\",\\n \"n_concurrent_trials\": 2,\\n \"use_spark\": True,\\n \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\\n}\\n\\nautoml.fit(\\n dataframe=dataframe,\\n label=label,\\n **automl_settings,\\n)\\n```\\n\\n[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\\n\\n', 'role': 'assistant'}, {'content': 'The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.', 'role': 'user'}], summary='The author of FLAML is Chi Wang, along with several co-authors for various publications related to FLAML.', cost=({'total_cost': 0.004711, 'gpt-35-turbo': {'cost': 0.004711, 'prompt_tokens': 3110, 'completion_tokens': 23, 'total_tokens': 3133}}, {'total_cost': 0}), human_input=[])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ @@ -2822,7 +2787,7 @@ ] }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "autogen312", "language": "python", "name": "python3" }, @@ -2836,7 +2801,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.4" }, "skip_test": "Requires interactive usage" }, diff --git a/notebook/agentchat_RetrieveChat_mongodb.ipynb b/notebook/agentchat_RetrieveChat_mongodb.ipynb new file mode 100644 index 000000000000..09c3c44bef27 --- /dev/null +++ b/notebook/agentchat_RetrieveChat_mongodb.ipynb @@ -0,0 +1,582 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using RetrieveChat Powered by MongoDB Atlas for Retrieve Augmented Code Generation and Question Answering\n", + "\n", + "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framework allows tool use and human participation through multi-agent conversation.\n", + "Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n", + "\n", + "RetrieveChat is a conversational system for retrieval-augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `AssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)). Essentially, `RetrieveUserProxyAgent` implement a different auto-reply mechanism corresponding to the RetrieveChat prompts.\n", + "\n", + "## Table of Contents\n", + "We'll demonstrate six examples of using RetrieveChat for code generation and question answering:\n", + "\n", + "- [Example 1: Generate code based off docstrings w/o human feedback](#example-1)\n", + "\n", + "````{=mdx}\n", + ":::info Requirements\n", + "Some extra dependencies are needed for this notebook, which can be installed via pip:\n", + "\n", + "```bash\n", + "pip install pyautogen[retrievechat-mongodb] flaml[automl]\n", + "```\n", + "\n", + "For more information, please refer to the [installation guide](/docs/installation/).\n", + ":::\n", + "````\n", + "\n", + "Ensure you have a MongoDB Atlas instance with Cluster Tier >= M10. Read more on Cluster support [here](https://www.mongodb.com/docs/atlas/atlas-search/manage-indexes/#create-and-manage-fts-indexes)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set your API Endpoint\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "models to use: ['gpt-3.5-turbo-0125']\n" + ] + } + ], + "source": [ + "import json\n", + "import os\n", + "\n", + "import autogen\n", + "from autogen import AssistantAgent\n", + "from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent\n", + "\n", + "# Accepted file formats for that can be stored in\n", + "# a vector database instance\n", + "from autogen.retrieve_utils import TEXT_FORMATS\n", + "\n", + "config_list = [{\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": os.environ[\"OPENAI_API_KEY\"], \"api_type\": \"openai\"}]\n", + "assert len(config_list) > 0\n", + "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````{=mdx}\n", + ":::tip\n", + "Learn more about configuring LLMs for agents [here](/docs/topics/llm_configuration).\n", + ":::\n", + "````\n", + "\n", + "## Construct agents for RetrieveChat\n", + "\n", + "We start by initializing the `AssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for AssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.message_generator` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accepted file formats for `docs_path`:\n", + "['txt', 'json', 'csv', 'tsv', 'md', 'html', 'htm', 'rtf', 'rst', 'jsonl', 'log', 'xml', 'yaml', 'yml', 'pdf']\n" + ] + } + ], + "source": [ + "print(\"Accepted file formats for `docs_path`:\")\n", + "print(TEXT_FORMATS)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# 1. create an AssistantAgent instance named \"assistant\"\n", + "assistant = AssistantAgent(\n", + " name=\"assistant\",\n", + " system_message=\"You are a helpful assistant.\",\n", + " llm_config={\n", + " \"timeout\": 600,\n", + " \"cache_seed\": 42,\n", + " \"config_list\": config_list,\n", + " },\n", + ")\n", + "\n", + "# 2. create the RetrieveUserProxyAgent instance named \"ragproxyagent\"\n", + "# Refer to https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/retrieve_user_proxy_agent\n", + "# and https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/vectordb/mongodb\n", + "# for more information on the RetrieveUserProxyAgent and MongoDBAtlasVectorDB\n", + "ragproxyagent = RetrieveUserProxyAgent(\n", + " name=\"ragproxyagent\",\n", + " human_input_mode=\"NEVER\",\n", + " max_consecutive_auto_reply=3,\n", + " retrieve_config={\n", + " \"task\": \"code\",\n", + " \"docs_path\": [\n", + " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Examples/Integrate%20-%20Spark.md\",\n", + " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Research.md\",\n", + " ],\n", + " \"chunk_token_size\": 2000,\n", + " \"model\": config_list[0][\"model\"],\n", + " \"vector_db\": \"mongodb\", # MongoDB Atlas database\n", + " \"collection_name\": \"demo_collection\",\n", + " \"db_config\": {\n", + " \"connection_string\": os.environ[\"MONGODB_URI\"], # MongoDB Atlas connection string\n", + " \"database_name\": \"test_db\", # MongoDB Atlas database\n", + " \"index_name\": \"vector_index\",\n", + " \"wait_until_index_ready\": 120.0, # Setting to wait 120 seconds or until index is constructed before querying\n", + " \"wait_until_document_ready\": 120.0, # Setting to wait 120 seconds or until document is properly indexed after insertion/update\n", + " },\n", + " \"get_or_create\": True, # set to False if you don't want to reuse an existing collection\n", + " \"overwrite\": False, # set to True if you want to overwrite an existing collection, each overwrite will force a index creation and reupload of documents\n", + " },\n", + " code_execution_config=False, # set to False if you don't want to execute the code\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1\n", + "\n", + "[Back to top](#table-of-contents)\n", + "\n", + "Use RetrieveChat to help generate sample code and automatically run the code and fix errors if there is any.\n", + "\n", + "Problem: Which API should I use if I want to use FLAML for a classification task and I want to train the model in 30 seconds. Use spark to parallel the training. Force cancel jobs if time limit is reached." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-25 13:47:30,700 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `demo_collection`.\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying to create collection.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-25 13:47:31,048 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n", + "2024-07-25 13:47:31,051 - autogen.agentchat.contrib.vectordb.mongodb - INFO - No documents to insert.\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", + "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[32mAdding content of doc 7968cf3c to context.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: How can I use FLAML to perform a classification task and use spark to do parallel training. Train 30 seconds and force cancel jobs if time limit is reached.\n", + "\n", + "Context is: # Integrate - Spark\n", + "\n", + "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", + "\n", + "- Use Spark ML estimators for AutoML.\n", + "- Use Spark to run training in parallel spark jobs.\n", + "\n", + "## Spark ML Estimators\n", + "\n", + "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", + "\n", + "### Data\n", + "\n", + "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", + "\n", + "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", + "\n", + "This function also accepts optional arguments `index_col` and `default_index_type`.\n", + "\n", + "- `index_col` is the column name to use as the index, default is None.\n", + "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", + "\n", + "Here is an example code snippet for Spark Data:\n", + "\n", + "```python\n", + "import pandas as pd\n", + "from flaml.automl.spark.utils import to_pandas_on_spark\n", + "\n", + "# Creating a dictionary\n", + "data = {\n", + " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", + " \"Age_Years\": [20, 15, 10, 7, 25],\n", + " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", + "}\n", + "\n", + "# Creating a pandas DataFrame\n", + "dataframe = pd.DataFrame(data)\n", + "label = \"Price\"\n", + "\n", + "# Convert to pandas-on-spark dataframe\n", + "psdf = to_pandas_on_spark(dataframe)\n", + "```\n", + "\n", + "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", + "\n", + "Here is an example of how to use it:\n", + "\n", + "```python\n", + "from pyspark.ml.feature import VectorAssembler\n", + "\n", + "columns = psdf.columns\n", + "feature_cols = [col for col in columns if col != label]\n", + "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", + "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "```\n", + "\n", + "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", + "\n", + "### Estimators\n", + "\n", + "#### Model List\n", + "\n", + "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", + "\n", + "#### Usage\n", + "\n", + "First, prepare your data in the required format as described in the previous section.\n", + "\n", + "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", + "\n", + "Here is an example code snippet using SparkML models in AutoML:\n", + "\n", + "```python\n", + "import flaml\n", + "\n", + "# prepare your data in pandas-on-spark format as we previously mentioned\n", + "\n", + "automl = flaml.AutoML()\n", + "settings = {\n", + " \"time_budget\": 30,\n", + " \"metric\": \"r2\",\n", + " \"estimator_list\": [\"lgbm_spark\"], # this setting is optional\n", + " \"task\": \"regression\",\n", + "}\n", + "\n", + "automl.fit(\n", + " dataframe=psdf,\n", + " label=label,\n", + " **settings,\n", + ")\n", + "```\n", + "\n", + "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/automl_bankrupt_synapseml.ipynb)\n", + "\n", + "## Parallel Spark Jobs\n", + "\n", + "You can activate Spark as the parallel backend during parallel tuning in both [AutoML](/docs/Use-Cases/Task-Oriented-AutoML#parallel-tuning) and [Hyperparameter Tuning](/docs/Use-Cases/Tune-User-Defined-Function#parallel-tuning), by setting the `use_spark` to `true`. FLAML will dispatch your job to the distributed Spark backend using [`joblib-spark`](https://github.com/joblib/joblib-spark).\n", + "\n", + "Please note that you should not set `use_spark` to `true` when applying AutoML and Tuning for Spark Data. This is because only SparkML models will be used for Spark Data in AutoML and Tuning. As SparkML models run in parallel, there is no need to distribute them with `use_spark` again.\n", + "\n", + "All the Spark-related arguments are stated below. These arguments are available in both Hyperparameter Tuning and AutoML:\n", + "\n", + "- `use_spark`: boolean, default=False | Whether to use spark to run the training in parallel spark jobs. This can be used to accelerate training on large models and large datasets, but will incur more overhead in time and thus slow down training in some cases. GPU training is not supported yet when use_spark is True. For Spark clusters, by default, we will launch one trial per executor. However, sometimes we want to launch more trials than the number of executors (e.g., local mode). In this case, we can set the environment variable `FLAML_MAX_CONCURRENT` to override the detected `num_executors`. The final number of concurrent trials will be the minimum of `n_concurrent_trials` and `num_executors`.\n", + "- `n_concurrent_trials`: int, default=1 | The number of concurrent trials. When n_concurrent_trials > 1, FLAML performes parallel tuning.\n", + "- `force_cancel`: boolean, default=False | Whether to forcely cancel Spark jobs if the search time exceeded the time budget. Spark jobs include parallel tuning jobs and Spark-based model training jobs.\n", + "\n", + "An example code snippet for using parallel Spark jobs:\n", + "\n", + "```python\n", + "import flaml\n", + "\n", + "automl_experiment = flaml.AutoML()\n", + "automl_settings = {\n", + " \"time_budget\": 30,\n", + " \"metric\": \"r2\",\n", + " \"task\": \"regression\",\n", + " \"n_concurrent_trials\": 2,\n", + " \"use_spark\": True,\n", + " \"force_cancel\": True, # Activating the force_cancel option can immediately halt Spark jobs once they exceed the allocated time_budget.\n", + "}\n", + "\n", + "automl.fit(\n", + " dataframe=dataframe,\n", + " label=label,\n", + " **automl_settings,\n", + ")\n", + "```\n", + "\n", + "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", + "# Research\n", + "\n", + "For technical details, please check our research publications.\n", + "\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021flaml,\n", + " title={FLAML: A Fast and Lightweight AutoML Library},\n", + " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", + " year={2021},\n", + " booktitle={MLSys},\n", + "}\n", + "```\n", + "\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021cfo,\n", + " title={Frugal Optimization for Cost-related Hyperparameters},\n", + " author={Qingyun Wu and Chi Wang and Silu Huang},\n", + " year={2021},\n", + " booktitle={AAAI},\n", + "}\n", + "```\n", + "\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021blendsearch,\n", + " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", + " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", + " year={2021},\n", + " booktitle={ICLR},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{liuwang2021hpolm,\n", + " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", + " author={Susan Xueqing Liu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ACL},\n", + "}\n", + "```\n", + "\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021chacha,\n", + " title={ChaCha for Online AutoML},\n", + " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", + " year={2021},\n", + " booktitle={ICML},\n", + "}\n", + "```\n", + "\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "\n", + "```bibtex\n", + "@inproceedings{wuwang2021fairautoml,\n", + " title={Fair AutoML},\n", + " author={Qingyun Wu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ArXiv preprint arXiv:2111.06495},\n", + "}\n", + "```\n", + "\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "\n", + "```bibtex\n", + "@inproceedings{kayaliwang2022default,\n", + " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", + " author={Moe Kayali and Chi Wang},\n", + " year={2022},\n", + " booktitle={ArXiv preprint arXiv:2202.09927},\n", + "}\n", + "```\n", + "\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "\n", + "```bibtex\n", + "@inproceedings{zhang2023targeted,\n", + " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", + " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", + " booktitle={International Conference on Learning Representations},\n", + " year={2023},\n", + " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", + "}\n", + "```\n", + "\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2023EcoOptiGen,\n", + " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", + " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2303.04673},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2023empirical,\n", + " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", + " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2306.01337},\n", + "}\n", + "```\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "To use FLAML to perform a classification task and use Spark for parallel training with a timeout of 30 seconds and force canceling jobs if the time limit is reached, you can follow the below code snippet:\n", + "\n", + "```python\n", + "import flaml\n", + "from flaml.automl.spark.utils import to_pandas_on_spark\n", + "from pyspark.ml.feature import VectorAssembler\n", + "\n", + "# Prepare your data in pandas-on-spark format\n", + "data = {\n", + " \"feature1\": [val1, val2, val3, val4],\n", + " \"feature2\": [val5, val6, val7, val8],\n", + " \"target\": [class1, class2, class1, class2],\n", + "}\n", + "\n", + "dataframe = pd.DataFrame(data)\n", + "label = \"target\"\n", + "psdf = to_pandas_on_spark(dataframe)\n", + "\n", + "# Prepare your features using VectorAssembler\n", + "columns = psdf.columns\n", + "feature_cols = [col for col in columns if col != label]\n", + "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", + "psdf = featurizer.transform(psdf)\n", + "\n", + "# Define AutoML settings and fit the model\n", + "automl = flaml.AutoML()\n", + "settings = {\n", + " \"time_budget\": 30,\n", + " \"metric\": \"accuracy\",\n", + " \"task\": \"classification\",\n", + " \"estimator_list\": [\"lgbm_spark\"], # Optional\n", + "}\n", + "\n", + "automl.fit(\n", + " dataframe=psdf,\n", + " label=label,\n", + " **settings,\n", + ")\n", + "```\n", + "\n", + "In the code:\n", + "- Replace `val1, val2, ..., class1, class2` with your actual data values.\n", + "- Ensure the features and target columns are correctly specified in the data dictionary.\n", + "- Set the `time_budget` parameter to 30 to limit the training time.\n", + "- The `force_cancel` parameter is set to `True` to force cancel Spark jobs if the time limit is exceeded.\n", + "\n", + "Make sure to adapt the code to your specific dataset and requirements.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "UPDATE CONTEXT\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", + "VectorDB returns doc_ids: [['bdfbc921', '7968cf3c']]\n", + "\u001b[32mNo more context, will terminate.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "# reset the assistant. Always reset the assistant before starting a new conversation.\n", + "assistant.reset()\n", + "\n", + "# given a problem, we use the ragproxyagent to generate a prompt to be sent to the assistant as the initial message.\n", + "# the assistant receives the message and generates a response. The response will be sent back to the ragproxyagent for processing.\n", + "# The conversation continues until the termination condition is met, in RetrieveChat, the termination condition when no human-in-loop is no code block detected.\n", + "# With human-in-loop, the conversation will continue until the user says \"exit\".\n", + "code_problem = \"How can I use FLAML to perform a classification task and use spark to do parallel training. Train 30 seconds and force cancel jobs if time limit is reached.\"\n", + "chat_result = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=code_problem)" + ] + } + ], + "metadata": { + "front_matter": { + "description": "Explore the use of AutoGen's RetrieveChat for tasks like code generation from docstrings, answering complex questions with human feedback, and exploiting features like Update Context, custom prompts, and few-shot learning.", + "tags": [ + "RAG" + ] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + }, + "skip_test": "Requires interactive usage" + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebook/agentchat_pgvector_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat_pgvector.ipynb similarity index 96% rename from notebook/agentchat_pgvector_RetrieveChat.ipynb rename to notebook/agentchat_RetrieveChat_pgvector.ipynb index 1a8d70e29654..4d9dd44c33d8 100644 --- a/notebook/agentchat_pgvector_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat_pgvector.ipynb @@ -10,7 +10,7 @@ "AutoGen offers conversable agents powered by LLM, tool or human, which can be used to perform tasks collectively via automated chat. This framework allows tool use and human participation through multi-agent conversation.\n", "Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n", "\n", - "RetrieveChat is a conversational system for retrieval-augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `RetrieveAssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)). Essentially, `RetrieveAssistantAgent` and `RetrieveUserProxyAgent` implement a different auto-reply mechanism corresponding to the RetrieveChat prompts.\n", + "RetrieveChat is a conversational system for retrieval-augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `AssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)). Essentially, `RetrieveUserProxyAgent` implement a different auto-reply mechanism corresponding to the RetrieveChat prompts.\n", "\n", "## Table of Contents\n", "We'll demonstrate six examples of using RetrieveChat for code generation and question answering:\n", @@ -92,29 +92,13 @@ "from sentence_transformers import SentenceTransformer\n", "\n", "import autogen\n", - "from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent\n", + "from autogen import AssistantAgent\n", "from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent\n", "\n", "# Accepted file formats for that can be stored in\n", "# a vector database instance\n", "from autogen.retrieve_utils import TEXT_FORMATS\n", "\n", - "config_list = [\n", - " {\n", - " \"model\": \"Meta-Llama-3-8B-Instruct-imatrix\",\n", - " \"api_key\": \"YOUR_API_KEY\",\n", - " \"base_url\": \"http://localhost:8080/v1\",\n", - " \"api_type\": \"openai\",\n", - " },\n", - " {\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": \"YOUR_API_KEY\", \"api_type\": \"openai\"},\n", - " {\n", - " \"model\": \"gpt-35-turbo\",\n", - " \"base_url\": \"...\",\n", - " \"api_type\": \"azure\",\n", - " \"api_version\": \"2023-07-01-preview\",\n", - " \"api_key\": \"...\",\n", - " },\n", - "]\n", "config_list = autogen.config_list_from_json(\n", " \"OAI_CONFIG_LIST\",\n", " file_location=\".\",\n", @@ -136,7 +120,7 @@ "\n", "## Construct agents for RetrieveChat\n", "\n", - "We start by initializing the `RetrieveAssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for RetrieveAssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.message_generator` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant." + "We start by initializing the `AssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for AssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.message_generator` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant." ] }, { @@ -173,8 +157,8 @@ } ], "source": [ - "# 1. create an RetrieveAssistantAgent instance named \"assistant\"\n", - "assistant = RetrieveAssistantAgent(\n", + "# 1. create an AssistantAgent instance named \"assistant\"\n", + "assistant = AssistantAgent(\n", " name=\"assistant\",\n", " system_message=\"You are a helpful assistant. You must always reply with some form of text.\",\n", " llm_config={\n", @@ -191,15 +175,9 @@ "sentence_transformer_ef = SentenceTransformer(\"all-distilroberta-v1\").encode\n", "\n", "# 2. create the RetrieveUserProxyAgent instance named \"ragproxyagent\"\n", - "# By default, the human_input_mode is \"ALWAYS\", which means the agent will ask for human input at every step. We set it to \"NEVER\" here.\n", - "# `docs_path` is the path to the docs directory. It can also be the path to a single file, or the url to a single file. By default,\n", - "# it is set to None, which works only if the collection is already created.\n", - "# `task` indicates the kind of task we're working on. In this example, it's a `code` task.\n", - "# `chunk_token_size` is the chunk token size for the retrieve chat. By default, it is set to `max_tokens * 0.6`, here we set it to 2000.\n", - "# `custom_text_types` is a list of file types to be processed. Default is `autogen.retrieve_utils.TEXT_FORMATS`.\n", - "# This only applies to files under the directories in `docs_path`. Explicitly included files and urls will be chunked regardless of their types.\n", - "# In this example, we set it to [\"non-existent-type\"] to only process markdown files. Since no \"non-existent-type\" files are included in the `websit/docs`,\n", - "# no files there will be processed. However, the explicitly included urls will still be processed.\n", + "# Refer to https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/retrieve_user_proxy_agent\n", + "# and https://microsoft.github.io/autogen/docs/reference/agentchat/contrib/vectordb/pgvectordb\n", + "# for more information on the RetrieveUserProxyAgent and PGVectorDB\n", "ragproxyagent = RetrieveUserProxyAgent(\n", " name=\"ragproxyagent\",\n", " human_input_mode=\"NEVER\",\n", @@ -209,9 +187,7 @@ " \"docs_path\": [\n", " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Examples/Integrate%20-%20Spark.md\",\n", " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Research.md\",\n", - " os.path.join(os.path.abspath(\"\"), \"..\", \"website\", \"docs\"),\n", " ],\n", - " \"custom_text_types\": [\"non-existent-type\"],\n", " \"chunk_token_size\": 2000,\n", " \"model\": config_list[0][\"model\"],\n", " \"vector_db\": \"pgvector\", # PGVector database\n", diff --git a/notebook/agentchat_RetrieveChat_qdrant.ipynb b/notebook/agentchat_RetrieveChat_qdrant.ipynb new file mode 100644 index 000000000000..0035a8e30817 --- /dev/null +++ b/notebook/agentchat_RetrieveChat_qdrant.ipynb @@ -0,0 +1,1012 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using RetrieveChat with Qdrant for Retrieve Augmented Code Generation and Question Answering\n", + "\n", + "[Qdrant](https://qdrant.tech/) is a high-performance vector search engine/database.\n", + "\n", + "This notebook demonstrates the usage of Qdrant for RAG, based on [agentchat_RetrieveChat.ipynb](https://colab.research.google.com/github/microsoft/autogen/blob/main/notebook/agentchat_RetrieveChat.ipynb).\n", + "\n", + "\n", + "RetrieveChat is a conversational system for retrieve augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `AssistantAgent` and `RetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)).\n", + "\n", + "We'll demonstrate usage of RetrieveChat with Qdrant for code generation and question answering w/ human feedback.\n", + "\n", + "````{=mdx}\n", + ":::info Requirements\n", + "Some extra dependencies are needed for this notebook, which can be installed via pip:\n", + "\n", + "```bash\n", + "pip install \"pyautogen[retrievechat-qdrant]\" \"flaml[automl]\"\n", + "```\n", + "\n", + "For more information, please refer to the [installation guide](/docs/installation/).\n", + ":::\n", + "````" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install \"pyautogen[retrievechat-qdrant]\" \"flaml[automl]\" -q" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set your API Endpoint\n", + "\n", + "The [`config_list_from_json`](https://microsoft.github.io/autogen/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "models to use: ['gpt4-1106-preview', 'gpt-4o', 'gpt-35-turbo', 'gpt-35-turbo-0613']\n" + ] + } + ], + "source": [ + "from qdrant_client import QdrantClient\n", + "from sentence_transformers import SentenceTransformer\n", + "\n", + "import autogen\n", + "from autogen import AssistantAgent\n", + "from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent\n", + "\n", + "# Accepted file formats for that can be stored in\n", + "# a vector database instance\n", + "from autogen.retrieve_utils import TEXT_FORMATS\n", + "\n", + "config_list = autogen.config_list_from_json(\"OAI_CONFIG_LIST\")\n", + "\n", + "assert len(config_list) > 0\n", + "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````{=mdx}\n", + ":::tip\n", + "Learn more about configuring LLMs for agents [here](/docs/topics/llm_configuration).\n", + ":::\n", + "````" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accepted file formats for `docs_path`:\n", + "['rtf', 'jsonl', 'xml', 'json', 'md', 'rst', 'docx', 'msg', 'pdf', 'log', 'xlsx', 'org', 'txt', 'csv', 'pptx', 'tsv', 'yml', 'epub', 'yaml', 'ppt', 'htm', 'doc', 'odt', 'html']\n" + ] + } + ], + "source": [ + "print(\"Accepted file formats for `docs_path`:\")\n", + "print(TEXT_FORMATS)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Construct agents for RetrieveChat\n", + "\n", + "We start by initializing the `AssistantAgent` and `RetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for AssistantAgent. The detailed instructions are given in the user message. Later we will use the `RetrieveUserProxyAgent.generate_init_prompt` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant.\n", + "\n", + "### You can find the list of all the embedding models supported by Qdrant [here](https://qdrant.github.io/fastembed/examples/Supported_Models/)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "67171b10626248ba8b5bff0f5a4d6895", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Fetching 5 files: 0%| | 0/5 [00:00\n", + "### Example 1\n", + "\n", + "[back to top](#toc)\n", + "\n", + "Use RetrieveChat to answer a question and ask for human-in-loop feedbacks.\n", + "\n", + "Problem: Is there a function named `tune_automl` in FLAML?" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying to create collection.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-15 23:19:34,988 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 3 chunks.\u001b[0m\n", + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "VectorDB returns doc_ids: [['987f060a-4399-b91a-0e51-51b6165ea5bb', '0ecd7192-3761-7d6f-9151-5ff504ca740b', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "\u001b[32mAdding content of doc 987f060a-4399-b91a-0e51-51b6165ea5bb to context.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: Is there a function called tune_automl?\n", + "\n", + "Context is: [![PyPI version](https://badge.fury.io/py/FLAML.svg)](https://badge.fury.io/py/FLAML)\n", + "![Conda version](https://img.shields.io/conda/vn/conda-forge/flaml)\n", + "[![Build](https://github.com/microsoft/FLAML/actions/workflows/python-package.yml/badge.svg)](https://github.com/microsoft/FLAML/actions/workflows/python-package.yml)\n", + "![Python Version](https://img.shields.io/badge/3.8%20%7C%203.9%20%7C%203.10-blue)\n", + "[![Downloads](https://pepy.tech/badge/flaml)](https://pepy.tech/project/flaml)\n", + "[![](https://img.shields.io/discord/1025786666260111483?logo=discord&style=flat)](https://discord.gg/Cppx2vSPVP)\n", + "\n", + "\n", + "\n", + "# A Fast Library for Automated Machine Learning & Tuning\n", + "\n", + "

\n", + " \n", + "
\n", + "

\n", + "\n", + ":fire: Heads-up: We have migrated [AutoGen](https://microsoft.github.io/autogen/) into a dedicated [github repository](https://github.com/microsoft/autogen). Alongside this move, we have also launched a dedicated [Discord](https://discord.gg/pAbnFJrkgZ) server and a [website](https://microsoft.github.io/autogen/) for comprehensive documentation.\n", + "\n", + ":fire: The automated multi-agent chat framework in [AutoGen](https://microsoft.github.io/autogen/) is in preview from v2.0.0.\n", + "\n", + ":fire: FLAML is highlighted in OpenAI's [cookbook](https://github.com/openai/openai-cookbook#related-resources-from-around-the-web).\n", + "\n", + ":fire: [autogen](https://microsoft.github.io/autogen/) is released with support for ChatGPT and GPT-4, based on [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673).\n", + "\n", + ":fire: FLAML supports Code-First AutoML & Tuning – Private Preview in [Microsoft Fabric Data Science](https://learn.microsoft.com/en-us/fabric/data-science/).\n", + "\n", + "## What is FLAML\n", + "\n", + "FLAML is a lightweight Python library for efficient automation of machine\n", + "learning and AI operations. It automates workflow based on large language models, machine learning models, etc.\n", + "and optimizes their performance.\n", + "\n", + "- FLAML enables building next-gen GPT-X applications based on multi-agent conversations with minimal effort. It simplifies the orchestration, automation and optimization of a complex GPT-X workflow. It maximizes the performance of GPT-X models and augments their weakness.\n", + "- For common machine learning tasks like classification and regression, it quickly finds quality models for user-provided data with low computational resources. It is easy to customize or extend. Users can find their desired customizability from a smooth range.\n", + "- It supports fast and economical automatic tuning (e.g., inference hyperparameters for foundation models, configurations in MLOps/LMOps workflows, pipelines, mathematical/statistical models, algorithms, computing experiments, software configurations), capable of handling large search space with heterogeneous evaluation cost and complex constraints/guidance/early stopping.\n", + "\n", + "FLAML is powered by a series of [research studies](https://microsoft.github.io/FLAML/docs/Research/) from Microsoft Research and collaborators such as Penn State University, Stevens Institute of Technology, University of Washington, and University of Waterloo.\n", + "\n", + "FLAML has a .NET implementation in [ML.NET](http://dot.net/ml), an open-source, cross-platform machine learning framework for .NET.\n", + "\n", + "## Installation\n", + "\n", + "FLAML requires **Python version >= 3.8**. It can be installed from pip:\n", + "\n", + "```bash\n", + "pip install flaml\n", + "```\n", + "\n", + "Minimal dependencies are installed without extra options. You can install extra options based on the feature you need. For example, use the following to install the dependencies needed by the [`autogen`](https://microsoft.github.io/autogen/) package.\n", + "\n", + "```bash\n", + "pip install \"flaml[autogen]\"\n", + "```\n", + "\n", + "Find more options in [Installation](https://microsoft.github.io/FLAML/docs/Installation).\n", + "Each of the [`notebook examples`](https://github.com/microsoft/FLAML/tree/main/notebook) may require a specific option to be installed.\n", + "\n", + "## Quickstart\n", + "\n", + "- (New) The [autogen](https://microsoft.github.io/autogen/) package enables the next-gen GPT-X applications with a generic multi-agent conversation framework.\n", + " It offers customizable and conversable agents which integrate LLMs, tools and human.\n", + " By automating chat among multiple capable agents, one can easily make them collectively perform tasks autonomously or with human feedback, including tasks that require using tools via code. For example,\n", + "\n", + "```python\n", + "from flaml import autogen\n", + "\n", + "assistant = autogen.AssistantAgent(\"assistant\")\n", + "user_proxy = autogen.UserProxyAgent(\"user_proxy\")\n", + "user_proxy.initiate_chat(\n", + " assistant,\n", + " message=\"Show me the YTD gain of 10 largest technology companies as of today.\",\n", + ")\n", + "# This initiates an automated chat between the two agents to solve the task\n", + "```\n", + "\n", + "Autogen also helps maximize the utility out of the expensive LLMs such as ChatGPT and GPT-4. It offers a drop-in replacement of `openai.Completion` or `openai.ChatCompletion` with powerful functionalites like tuning, caching, templating, filtering. For example, you can optimize generations by LLM with your own tuning data, success metrics and budgets.\n", + "\n", + "```python\n", + "# perform tuning\n", + "config, analysis = autogen.Completion.tune(\n", + " data=tune_data,\n", + " metric=\"success\",\n", + " mode=\"max\",\n", + " eval_func=eval_func,\n", + " inference_budget=0.05,\n", + " optimization_budget=3,\n", + " num_samples=-1,\n", + ")\n", + "# perform inference for a test instance\n", + "response = autogen.Completion.create(context=test_instance, **config)\n", + "```\n", + "\n", + "- With three lines of code, you can start using this economical and fast\n", + " AutoML engine as a [scikit-learn style estimator](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML).\n", + "\n", + "```python\n", + "from flaml import AutoML\n", + "\n", + "automl = AutoML()\n", + "automl.fit(X_train, y_train, task=\"classification\")\n", + "```\n", + "\n", + "- You can restrict the learners and use FLAML as a fast hyperparameter tuning\n", + " tool for XGBoost, LightGBM, Random Forest etc. or a [customized learner](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML#estimator-and-search-space).\n", + "\n", + "```python\n", + "automl.fit(X_train, y_train, task=\"classification\", estimator_list=[\"lgbm\"])\n", + "```\n", + "\n", + "- You can also run generic hyperparameter tuning for a [custom function](https://microsoft.github.io/FLAML/docs/Use-Cases/Tune-User-Defined-Function).\n", + "\n", + "```python\n", + "from flaml import tune\n", + "tune.run(evaluation_function, config={…}, low_cost_partial_config={…}, time_budget_s=3600)\n", + "```\n", + "\n", + "- [Zero-shot AutoML](https://microsoft.github.io/FLAML/docs/Use-Cases/Zero-Shot-AutoML) allows using the existing training API from lightgbm, xgboost etc. while getting the benefit of AutoML in choosing high-performance hyperparameter configurations per task.\n", + "\n", + "```python\n", + "from flaml.default import LGBMRegressor\n", + "\n", + "# Use LGBMRegressor in the same way as you use lightgbm.LGBMRegressor.\n", + "estimator = LGBMRegressor()\n", + "# The hyperparameters are automatically set according to the training data.\n", + "estimator.fit(X_train, y_train)\n", + "```\n", + "\n", + "## Documentation\n", + "\n", + "You can find a detailed documentation about FLAML [here](https://microsoft.github.io/FLAML/).\n", + "\n", + "In addition, you can find:\n", + "\n", + "- [Research](https://microsoft.github.io/FLAML/docs/Research) and [blogposts](https://microsoft.github.io/FLAML/blog) around FLAML.\n", + "\n", + "- [Discord](https://discord.gg/Cppx2vSPVP).\n", + "\n", + "- [Contributing guide](https://microsoft.github.io/FLAML/docs/Contribute).\n", + "\n", + "- ML.NET documentation and tutorials for [Model Builder](https://learn.microsoft.com/dotnet/machine-learning/tutorials/predict-prices-with-model-builder), [ML.NET CLI](https://learn.microsoft.com/dotnet/machine-learning/tutorials/sentiment-analysis-cli), and [AutoML API](https://learn.microsoft.com/dotnet/machine-learning/how-to-guides/how-to-use-the-automl-api).\n", + "\n", + "## Contributing\n", + "\n", + "This project welcomes contributions and suggestions. Most contributions require you to agree to a\n", + "Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\n", + "the rights to use your contribution. For details, visit .\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "No, there is no function called `tune_automl` specifically mentioned in the context provided. However, FLAML does offer general hyperparameter tuning capabilities which could be related to this. In the context of FLAML, there is a generic function called `tune.run()` that can be used for hyperparameter tuning.\n", + "\n", + "Here's a short example of how to use FLAML's tune for a user-defined function based on the given context:\n", + "\n", + "```python\n", + "from flaml import tune\n", + "\n", + "def evaluation_function(config):\n", + " # evaluation logic that returns a metric score\n", + " ...\n", + "\n", + "# define the search space for hyperparameters\n", + "config_search_space = {\n", + " 'max_depth': tune.randint(lower=3, upper=10),\n", + " 'learning_rate': tune.loguniform(lower=1e-4, upper=1e-1),\n", + "}\n", + "\n", + "# run hyperparameter tuning\n", + "tune.run(\n", + " evaluation_function,\n", + " config=config_search_space,\n", + " low_cost_partial_config={'max_depth': 3},\n", + " time_budget_s=3600\n", + ")\n", + "```\n", + "\n", + "Please note that if you are referring to a different kind of function or use case, you might need to specify more details or check the official documentation or source code of the FLAML library.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "UPDATE CONTEXT\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32mAdding content of doc 0ecd7192-3761-7d6f-9151-5ff504ca740b to context.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32mAdding content of doc ddbaaafc-abdd-30b4-eecd-ec2c32818952 to context.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: Is there a function called tune_automl?\n", + "\n", + "Context is: # Research\n", + "\n", + "For technical details, please check our research publications.\n", + "\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021flaml,\n", + " title={FLAML: A Fast and Lightweight AutoML Library},\n", + " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", + " year={2021},\n", + " booktitle={MLSys},\n", + "}\n", + "```\n", + "\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021cfo,\n", + " title={Frugal Optimization for Cost-related Hyperparameters},\n", + " author={Qingyun Wu and Chi Wang and Silu Huang},\n", + " year={2021},\n", + " booktitle={AAAI},\n", + "}\n", + "```\n", + "\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021blendsearch,\n", + " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", + " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", + " year={2021},\n", + " booktitle={ICLR},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{liuwang2021hpolm,\n", + " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", + " author={Susan Xueqing Liu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ACL},\n", + "}\n", + "```\n", + "\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021chacha,\n", + " title={ChaCha for Online AutoML},\n", + " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", + " year={2021},\n", + " booktitle={ICML},\n", + "}\n", + "```\n", + "\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "\n", + "```bibtex\n", + "@inproceedings{wuwang2021fairautoml,\n", + " title={Fair AutoML},\n", + " author={Qingyun Wu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ArXiv preprint arXiv:2111.06495},\n", + "}\n", + "```\n", + "\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "\n", + "```bibtex\n", + "@inproceedings{kayaliwang2022default,\n", + " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", + " author={Moe Kayali and Chi Wang},\n", + " year={2022},\n", + " booktitle={ArXiv preprint arXiv:2202.09927},\n", + "}\n", + "```\n", + "\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "\n", + "```bibtex\n", + "@inproceedings{zhang2023targeted,\n", + " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", + " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", + " booktitle={International Conference on Learning Representations},\n", + " year={2023},\n", + " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", + "}\n", + "```\n", + "\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2023EcoOptiGen,\n", + " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", + " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2303.04673},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2023empirical,\n", + " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", + " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2306.01337},\n", + "}\n", + "```\n", + "If you are new to GitHub [here](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) is a detailed help source on getting involved with development on GitHub.\n", + "\n", + "When you submit a pull request, a CLA bot will automatically determine whether you need to provide\n", + "a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\n", + "provided by the bot. You will only need to do this once across all repos using our CLA.\n", + "\n", + "This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\n", + "For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\n", + "contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n", + "\n", + "\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: Is there a function called tune_automl?\n", + "\n", + "Context is: # Research\n", + "\n", + "For technical details, please check our research publications.\n", + "\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021flaml,\n", + " title={FLAML: A Fast and Lightweight AutoML Library},\n", + " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", + " year={2021},\n", + " booktitle={MLSys},\n", + "}\n", + "```\n", + "\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021cfo,\n", + " title={Frugal Optimization for Cost-related Hyperparameters},\n", + " author={Qingyun Wu and Chi Wang and Silu Huang},\n", + " year={2021},\n", + " booktitle={AAAI},\n", + "}\n", + "```\n", + "\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021blendsearch,\n", + " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", + " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", + " year={2021},\n", + " booktitle={ICLR},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{liuwang2021hpolm,\n", + " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", + " author={Susan Xueqing Liu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ACL},\n", + "}\n", + "```\n", + "\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021chacha,\n", + " title={ChaCha for Online AutoML},\n", + " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", + " year={2021},\n", + " booktitle={ICML},\n", + "}\n", + "```\n", + "\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "\n", + "```bibtex\n", + "@inproceedings{wuwang2021fairautoml,\n", + " title={Fair AutoML},\n", + " author={Qingyun Wu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ArXiv preprint arXiv:2111.06495},\n", + "}\n", + "```\n", + "\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "\n", + "```bibtex\n", + "@inproceedings{kayaliwang2022default,\n", + " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", + " author={Moe Kayali and Chi Wang},\n", + " year={2022},\n", + " booktitle={ArXiv preprint arXiv:2202.09927},\n", + "}\n", + "```\n", + "\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "\n", + "```bibtex\n", + "@inproceedings{zhang2023targeted,\n", + " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", + " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", + " booktitle={International Conference on Learning Representations},\n", + " year={2023},\n", + " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", + "}\n", + "```\n", + "\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2023EcoOptiGen,\n", + " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", + " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2303.04673},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2023empirical,\n", + " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", + " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2306.01337},\n", + "}\n", + "```\n", + "If you are new to GitHub [here](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) is a detailed help source on getting involved with development on GitHub.\n", + "\n", + "When you submit a pull request, a CLA bot will automatically determine whether you need to provide\n", + "a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\n", + "provided by the bot. You will only need to do this once across all repos using our CLA.\n", + "\n", + "This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\n", + "For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\n", + "contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "UPDATE CONTEXT\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", + "VectorDB returns doc_ids: [['987f060a-4399-b91a-0e51-51b6165ea5bb', '0ecd7192-3761-7d6f-9151-5ff504ca740b', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "VectorDB returns doc_ids: [['987f060a-4399-b91a-0e51-51b6165ea5bb', '0ecd7192-3761-7d6f-9151-5ff504ca740b', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "VectorDB returns doc_ids: [['987f060a-4399-b91a-0e51-51b6165ea5bb', '0ecd7192-3761-7d6f-9151-5ff504ca740b', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "VectorDB returns doc_ids: [['987f060a-4399-b91a-0e51-51b6165ea5bb', '0ecd7192-3761-7d6f-9151-5ff504ca740b', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "\u001b[32mNo more context, will terminate.\u001b[0m\n", + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "# reset the assistant. Always reset the assistant before starting a new conversation.\n", + "assistant.reset()\n", + "\n", + "qa_problem = \"Is there a function called tune_automl?\"\n", + "chat_results = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Example 2\n", + "\n", + "[back to top](#toc)\n", + "\n", + "Use RetrieveChat to answer a question that is not related to code generation.\n", + "\n", + "Problem: Who is the author of FLAML?" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "VectorDB returns doc_ids: [['0ecd7192-3761-7d6f-9151-5ff504ca740b', '987f060a-4399-b91a-0e51-51b6165ea5bb', 'ddbaaafc-abdd-30b4-eecd-ec2c32818952']]\n", + "\u001b[32mAdding content of doc 0ecd7192-3761-7d6f-9151-5ff504ca740b to context.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", + "\n", + "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", + "context provided by the user.\n", + "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", + "For code generation, you must obey the following rules:\n", + "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", + "Rule 2. You must follow the formats below to write your code:\n", + "```language\n", + "# your code\n", + "```\n", + "\n", + "User's question is: Who is the author of FLAML?\n", + "\n", + "Context is: # Research\n", + "\n", + "For technical details, please check our research publications.\n", + "\n", + "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021flaml,\n", + " title={FLAML: A Fast and Lightweight AutoML Library},\n", + " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", + " year={2021},\n", + " booktitle={MLSys},\n", + "}\n", + "```\n", + "\n", + "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021cfo,\n", + " title={Frugal Optimization for Cost-related Hyperparameters},\n", + " author={Qingyun Wu and Chi Wang and Silu Huang},\n", + " year={2021},\n", + " booktitle={AAAI},\n", + "}\n", + "```\n", + "\n", + "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2021blendsearch,\n", + " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", + " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", + " year={2021},\n", + " booktitle={ICLR},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{liuwang2021hpolm,\n", + " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", + " author={Susan Xueqing Liu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ACL},\n", + "}\n", + "```\n", + "\n", + "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2021chacha,\n", + " title={ChaCha for Online AutoML},\n", + " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", + " year={2021},\n", + " booktitle={ICML},\n", + "}\n", + "```\n", + "\n", + "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", + "\n", + "```bibtex\n", + "@inproceedings{wuwang2021fairautoml,\n", + " title={Fair AutoML},\n", + " author={Qingyun Wu and Chi Wang},\n", + " year={2021},\n", + " booktitle={ArXiv preprint arXiv:2111.06495},\n", + "}\n", + "```\n", + "\n", + "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", + "\n", + "```bibtex\n", + "@inproceedings{kayaliwang2022default,\n", + " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", + " author={Moe Kayali and Chi Wang},\n", + " year={2022},\n", + " booktitle={ArXiv preprint arXiv:2202.09927},\n", + "}\n", + "```\n", + "\n", + "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", + "\n", + "```bibtex\n", + "@inproceedings{zhang2023targeted,\n", + " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", + " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", + " booktitle={International Conference on Learning Representations},\n", + " year={2023},\n", + " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", + "}\n", + "```\n", + "\n", + "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wang2023EcoOptiGen,\n", + " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", + " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2303.04673},\n", + "}\n", + "```\n", + "\n", + "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", + "\n", + "```bibtex\n", + "@inproceedings{wu2023empirical,\n", + " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", + " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", + " year={2023},\n", + " booktitle={ArXiv preprint arXiv:2306.01337},\n", + "}\n", + "```\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", + "\n", + "The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "# reset the assistant. Always reset the assistant before starting a new conversation.\n", + "assistant.reset()\n", + "\n", + "qa_problem = \"Who is the author of FLAML?\"\n", + "chat_results = ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" + ] + } + ], + "metadata": { + "front_matter": { + "description": "This notebook demonstrates the usage of QdrantRetrieveUserProxyAgent for RAG.", + "tags": [ + "rag" + ] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebook/agentchat_capability_long_context_handling.ipynb b/notebook/agentchat_capability_long_context_handling.ipynb deleted file mode 100644 index 0a9d715e3e5c..000000000000 --- a/notebook/agentchat_capability_long_context_handling.ipynb +++ /dev/null @@ -1,687 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Handling A Long Context via `TransformChatHistory`\n", - "\n", - "
\n", - " Deprecation Notice: TransformChatHistory is no longer supported and will be removed in version 0.2.30. Please transition to using TransformMessages as the new standard method. For a detailed introduction to this method, including how to limit the number of tokens in message context history to replace TransformChatHistory, visit our guide Introduction to Transform Messages.\n", - "
\n", - "\n", - "This notebook illustrates how you can use the `TransformChatHistory` capability to give any `Conversable` agent an ability to handle a long context. \n", - "\n", - "````{=mdx}\n", - ":::info Requirements\n", - "Install `pyautogen`:\n", - "```bash\n", - "pip install pyautogen\n", - "```\n", - "\n", - "For more information, please refer to the [installation guide](/docs/installation/).\n", - ":::\n", - "````" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "import autogen\n", - "from autogen.agentchat.contrib.capabilities import context_handling" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "llm_config = {\n", - " \"config_list\": [{\"model\": \"gpt-3.5-turbo\", \"api_key\": os.environ.get(\"OPENAI_API_KEY\")}],\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "````{=mdx}\n", - ":::tip\n", - "Learn more about configuring LLMs for agents [here](/docs/topics/llm_configuration).\n", - ":::\n", - "````\n", - "\n", - "To add this ability to any agent, define the capability and then use `add_to_agent`." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "plot and save a graph of x^2 from -10 to 10\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "To plot and save a graph of the function x^2 from -10 to 10, you can use the matplotlib library in Python. Here is the code:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Calculate corresponding y values (x^2)\n", - "y = x**2\n", - "\n", - "# Create the plot\n", - "plt.plot(x, y)\n", - "\n", - "# Add labels and title\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Plot of x^2')\n", - "\n", - "# Save the plot as a file\n", - "plt.savefig('x_squared_plot.png')\n", - "\n", - "# Show the plot\n", - "plt.show()\n", - "```\n", - "\n", - "This code will create a plot of the function x^2 and save it as \"x_squared_plot.png\" in the current directory. Make sure you have the matplotlib library installed before running this code.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Figure(640x480)\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 0 messages. Reduced from 3 to 3.\u001b[0m\n", - "\u001b[33mTruncated 139 tokens. Tokens reduced from 223 to 84\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480. \n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Generate y values by squaring x\n", - "y = x ** 2\n", - "\n", - "# Plot the graph\n", - "plt.plot(x, y)\n", - "plt.xlabel('x')\n", - "plt.ylabel('x^2')\n", - "plt.title('Graph of x^2')\n", - "plt.grid(True)\n", - "\n", - "# Save the graph as an image file, for example as 'graph.png'\n", - "plt.savefig('graph.png')\n", - "```\n", - "\n", - "By executing this updated code, the graph will be saved as an image file named 'graph.png' in the same directory as your Python script.\n", - "\n", - "Please let me know if you need any further assistance.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 0 messages. Reduced from 5 to 5.\u001b[0m\n", - "\u001b[33mTruncated 159 tokens. Tokens reduced from 306 to 147\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Calculate y values (x^2)\n", - "y = x**2\n", - "\n", - "# Plot the graph\n", - "plt.plot(x, y)\n", - "\n", - "# Add labels and title\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Graph of x^2')\n", - "\n", - "# Save the graph as an image file\n", - "plt.savefig('graph.png')\n", - "\n", - "# Close the plot\n", - "plt.close()\n", - "```\n", - "\n", - "This code will save the plot as an image file named \"graph.png\" in the current directory. You can change the filename and path if needed.\n", - "\n", - "Please let me know if you need any further assistance.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 0 messages. Reduced from 7 to 7.\u001b[0m\n", - "\u001b[33mTruncated 159 tokens. Tokens reduced from 369 to 210\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Generate y values\n", - "y = x**2\n", - "\n", - "# Plot the graph\n", - "plt.plot(x, y)\n", - "\n", - "# Set labels and title\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Graph of x^2')\n", - "\n", - "# Save the graph as an image file\n", - "plt.savefig('graph.png')\n", - "```\n", - "\n", - "This code will save the graph as a PNG image file named 'graph.png' in the same directory where you are running the script. You can change the filename and format (e.g., 'graph.jpg') if desired.\n", - "\n", - "Please note that you will need to have the matplotlib library installed to run this code. If you don't have it installed, you can install it by running `pip install matplotlib` in your terminal or command prompt.\n", - "\n", - "Let me know if you need any further assistance!\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 0 messages. Reduced from 9 to 9.\u001b[0m\n", - "\u001b[33mTruncated 198 tokens. Tokens reduced from 471 to 273\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Generate y values by squaring x\n", - "y = x**2\n", - "\n", - "# Plot the graph\n", - "plt.plot(x, y)\n", - "\n", - "# Add labels and title\n", - "plt.xlabel('x')\n", - "plt.ylabel('x^2')\n", - "plt.title('Graph of x^2')\n", - "\n", - "# Save the graph as an image file\n", - "plt.savefig('x_squared_plot.png')\n", - "\n", - "# Display the graph\n", - "plt.show()\n", - "```\n", - "\n", - "This code will save the graph as a PNG image file named \"x_squared_plot.png\" in the current working directory. You can customize the filename and file format according to your needs.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Figure(640x480)\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 1 messages. Reduced from 11 to 10.\u001b[0m\n", - "\u001b[33mTruncated 174 tokens. Tokens reduced from 501 to 327\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Generate y values\n", - "y = x ** 2\n", - "\n", - "# Create the plot\n", - "plt.plot(x, y)\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Graph of x^2')\n", - "\n", - "# Save the plot as an image file\n", - "plt.savefig('x_squared.png')\n", - "\n", - "plt.show()\n", - "```\n", - "\n", - "The code will save the plot as a PNG image file named \"x_squared.png\" in the current directory. You can change the filename or file extension as needed.\n", - "\n", - "Please let me know if you need any further assistance!\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Figure(640x480)\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 3 messages. Reduced from 13 to 10.\u001b[0m\n", - "\u001b[33mTruncated 227 tokens. Tokens reduced from 554 to 327\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "To save the graph as an image file, you can modify the code as follows:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# Create the plot\n", - "plt.plot(x, y)\n", - "\n", - "# Set the plot title and axes labels\n", - "plt.title(\"Plot of x^2\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "\n", - "# Save the plot as an image file\n", - "plt.savefig(\"plot.png\")\n", - "```\n", - "\n", - "This code will save the plot as a PNG file with the name \"plot.png\" in the same directory as your Python script. You can change the file name and format (e.g., \"plot.jpg\") if desired.\n", - "\n", - "Once you run this modified code, the plot will be saved as an image file. You can then use this file for further analysis or to share the plot with others.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 1 (execution failed)\n", - "Code output: \n", - "Traceback (most recent call last):\n", - " File \"\", line 4, in \n", - " plt.plot(x, y)\n", - "NameError: name 'x' is not defined\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 5 messages. Reduced from 15 to 10.\u001b[0m\n", - "\u001b[33mTruncated 258 tokens. Tokens reduced from 622 to 364\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "It seems that the code encountered an error because the variables `x` and `y` were not defined.\n", - "\n", - "Let's modify the code to define the variables `x` and `y` and then save the graph as an image file:\n", - "\n", - "```python\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Define the x values as an array from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Define the y values as the square of x\n", - "y = x ** 2\n", - "\n", - "# Create the plot\n", - "plt.plot(x, y)\n", - "\n", - "# Set the plot title and axes labels\n", - "plt.title(\"Plot of x^2\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "\n", - "# Save the plot as an image file\n", - "plt.savefig('plot.png')\n", - "```\n", - "This code uses the `numpy` library to generate an array of x values from -10 to 10 and calculates the corresponding y values as the square of x. The plot is then saved as an image file named `plot.png`.\n", - "\n", - "Please make sure you have the `numpy` and `matplotlib` libraries installed before running this code. Let me know if you need help with that.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 7 messages. Reduced from 17 to 10.\u001b[0m\n", - "\u001b[33mTruncated 401 tokens. Tokens reduced from 765 to 364\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The code executed successfully. The graph was saved as an image file with the name \"plot.png\" in the current working directory.\n", - "\n", - "Now you can share the \"plot.png\" file or view it using an image viewer.\n", - "\n", - "Let me know if there's anything else I can help with!\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 9 messages. Reduced from 19 to 10.\u001b[0m\n", - "\u001b[33mTruncated 282 tokens. Tokens reduced from 633 to 351\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Is there anything else I can help you with?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 11 messages. Reduced from 21 to 10.\u001b[0m\n", - "\u001b[33mTruncated 342 tokens. Tokens reduced from 634 to 292\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "If you need any further assistance, feel free to ask. I'm here to help!\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_history=[{'content': 'plot and save a graph of x^2 from -10 to 10', 'role': 'assistant'}, {'content': 'To plot and save a graph of the function x^2 from -10 to 10, you can use the matplotlib library in Python. Here is the code:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values from -10 to 10\\nx = np.linspace(-10, 10, 100)\\n\\n# Calculate corresponding y values (x^2)\\ny = x**2\\n\\n# Create the plot\\nplt.plot(x, y)\\n\\n# Add labels and title\\nplt.xlabel(\\'x\\')\\nplt.ylabel(\\'y\\')\\nplt.title(\\'Plot of x^2\\')\\n\\n# Save the plot as a file\\nplt.savefig(\\'x_squared_plot.png\\')\\n\\n# Show the plot\\nplt.show()\\n```\\n\\nThis code will create a plot of the function x^2 and save it as \"x_squared_plot.png\" in the current directory. Make sure you have the matplotlib library installed before running this code.', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\nFigure(640x480)\\n', 'role': 'assistant'}, {'content': \"Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480. \\n\\nTo save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values from -10 to 10\\nx = np.linspace(-10, 10, 100)\\n\\n# Generate y values by squaring x\\ny = x ** 2\\n\\n# Plot the graph\\nplt.plot(x, y)\\nplt.xlabel('x')\\nplt.ylabel('x^2')\\nplt.title('Graph of x^2')\\nplt.grid(True)\\n\\n# Save the graph as an image file, for example as 'graph.png'\\nplt.savefig('graph.png')\\n```\\n\\nBy executing this updated code, the graph will be saved as an image file named 'graph.png' in the same directory as your Python script.\\n\\nPlease let me know if you need any further assistance.\", 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\n', 'role': 'assistant'}, {'content': 'Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\\n\\nTo save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values from -10 to 10\\nx = np.linspace(-10, 10, 100)\\n\\n# Calculate y values (x^2)\\ny = x**2\\n\\n# Plot the graph\\nplt.plot(x, y)\\n\\n# Add labels and title\\nplt.xlabel(\\'x\\')\\nplt.ylabel(\\'y\\')\\nplt.title(\\'Graph of x^2\\')\\n\\n# Save the graph as an image file\\nplt.savefig(\\'graph.png\\')\\n\\n# Close the plot\\nplt.close()\\n```\\n\\nThis code will save the plot as an image file named \"graph.png\" in the current directory. You can change the filename and path if needed.\\n\\nPlease let me know if you need any further assistance.', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\n', 'role': 'assistant'}, {'content': \"Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\\n\\nTo save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values\\nx = np.linspace(-10, 10, 100)\\n\\n# Generate y values\\ny = x**2\\n\\n# Plot the graph\\nplt.plot(x, y)\\n\\n# Set labels and title\\nplt.xlabel('x')\\nplt.ylabel('y')\\nplt.title('Graph of x^2')\\n\\n# Save the graph as an image file\\nplt.savefig('graph.png')\\n```\\n\\nThis code will save the graph as a PNG image file named 'graph.png' in the same directory where you are running the script. You can change the filename and format (e.g., 'graph.jpg') if desired.\\n\\nPlease note that you will need to have the matplotlib library installed to run this code. If you don't have it installed, you can install it by running `pip install matplotlib` in your terminal or command prompt.\\n\\nLet me know if you need any further assistance!\", 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\n', 'role': 'assistant'}, {'content': 'Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\\n\\nTo save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values from -10 to 10\\nx = np.linspace(-10, 10, 100)\\n\\n# Generate y values by squaring x\\ny = x**2\\n\\n# Plot the graph\\nplt.plot(x, y)\\n\\n# Add labels and title\\nplt.xlabel(\\'x\\')\\nplt.ylabel(\\'x^2\\')\\nplt.title(\\'Graph of x^2\\')\\n\\n# Save the graph as an image file\\nplt.savefig(\\'x_squared_plot.png\\')\\n\\n# Display the graph\\nplt.show()\\n```\\n\\nThis code will save the graph as a PNG image file named \"x_squared_plot.png\" in the current working directory. You can customize the filename and file format according to your needs.', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\nFigure(640x480)\\n', 'role': 'assistant'}, {'content': 'Great! The code executed successfully and generated a plot of the function x^2 from -10 to 10. The plot was displayed in a figure with size 640x480.\\n\\nTo save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Generate x values\\nx = np.linspace(-10, 10, 100)\\n\\n# Generate y values\\ny = x ** 2\\n\\n# Create the plot\\nplt.plot(x, y)\\nplt.xlabel(\\'x\\')\\nplt.ylabel(\\'y\\')\\nplt.title(\\'Graph of x^2\\')\\n\\n# Save the plot as an image file\\nplt.savefig(\\'x_squared.png\\')\\n\\nplt.show()\\n```\\n\\nThe code will save the plot as a PNG image file named \"x_squared.png\" in the current directory. You can change the filename or file extension as needed.\\n\\nPlease let me know if you need any further assistance!', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\nFigure(640x480)\\n', 'role': 'assistant'}, {'content': 'To save the graph as an image file, you can modify the code as follows:\\n\\n```python\\nimport matplotlib.pyplot as plt\\n\\n# Create the plot\\nplt.plot(x, y)\\n\\n# Set the plot title and axes labels\\nplt.title(\"Plot of x^2\")\\nplt.xlabel(\"x\")\\nplt.ylabel(\"y\")\\n\\n# Save the plot as an image file\\nplt.savefig(\"plot.png\")\\n```\\n\\nThis code will save the plot as a PNG file with the name \"plot.png\" in the same directory as your Python script. You can change the file name and format (e.g., \"plot.jpg\") if desired.\\n\\nOnce you run this modified code, the plot will be saved as an image file. You can then use this file for further analysis or to share the plot with others.', 'role': 'user'}, {'content': 'exitcode: 1 (execution failed)\\nCode output: \\nTraceback (most recent call last):\\n File \"\", line 4, in \\n plt.plot(x, y)\\nNameError: name \\'x\\' is not defined\\n', 'role': 'assistant'}, {'content': 'It seems that the code encountered an error because the variables `x` and `y` were not defined.\\n\\nLet\\'s modify the code to define the variables `x` and `y` and then save the graph as an image file:\\n\\n```python\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\n\\n# Define the x values as an array from -10 to 10\\nx = np.linspace(-10, 10, 100)\\n\\n# Define the y values as the square of x\\ny = x ** 2\\n\\n# Create the plot\\nplt.plot(x, y)\\n\\n# Set the plot title and axes labels\\nplt.title(\"Plot of x^2\")\\nplt.xlabel(\"x\")\\nplt.ylabel(\"y\")\\n\\n# Save the plot as an image file\\nplt.savefig(\\'plot.png\\')\\n```\\nThis code uses the `numpy` library to generate an array of x values from -10 to 10 and calculates the corresponding y values as the square of x. The plot is then saved as an image file named `plot.png`.\\n\\nPlease make sure you have the `numpy` and `matplotlib` libraries installed before running this code. Let me know if you need help with that.', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\n', 'role': 'assistant'}, {'content': 'Great! The code executed successfully. The graph was saved as an image file with the name \"plot.png\" in the current working directory.\\n\\nNow you can share the \"plot.png\" file or view it using an image viewer.\\n\\nLet me know if there\\'s anything else I can help with!', 'role': 'user'}, {'content': '', 'role': 'assistant'}, {'content': 'Is there anything else I can help you with?', 'role': 'user'}, {'content': '', 'role': 'assistant'}, {'content': \"If you need any further assistance, feel free to ask. I'm here to help!\", 'role': 'user'}], summary=\"If you need any further assistance, feel free to ask. I'm here to help!\", cost=({'total_cost': 0.015855, 'gpt-3.5-turbo-0613': {'cost': 0.015855, 'prompt_tokens': 8242, 'completion_tokens': 1746, 'total_tokens': 9988}}, {'total_cost': 0.0147465, 'gpt-3.5-turbo-0613': {'cost': 0.0147465, 'prompt_tokens': 7755, 'completion_tokens': 1557, 'total_tokens': 9312}}), human_input=[])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assistant = autogen.AssistantAgent(\n", - " \"assistant\",\n", - " llm_config=llm_config,\n", - ")\n", - "\n", - "\n", - "# Instantiate the capability to manage chat history\n", - "manage_chat_history = context_handling.TransformChatHistory(max_tokens_per_message=50, max_messages=10, max_tokens=1000)\n", - "# Add the capability to the assistant\n", - "manage_chat_history.add_to_agent(assistant)\n", - "\n", - "user_proxy = autogen.UserProxyAgent(\n", - " \"user_proxy\",\n", - " human_input_mode=\"NEVER\",\n", - " is_termination_msg=lambda x: \"TERMINATE\" in x.get(\"content\", \"\"),\n", - " code_execution_config={\n", - " \"work_dir\": \"coding\",\n", - " \"use_docker\": False,\n", - " },\n", - " max_consecutive_auto_reply=10,\n", - ")\n", - "\n", - "user_proxy.initiate_chat(assistant, message=\"plot and save a graph of x^2 from -10 to 10\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why is this important?\n", - "This capability is especially useful if you expect the agent histories to become exceptionally large and exceed the context length offered by your LLM.\n", - "For example, in the example below, we will define two agents -- one without this ability and one with this ability.\n", - "\n", - "The agent with this ability will be able to handle longer chat history without crashing." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "plot and save a graph of x^2 from -10 to 10\n", - "\n", - "--------------------------------------------------------------------------------\n", - "Encountered an error with the base assistant\n", - "Error code: 400 - {'error': {'message': \"This model's maximum context length is 4097 tokens. However, your messages resulted in 1009487 tokens. Please reduce the length of the messages.\", 'type': 'invalid_request_error', 'param': 'messages', 'code': 'context_length_exceeded'}}\n", - "\n", - "\n", - "\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "plot and save a graph of x^2 from -10 to 10\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 1991 messages. Reduced from 2001 to 10.\u001b[0m\n", - "\u001b[33mTruncated 1000800 tokens. Tokens reduced from 1001015 to 215\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Here's the Python code to plot and save a graph of x^2 from -10 to 10:\n", - "\n", - "```python\n", - "# filename: plot_graph.py\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "# Generate x values from -10 to 10\n", - "x = np.linspace(-10, 10, 100)\n", - "\n", - "# Calculate y values as x^2\n", - "y = x**2\n", - "\n", - "# Create plot\n", - "plt.plot(x, y)\n", - "\n", - "# Add labels and title\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Graph of y = x^2')\n", - "\n", - "# Save the plot as a PNG image\n", - "plt.savefig('graph.png')\n", - "\n", - "# Show the plot\n", - "plt.show()\n", - "```\n", - "\n", - "To execute this code, save it to a file called `plot_graph.py` and run it using Python. This will generate a file called `graph.png` in the same directory, which will contain the graph of x^2 from -10 to 10.\n", - "\n", - "Note: Make sure you have the matplotlib library installed. You can install it by running `pip install matplotlib` in your terminal or command prompt.\n", - "\n", - "Let me know if you need any further assistance!\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Figure(640x480)\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 1993 messages. Reduced from 2003 to 10.\u001b[0m\n", - "\u001b[33mTruncated 997232 tokens. Tokens reduced from 997466 to 234\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "Great! The graph of x^2 from -10 to 10 has been plotted and saved successfully. You can find the saved graph as an image file on your computer. \n", - "\n", - "Is there anything else I can help you with?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mTruncated 1995 messages. Reduced from 2005 to 10.\u001b[0m\n", - "\u001b[33mTruncated 997096 tokens. Tokens reduced from 997326 to 230\u001b[0m\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - } - ], - "source": [ - "assistant_base = autogen.AssistantAgent(\n", - " \"assistant\",\n", - " llm_config=llm_config,\n", - ")\n", - "\n", - "assistant_with_context_handling = autogen.AssistantAgent(\n", - " \"assistant\",\n", - " llm_config=llm_config,\n", - ")\n", - "# suppose this capability is not available\n", - "manage_chat_history = context_handling.TransformChatHistory(max_tokens_per_message=50, max_messages=10, max_tokens=1000)\n", - "manage_chat_history.add_to_agent(assistant_with_context_handling)\n", - "\n", - "user_proxy = autogen.UserProxyAgent(\n", - " \"user_proxy\",\n", - " human_input_mode=\"NEVER\",\n", - " is_termination_msg=lambda x: \"TERMINATE\" in x.get(\"content\", \"\"),\n", - " code_execution_config={\n", - " \"work_dir\": \"coding\",\n", - " \"use_docker\": False,\n", - " },\n", - " max_consecutive_auto_reply=2,\n", - ")\n", - "\n", - "# suppose the chat history is large\n", - "# Create a very long chat history that is bound to cause a crash\n", - "# for gpt 3.5\n", - "long_history = []\n", - "for i in range(1000):\n", - " # define a fake, very long message\n", - " assitant_msg = {\"role\": \"assistant\", \"content\": \"test \" * 1000}\n", - " user_msg = {\"role\": \"user\", \"content\": \"\"}\n", - "\n", - " assistant_base.send(assitant_msg, user_proxy, request_reply=False, silent=True)\n", - " assistant_with_context_handling.send(assitant_msg, user_proxy, request_reply=False, silent=True)\n", - " user_proxy.send(user_msg, assistant_base, request_reply=False, silent=True)\n", - " user_proxy.send(user_msg, assistant_with_context_handling, request_reply=False, silent=True)\n", - "\n", - "try:\n", - " user_proxy.initiate_chat(assistant_base, message=\"plot and save a graph of x^2 from -10 to 10\", clear_history=False)\n", - "except Exception as e:\n", - " print(\"Encountered an error with the base assistant\")\n", - " print(e)\n", - " print(\"\\n\\n\")\n", - "\n", - "try:\n", - " user_proxy.initiate_chat(\n", - " assistant_with_context_handling, message=\"plot and save a graph of x^2 from -10 to 10\", clear_history=False\n", - " )\n", - "except Exception as e:\n", - " print(e)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebook/agentchat_compression.ipynb b/notebook/agentchat_compression.ipynb deleted file mode 100644 index d7f3a7204db8..000000000000 --- a/notebook/agentchat_compression.ipynb +++ /dev/null @@ -1,876 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Conversations with Chat History Compression Enabled\n", - "\n", - "
\n", - " Deprecation Notice: CompressibleAgent has been deprecated and will no longer be available as of version 0.2.30. Please transition to using TransformMessages, which is now the recommended approach. For a detailed guide on implementing this new standard, refer to our user guide on Compressing Text with LLMLingua. This guide provides examples for effectively utilizing LLMLingua transform as a replacement for CompressibleAgent.\n", - "
\n", - "\n", - "AutoGen offers conversable agents powered by LLM, tools, or humans, which can be used to perform tasks collectively via automated chat. This framework allows tool use and human participance through multi-agent conversation. Please find documentation about this feature [here](https://microsoft.github.io/autogen/docs/Use-Cases/agent_chat).\n", - "\n", - "In this notebook, we demonstrate how to enable compression of history messages using the `CompressibleAgent`. While this agent retains all the default functionalities of the `AssistantAgent`, it also provides the added feature of compression when activated through the `compress_config` setting.\n", - "\n", - "Different compression modes are supported:\n", - "\n", - "1. `compress_config=False` (Default): `CompressibleAgent` is equivalent to `AssistantAgent`.\n", - "2. `compress_config=True` or `compress_config={\"mode\": \"TERMINATE\"}`: no compression will be performed. However, we will count token usage before sending requests to the OpenAI model. The conversation will be terminated directly if the total token usage exceeds the maximum token usage allowed by the model (to avoid the token limit error from OpenAI API).\n", - "3. `compress_config={\"mode\": \"COMPRESS\", \"trigger_count\": , \"leave_last_n\": }`: compression is enabled.\n", - "\n", - " ```python\n", - " # default compress_config\n", - " compress_config = {\n", - " \"mode\": \"COMPRESS\",\n", - " \"compress_function\": None,\n", - " \"trigger_count\": 0.7, # default to 0.7, or your pre-set number\n", - " \"broadcast\": True, # the compressed with be broadcast to sender. This will not be used in groupchat.\n", - "\n", - " # the following settings are for this mode only\n", - " \"leave_last_n\": 2, # leave the last n messages in the history to avoid compression\n", - " \"verbose\": False, # if True, print out the content to be compressed and the compressed content\n", - " }\n", - " ```\n", - "\n", - " Currently, our compression logic is as follows:\n", - " 1. We will always leave the first user message (as well as system prompts) and compress the rest of the history messages.\n", - " 2. You can choose to not compress the last n messages in the history with \"leave_last_n\".\n", - " 2. The summary is performed on a per-message basis, with the role of the messages (See compressed content in the example below).\n", - "\n", - "4. `compress_config={\"mode\": \"CUSTOMIZED\", \"compress_function\": }t`: the `compress_function` function will be called on trigger count. The function should accept a list of messages as input and return a tuple of (is_success: bool, compressed_messages: List[Dict]). The whole message history (except system prompt) will be passed.\n", - "\n", - "\n", - "By adjusting `trigger_count`, you can decide when to compress the history messages based on existing tokens. If this is a float number between 0 and 1, it is interpreted as a ratio of max tokens allowed by the model. For example, the AssistantAgent uses gpt-4 with max tokens 8192, the trigger_count = 0.7 * 8192 = 5734.4 -> 5734. Do not set `trigger_count` to the max tokens allowed by the model, since the same LLM is employed for compression and it needs tokens to generate the compressed content. \n", - "\n", - "\n", - "\n", - "## Limitations\n", - "- For now, the compression feature **is not well-supported for groupchat**. If you initialize a `CompressibleAgent` in a groupchat with compression, the compressed cannot be broadcast to all other agents in the groupchat. If you use this feature in groupchat, extra cost will be incurred since compression will be performed on at per-agent basis.\n", - "- We do not support async compression for now.\n", - "\n", - "## Requirements\n", - "\n", - "````{=mdx}\n", - ":::info Requirements\n", - "Install `pyautogen`:\n", - "```bash\n", - "pip install pyautogen\n", - "```\n", - "\n", - "For more information, please refer to the [installation guide](/docs/installation/).\n", - ":::\n", - "````" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Set your API Endpoint\n", - "\n", - "The [`config_list_from_json`](https://microsoft.github.io/autogen/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# define functions according to the function description\n", - "from IPython import get_ipython\n", - "\n", - "import autogen\n", - "from autogen.agentchat.contrib.compressible_agent import CompressibleAgent\n", - "from autogen.agentchat.contrib.math_user_proxy_agent import MathUserProxyAgent\n", - "\n", - "config_list = autogen.config_list_from_json(\n", - " \"OAI_CONFIG_LIST\",\n", - " filter_dict={\n", - " \"model\": [\"gpt-4-1106-preview\"],\n", - " },\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "````{=mdx}\n", - ":::tip\n", - "Learn more about configuring LLMs for agents [here](/docs/topics/llm_configuration).\n", - ":::\n", - "````" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example 1\n", - "This example is from [agentchat_MathChat.ipynb](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_MathChat.ipynb). Compression with code execution.\n", - "\n", - "You must set the `model` field in `llm_config`, as it will be used to calculate the token usage.\n", - "\n", - "Note: we set `trigger_count=600`, and `leave_last_n=2`. In this example, we set a low trigger_count to demonstrate the compression feature. \n", - "The token count after compression is still bigger than trigger count, mainly because the trigger count is low an the first and last 2 messages are not compressed. Thus, the compression is performed at each turn. In practice, you want to adjust the trigger_count to a bigger number and properly set the `leave_last_n` to avoid compression at each turn. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n", - "\n", - "Let's use Python to solve a math problem.\n", - "\n", - "Query requirements:\n", - "You should always use the 'print' function for the output and use fractions/radical forms instead of decimals.\n", - "You can use packages like sympy to help you.\n", - "You must follow the formats below to write your code:\n", - "```python\n", - "# your code\n", - "```\n", - "\n", - "First state the key idea to solve the problem. You may choose from three ways to solve the problem:\n", - "Case 1: If the problem can be solved with Python code directly, please write a program to solve it. You can enumerate all possible arrangements if needed.\n", - "Case 2: If the problem is mostly reasoning, you can solve it by yourself directly.\n", - "Case 3: If the problem cannot be handled in the above two ways, please follow this process:\n", - "1. Solve the problem step by step (do not over-divide the steps).\n", - "2. Take out any queries that can be asked through Python (for example, any calculations or equations that can be calculated).\n", - "3. Wait for me to give the results.\n", - "4. Continue if you think the result is correct. If the result is invalid or unexpected, please correct your query or reasoning.\n", - "\n", - "After all the queries are run and you get the answer, put the answer in \\boxed{}.\n", - "\n", - "Problem:\n", - "Find all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to mathproxyagent):\n", - "\n", - "Key Idea:\n", - "To solve this inequality $(2x + 10)(x + 3) < (3x + 9)(x + 8)$, we'll first expand both sides of the inequality, then collect all terms on one side to form a quadratic inequality. After simplifying, we will factor the quadratic expression if possible. Then, we'll determine the critical points of the inequality by setting the factors to zero. Finally, we'll use a sign chart or test values within intervals determined by the critical points to find the intervals where the original inequality is satisfied.\n", - "\n", - "Case 1 applies here, so I will write a Python program using sympy that simplifies the inequality, factors the resulting expression, solves for the critical points, and then determines the intervals where the inequality holds true.\n", - "\n", - "```python\n", - "from sympy import symbols, solve, simplify\n", - "\n", - "# Define the variable\n", - "x = symbols('x')\n", - "\n", - "# Given inequality\n", - "lhs = (2*x + 10)*(x + 3)\n", - "rhs = (3*x + 9)*(x + 8)\n", - "\n", - "# Move all terms to one side of the inequality\n", - "inequality = simplify(lhs - rhs) < 0\n", - "\n", - "# Solve the inequality\n", - "solutions = solve(inequality, x)\n", - "\n", - "# Print the solution using interval notation\n", - "print(solutions)\n", - "```\n", - "\n", - "Running this code will provide us with the solution in interval notation. We'll express the final answer in the requested format afterward.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mmathproxyagent\u001b[0m (to assistant):\n", - "\n", - "((-oo < x) & (x < -14)) | ((-3 < x) & (x < oo))\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Warning: Compression skipped at trigger count threshold. The first msg and last 2 msgs will not be compressed. current msg count: 3. Consider raising trigger_count.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33massistant\u001b[0m (to mathproxyagent):\n", - "\n", - "The solution obtained from running the Python code suggests that the values of $x$ that satisfy the inequality $(2x + 10)(x + 3) < (3x + 9)(x + 8)$ are in the intervals $(-\\infty, -14)$ and $(-3, \\infty)$.\n", - "\n", - "Therefore, the answer in interval notation is:\n", - "\n", - "$$\n", - "\\boxed{(-\\infty, -14) \\cup (-3, \\infty)}\n", - "$$\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': \"Let's use Python to solve a math problem.\\n\\nQuery requirements:\\nYou should always use the 'print' function for the output and use fractions/radical forms instead of decimals.\\nYou can use packages like sympy to help you.\\nYou must follow the formats below to write your code:\\n```python\\n# your code\\n```\\n\\nFirst state the key idea to solve the problem. You may choose from three ways to solve the problem:\\nCase 1: If the problem can be solved with Python code directly, please write a program to solve it. You can enumerate all possible arrangements if needed.\\nCase 2: If the problem is mostly reasoning, you can solve it by yourself directly.\\nCase 3: If the problem cannot be handled in the above two ways, please follow this process:\\n1. Solve the problem step by step (do not over-divide the steps).\\n2. Take out any queries that can be asked through Python (for example, any calculations or equations that can be calculated).\\n3. Wait for me to give the results.\\n4. Continue if you think the result is correct. If the result is invalid or unexpected, please correct your query or reasoning.\\n\\nAfter all the queries are run and you get the answer, put the answer in \\\\boxed{}.\\n\\nProblem:\\nFind all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.\", 'role': 'assistant'}, {'content': \"Key Idea:\\nTo solve this inequality $(2x + 10)(x + 3) < (3x + 9)(x + 8)$, we'll first expand both sides of the inequality, then collect all terms on one side to form a quadratic inequality. After simplifying, we will factor the quadratic expression if possible. Then, we'll determine the critical points of the inequality by setting the factors to zero. Finally, we'll use a sign chart or test values within intervals determined by the critical points to find the intervals where the original inequality is satisfied.\\n\\nCase 1 applies here, so I will write a Python program using sympy that simplifies the inequality, factors the resulting expression, solves for the critical points, and then determines the intervals where the inequality holds true.\\n\\n```python\\nfrom sympy import symbols, solve, simplify\\n\\n# Define the variable\\nx = symbols('x')\\n\\n# Given inequality\\nlhs = (2*x + 10)*(x + 3)\\nrhs = (3*x + 9)*(x + 8)\\n\\n# Move all terms to one side of the inequality\\ninequality = simplify(lhs - rhs) < 0\\n\\n# Solve the inequality\\nsolutions = solve(inequality, x)\\n\\n# Print the solution using interval notation\\nprint(solutions)\\n```\\n\\nRunning this code will provide us with the solution in interval notation. We'll express the final answer in the requested format afterward.\", 'role': 'user'}, {'content': '((-oo < x) & (x < -14)) | ((-3 < x) & (x < oo))', 'role': 'assistant'}, {'content': 'The solution obtained from running the Python code suggests that the values of $x$ that satisfy the inequality $(2x + 10)(x + 3) < (3x + 9)(x + 8)$ are in the intervals $(-\\\\infty, -14)$ and $(-3, \\\\infty)$.\\n\\nTherefore, the answer in interval notation is:\\n\\n$$\\n\\\\boxed{(-\\\\infty, -14) \\\\cup (-3, \\\\infty)}\\n$$', 'role': 'user'}], summary='The solution obtained from running the Python code suggests that the values of $x$ that satisfy the inequality $(2x + 10)(x + 3) < (3x + 9)(x + 8)$ are in the intervals $(-\\\\infty, -14)$ and $(-3, \\\\infty)$.\\n\\nTherefore, the answer in interval notation is:\\n\\n$$\\n\\\\boxed{(-\\\\infty, -14) \\\\cup (-3, \\\\infty)}\\n$$', cost=({'total_cost': 0.052199999999999996, 'gpt-4': {'cost': 0.052199999999999996, 'prompt_tokens': 954, 'completion_tokens': 393, 'total_tokens': 1347}}, {'total_cost': 0.052199999999999996, 'gpt-4': {'cost': 0.052199999999999996, 'prompt_tokens': 954, 'completion_tokens': 393, 'total_tokens': 1347}}), human_input=[])" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 1. replace AssistantAgent with CompressibleAgent\n", - "assistant = CompressibleAgent(\n", - " name=\"assistant\",\n", - " system_message=\"You are a helpful assistant.\",\n", - " llm_config={\n", - " \"timeout\": 600,\n", - " \"cache_seed\": 42,\n", - " \"config_list\": config_list,\n", - " \"model\": \"gpt-4-1106-preview\", # you must set the model field in llm_config, as it will be used to calculate the token usage.\n", - " },\n", - " compress_config={\n", - " \"mode\": \"COMPRESS\",\n", - " \"trigger_count\": 600, # set this to a large number for less frequent compression\n", - " \"verbose\": True, # to allow printing of compression information: context before and after compression\n", - " \"leave_last_n\": 2,\n", - " },\n", - ")\n", - "\n", - "# 2. create the MathUserProxyAgent instance named \"mathproxyagent\"\n", - "mathproxyagent = MathUserProxyAgent(\n", - " name=\"mathproxyagent\",\n", - " human_input_mode=\"NEVER\",\n", - " code_execution_config={\n", - " \"use_docker\": False\n", - " }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n", - " max_consecutive_auto_reply=5,\n", - ")\n", - "math_problem = (\n", - " \"Find all $x$ that satisfy the inequality $(2x+10)(x+3)<(3x+9)(x+8)$. Express your answer in interval notation.\"\n", - ")\n", - "mathproxyagent.initiate_chat(assistant, message=mathproxyagent.message_generator, problem=math_problem)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example 2\n", - "This example is from [agentchat_function_call.ipynb](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_function_call.ipynb). Compression with function calls. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33muser_proxy\u001b[0m (to chatbot):\n", - "\n", - "Draw two agents chatting with each other with an example dialog. Don't add plt.show().\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mchatbot\u001b[0m (to user_proxy):\n", - "\n", - "\u001b[32m***** Suggested function Call: python *****\u001b[0m\n", - "Arguments: \n", - "{\n", - " \"cell\": \"import matplotlib.pyplot as plt\\nimport numpy as np\\n\\nfig, ax = plt.subplots()\\n\\n# Define the agents as circles\\nagent1 = plt.Circle((0.4, 0.5), 0.1, color='blue')\\nagent2 = plt.Circle((0.6, 0.5), 0.1, color='red')\\n\\n# Draw the agents\\nax.add_artist(agent1)\\nax.add_artist(agent2)\\n\\n# Example dialog boxes\\nplt.text(0.28, 0.6, \\\"Hello!\\\", fontsize=12, bbox=dict(facecolor='white', alpha=0.5))\\nplt.text(0.58, 0.6, \\\"Hi there!\\\", fontsize=12, bbox=dict(facecolor='white', alpha=0.5))\\n\\n# Set the limits and remove axes\\nax.set_xlim(0, 1)\\nax.set_ylim(0, 1)\\nax.axis('off')\\n\"\n", - "}\n", - "\u001b[32m*******************************************\u001b[0m\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[35m\n", - ">>>>>>>> EXECUTING FUNCTION python...\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "(0.0, 1.0, 0.0, 1.0)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgMAAAGFCAYAAABg2vAPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAdR0lEQVR4nO3de3SU9Z3H8c8zMyEkIjcN18YGAkGE2iO4si1QoSpC16VILSq6HjheIko99ZYK1kUUQZG2dk2VrOi6eNl1o1h6WkWqBRQQXStdFQ0GMRQpYkAS0IEwycz+8esQQhBzmzxP8n2/zomcTJLJb5xn8rznufweL5FIJAQAAMwK+T0AAADgL2IAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMI4YAADAOGIAAADjiAEAAIwjBgAAMC7i9wDQdlVWVioajfo9jFaTmZmpLl26+D2Mds3aMpVqLLNoKGIATVJZWanCwkLFYjG/h9Jq0tLSNHPmTP64pojFZSrVWGbRUMQAmiQajSoWi2ny5MnKysryezgpV15ermXLlikajfKHNUWsLVOpxjKLxiAG0CxZWVnq3bu338NAO8IyBbQ+DiAEAMA4YgAAAOOIAQAAjCMG0OZMmzZNOTk5dW7zPE933nmnL+NBcOXk5GjatGlN/vlp06apU6dOLTcgIKCIAaTM448/Ls/z9NZbbx3z62PGjNHQoUNbeVT1rV69Wp7nafXq1X4PBceRquUpGo3qzjvvbHPPPwGMlsTZBADarc2bNysUOv57nmg0qrlz50pyQQFYxJYBAO1Wenq60tLS/B7GcSUSCR04cMDvYcA4YgCB8uSTT2r48OHKyMhQ9+7ddckll2j79u1Nuq+NGzdqwoQJ6ty5szp16qRzzjlHGzZsaOERI8i+7piBsrKywxMczZ07V57nHXPz+44dOzRp0iR16tRJWVlZuuWWW1RTU1Pne+LxuB544AENGTJEHTt2VM+ePZWfn6+9e/fWG9MFF1ygl156SWeeeaYyMjJUVFQkSaqoqNBPf/pTZWdnKz09XQMGDNB9992neDze/P8ZwHGwmwApV1lZqd27d9e7/ehpZ++55x7dcccdmjJliq666iqVl5frwQcf1Pe+9z1t3LhRXbt2bfDv3LRpk0aPHq3OnTuroKBAaWlpKioq0pgxY7RmzRqNGDGiuQ8LPmno8tQQWVlZevjhhzVjxgxdeOGFmjx5siTp9NNPP/w9NTU1Ov/88zVixAgtWrRIL7/8sn7xi18oNzdXM2bMOPx9+fn5evzxxzV9+nTdcMMN+vjjj1VYWKiNGzdq3bp1dbZQbN68WZdeeqny8/N19dVXa9CgQYpGozr77LO1Y8cO5efn65RTTtH69es1a9Ys7dy5Uw888ECjHx/QUMQAUu7cc8/9yq8NGTJEkrRt2zbNmTNH8+bN0+zZsw9/ffLkyTrjjDP00EMP1bn96/z85z9XLBbT2rVr1b9/f0nSFVdcoUGDBqmgoEBr1qxp4qOB3xqyPDXUCSecoIsuukgzZszQ6aefrssvv7ze9xw8eFAXX3yx7rjjDknStddeq2HDhunRRx89HANr167VkiVL9NRTT2nq1KmHf3bs2LEaP368iouL69y+ZcsWrVixQueff/7h2+bNm6ePPvpIGzdu1MCBAyW5wOjTp4/uv/9+3XzzzcrOzm7U4wMaihhAyv3mN79RXl5evdtvvvnmw5taly1bpng8rilTptR519erVy8NHDhQq1atanAM1NTUaOXKlZo0adLhEJCk3r17a+rUqXrkkUe0b98+de7cuZmPDH5oyPLU0q699to6n48ePVpPPPHE4c+Li4vVpUsXnXfeeXWW3+HDh6tTp05atWpVnRjo169fnRBI3sfo0aPVrVu3Ovdx7rnn6t5779Wrr76qyy67rKUfGiCJGEArOOuss3TmmWfWu/3IP3qlpaVKJBKH3xEdrTEHgZWXlysajWrQoEH1vjZ48GDF43Ft37690e8iEQwNWZ5aUseOHetdOKlbt251jgUoLS1VZWWlevToccz7+Oyzz+p83q9fv3rfU1paqnfeeecrL9J09H0ALYkYQCDE43F5nqcXX3xR4XC43teZ+AV+OdbyeLR4PK4ePXroqaeeOubXj17BZ2RkHPM+zjvvPBUUFBzzPo61NQRoKcQAAiE3N1eJREL9+vVr9h+9rKwsZWZmavPmzfW+VlJSolAoVGff65gxY5RIJJr1O9F2eZ7X7PvIzc3Vyy+/rJEjRx5zRd/Q+/jiiy+Oe0zEkVhm0ZI4tRCBMHnyZIXDYc2dO7feH7lEIqE9e/Y0+L7C4bDGjRun5cuXq6ys7PDtu3bt0tNPP61Ro0bVOV6gsrJSJSUlikajzX4caHsyMzMludP6mmrKlCmqqanR3XffXe9r1dXVDbrvKVOm6PXXX9dLL71U72sVFRWqrq4+/HksFlNJSUlKdovAJrYMIBByc3M1b948zZo1S2VlZZo0aZJOPPFEffzxx3r++ed1zTXX6JZbbmnw/c2bN09//OMfNWrUKF133XWKRCIqKipSVVWVFi5cWOd7n3/+eU2fPl2rVq065gx0VVXSrl1SWZn03HNSRoZ06JD7qK6W0tKkDh2k9HT3b6dO0oAB7iM9vZn/Y1Df559LJSXuCaipqf3Yt086cED63/+VwmEpEpFiMXdbVdVXPhkZGRk67bTT9MwzzygvL0/du3fX0KFDGzW18dlnn638/HwtWLBAf/nLXzRu3DilpaWptLRUxcXF+vWvf62LLrrouPdx66236ne/+50uuOACTZs2TcOHD9eXX36pd999V88++6zKysp08sknS3LzHgwePFhz5sxhSmK0CGIAgXHbbbcpLy9Pv/rVrw5PD5udna1x48Zp4sSJjbqvIUOG6LXXXtOsWbO0YMECxeNxjRgxQk8++eRXzjFQWSlt3izt2eM+ysul3bulaFTav19avVr6z/903+t5UnKW20Si9uNInif17SsNHiydeqqUl1f7kZ3t1lc4hnhcqqiofSL27HFPxIoV7uvPPiv16VP7/cnN/Hv3uifrhRdqn4wvv3RP6oIFUmamdPLJUlaWdNJJtR/dumnJkiX6yU9+ohtvvFGHDh3SnDlzGn2dg8WLF2v48OEqKirS7NmzFYlElJOTo8svv1wjR4782p/PzMzUmjVrNH/+fBUXF2vp0qXq3Lmz8vLyNHfuXHXp0qVR4wEaw0uw4wlNsHPnThUVFSk/P1+9e/f2eziNlkhIn37q1hPvv+9W/MlXgue5jyMnfdu/f6eee65I27blS2r8401Lc1sRkr8jLU0aMUKaPFn64Q+lI86AtGnrVu184gkV/eEPyh8zRr3/vun+cHW15CmDoVDdevM8qUcPV22DBkm9etUGRhvW1l+jaF1sGYAZNTXStm1uC/MHH7h3+6FQ3ZW+dOx3+c119OR4sZi0fr20bp10001uPfSjH7kwGD68XayLji+RkN56S1q+3O17KSlxt+fk1F3xJxItGwLSsZ/wXbtcEa5eLZ14Yu3mnG9+k004MIEYQLtWVSWVlrp1zYcfut3MRwaAn1O+H/m7P/jAbcmeN0/q2bN2i8H3v++2IrQLsZj0pz+5AFi2zK2Aw+H6K3+/JJ+Q/ftdqLz5pjsIJC/PhcHAgRwEgnaLGEC7FI1Kb7whbdjggiAoAXA8yXXirl3SI49IDz8sfeMb0uzZ0vTpUseO/o6vyQ4elB57TJo/X9qxwx3YlzwyPkUzBjZbciE5dMjtR3rvPRcC//iPbv9OcjcG0E5waiHalf37pZdekn75S+nVV10ISMENgK+SXFfu2CFdf7074HDRIumLL/wdV6Ps3+8GnZ0tzZwp/e1v7vYjTpFrE5ILT1WVW6h++Utp5Ur3+IB2gi0DaBcqKqS1a6W333aft7WV/1dJbjXfvVsqKHC7EW68UbrhBqlbN3/H9pX27pX+7d/cSnP/fn83/be0RMLFzIYN7mPYMGnUKKkRV9QEgogtA2jTqqvdMV8PPuhCIB5vPyFwtETCnf54111Sbq47zTFQ69lEwg0qN9cNct++gA2wBSUXtLffdgvf6tVtb4sHcARiAG3WRx9JhYXSmjVu13N7jYCjJU/DnzbNvSndtMnvEcntUx81yg2qosLWk1FT4xbCwkK3UAJtELsJ0Czl5eWt/jurqqRXXnHHdXle67z5jEZb/3EeT/Ixv/mm9O1vS//6r+5Aw0hrv6Krq92BgXfdVXs+ZDOfkPK2Oi30/v3SQw9Jp50mnXOO72ce+PHaRNvFpENoksrKShUWFip29An0KbZ7t7RqlTtboLWX3MrKNFVUzJQUvJngPM8d6P4//+POQGgVn3wiTZni9p23wJNRKamwa1fF2vpMe57nzjYYO9bNeOijtLQ0zZw5k9kL8bWIATRZZWVlq17cZ+lS6fbb3XrHn63QmQpiCCRFIm6+nOJi98Y0pV55Rfrxj9274RbcV14pqY1uF6grFHJRMH++9C//4tswMjMzCQE0CDGAwIvHpZ/9zJ2lhuNLXi/h8cdTuA5autRNfCDZOTagOW69VbrvPgPTSqItIwYQaLGYdOWV0hNP+D2StmfRIunmm1Nwp7fe2sJ3asAVV0hLlrSj6STR3hADCKyDB6ULL3STCLGUNk1BgXTvvS3wpjSRcJtn7r+/RcZljudJ48e7aZjb7FSSaM84tRCBVFMjXXqpm+iNEGi6hQvdbutmu+ceQqA5EglXtVOnBncKZpjGlgEETiLhZq99+GFCoKU89ljtbv4m/fCVV7boeMzyPOm669xERRxDgAAhBhA4994rzZrl9yjal1BI+v3vpQkTGvmDL7wg/fM/c6BgS1uwQLrtNr9HARxGDCBQVq1yp8WxVLYsz3OnHb73nrtuUINs3y4NGeKujsQT0rI8z13OecwYv0cCSOKYAQTI55+74wTYetryEgk3UdNllzVwl3VNjdu/feAAIZAKnucW9s8/93skgCRiAAGRSLjd0rt3s0U6Vaqrpddea+BxgAsXustAcvGd1IjHpfJy6eqriS0EArsJEAi//a07jRCpF4lIJSXu4oLHtGWLNHgwIdBann9emjTJ71HAOLYMwHeHDkk33VQ7ex5Sr6CgqV9EiwqF3MxQrXyND+Bo/PmF74qKpLIydg+0lupqN/fN+vXH+OK6de6dKlsFWkc8Lm3d6l4EgI/YTQBfRaNS375SRYXfI7ElHJZGjHDr/jpGjpTeeIOJcVpbt27uKpCZmX6PBEaxZQC+euYZQsAPNTVuy8A77xxx4//9n7uREGh9e/e6608DPiEG4KvCQo4V8EskIi1efMQNixe7G9H6QiE3KyHgE3YTwDd//rN05pl+j8K2jAxp1y7pRO2XevZ08wrAP3/+szRsmN+jgEG8J4Nviot5I+q3Awfc9XO0YgUh4LdIxL0oAB8QA/DNihUctO63SER6+WW5/1Bm/qqudi8KwAfsJoAv9uyRsrKYfC0IsrOlvyay3dHs8JfnuZkJTzrJ75HAGLYMwBerVxMCQRHaXkYIBEUiIa1Z4/coYBAxAF+UlLBVOihOVYnfQ0BScq5ooJURA/DF1q1+jwBJudqqhLhUZGDw4oAPiAH4orSUgweDIje0VTUhNtMEQnW1e3EArYy/APDFtm1+jwBJfeOfyBOzDgYGLw74gC0D8MWhQ36PAEkdVKWQuEpUYPDigA+IAfiCKxQGByEQMLw44ANiAL5IS/N7BEg6pA4cQBgkvDjgA2IAvuje3e8RIKnS66a4F/Z7GEjixQEfEAPwxaBBXK0wKLaqv0IeM0AFQijkXhxAK+PPMXzRv78U5s1oIGxJ9FcoztkEgRAOuxcH0MqIAfgiN1eKxfweBSTpI+X6PQQkxWLEAHxBDMAX3/mO3yNAUmn6t5To2NHvYSDpu9/1ewQwiBiAL4YOlU4+2e9RIBSSRn2/g7wxYziIIwiysqQhQ/weBQzi1Q9feJ40fjwXK/JbIiGdf77+/h/4KhJxLwqP0zzR+ogB+Oaf/onrE/gtkZAmTJD0gx8w2Y3fqqvd8wD4wEskuKo8/HHwoNSrl1RZ6fdIbAqH3e7pV1/9+w2jR0vr1xMFfunaVfr0Uyk93e+RwCC2DMA3HTtKV1/NKYZ+qamRrr/+iBuuv54Q8Es47F4MhAB8wpYB+GrLFmngQL9HYVP37tLOnVKHDn+/oapK6t1b2rvX13GZVVoqDRjg9yhgFFsG4KsBA6RLLuFAwtbmedLttx8RApJ7V3r77RzA1toiEenSSwkB+IotA/BdWZnbOsDBhK0jFJL69nVvROttla6qciulv/2NXQatJS1N+vBDKSfH75HAMLYMwHc5OdKNN3Kae2uJx6VFi75i93R6uvsiIdA6QiG38BMC8BlbBhAIFRXSqadK5eWsh1IpEnGzP65Zc5y9AYmEdPbZ0uuvs7kmlUIhN8nQ5s1Sly5+jwbG8V4MgdC1q/Rf/+XWQ0gNz5MyM6WnnvqawwI8z31TRgbHD6RSIiH9938TAggEYgCBMXas9LOfsf5JlURCeuwxKTu7Ad+cne2+mTpLDc+TbrtNGjPG75EAkthNgICJxaSRI6WNG9lC3ZI8T7rqKunf/72RP3jNNdKSJURBSwqHpWHDpHXr3MGDQAAQAwic8nLprLOkTz4hCFpCOCydc470+983Yd0Ti7l5o//0JzdLEZonEnFbXd54wx0vAAQEMYBA2rpV+od/cFMVsw5qukhE+ta33JTDnTo18U6++MJNVfzuuzwZzREOu4Nj3nxT6t/f79EAdXDMAAKpf39p5Uo3ZTHTFTdN8k3oihXNCAHJ/fCKFdIpp/BkNFU47BbmlSsJAQQSMYDAGj7c7Vbt1o0ZChsrHJaGDJE2bJB69GiBO+zZ051qOHQoQdBYkYhbiNetc8cKAAFEDCDQvv1tt3v1G99gHdRQoZDbqv/aay0UAkk9e7r9DaNHM0NUQ0UibuF98023MAMBxSsagde/vwuCM85gHdQQF1/stuqfeGIK7rxzZ3fnF1+cgjtvZzzPLbRvvCH16+f3aIDj4k8r2oQePdxW1hkz3OfMRVBXOOzehBYWuvmCUnol3PR090sKC90vZZNNXcmF8/rrpbVrW3jzDJAanE2ANmfZMmn6dCka5dRDyW0tycmRiot92CX99tvSj3/srjbFPNIujk44QfqP/5AuvNDv0QANxpYBtDmTJ0vvvy9NnOg+t7rrIBJxj/2mm6R33vHp2LRhw9wvT15pyupWguRCOHGitGkTIYA2hy0DaNNefFG69lpp+3Y7k+R5nnus3/mOVFTk5hEIhHffdTMWbthQO0gLPM+ddrl4sTR+vN+jAZrE6HsqtBcTJkglJdLcue6Aufa8lSD52Pr0cVuh164NUAhIbjDr1rnB9enjbmvPB3eEQm6hmztX+uADQgBtGlsG0G7s2yc99JC0cKG7JHJ7WbJDIbc7PjdXuuMOaerUNjClfSwmPf20dPfd0kcf1T6I9sDz3LwBBQXuiNbOnf0eEdBsxADanWhUevRRaf586dNP2+56KBx2s/8OHSrNmeN2Q7e5XfI1Ne6Iz7vukt57r/ZBtTXJhahXL2n2bOnKK931oIF2ghhAu3XokPTkk9LSpW4Cnng8+OuiSMSdIdGxo9sFctVV7t82v7U9kXAHeCxZ4v49eLD2wQZVcmFJzuJ0xRXS5ZdLHTr4PTKgxREDMGHvXumFF6Tf/lb6wx+kAweCsy5KrnNOOsmdKfHDH7qrDHbs6PfIUuTgQemVV6Tly91Wgz17gvNkJMeRkeGu1jhpkvSDH7jdAkA7RgzAnKoqadWq2nXRZ5+5d97J9UAqXxHJs+9iMff5wIHSj37kAuCss9r3AZDHFI+7qXqXL5eee04qLXW3p6W5Qkrl/p2jn/SePV2NTZwojR2b4pmbgGAhBmBaPO7mzXn7bWnzZvfx/vvSX/9auzvh6BV4Q6Sl1Q2L9HQ3rfKQIVJenvv47nddDOAIpaXS+vXShx+6j02b3PWsq6rc15Mr8MY+GUeGRTjsTgU87TTp1FPdkzFsmLsyVpvfHwM0DTEAHEMs5ibVS66TPvxQ2rJF+vJLt15KfsRibkWfnu52JWdkuIPLBw5065hBg9y/ffsafNffUuJxaccOV2rJJ6O01J0+cuCAOzgk+YSkpdU+IenpbjbAAQNqCywvz03XGPjTMYDWRQwAAGAc71UAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACMIwYAADCOGAAAwDhiAAAA44gBAACM+3/SEA9yKFMnowAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33muser_proxy\u001b[0m (to chatbot):\n", - "\n", - "\u001b[32m***** Response from calling function \"python\" *****\u001b[0m\n", - "(0.0, 1.0, 0.0, 1.0)\n", - "\u001b[32m***************************************************\u001b[0m\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mchatbot\u001b[0m (to user_proxy):\n", - "\n", - "The two agents have been drawn, each represented as a circle, and an example of their dialogue is displayed above them. Since `plt.show()` was not to be included, the plot is not displayed here, but the agents along with their dialogue would appear within the figure's coordinate system, which extends from 0 to 1 on both the x and y axes.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33muser_proxy\u001b[0m (to chatbot):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mchatbot\u001b[0m (to user_proxy):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': \"Draw two agents chatting with each other with an example dialog. Don't add plt.show().\", 'role': 'assistant'}, {'function_call': {'arguments': '{\\n \"cell\": \"import matplotlib.pyplot as plt\\\\nimport numpy as np\\\\n\\\\nfig, ax = plt.subplots()\\\\n\\\\n# Define the agents as circles\\\\nagent1 = plt.Circle((0.4, 0.5), 0.1, color=\\'blue\\')\\\\nagent2 = plt.Circle((0.6, 0.5), 0.1, color=\\'red\\')\\\\n\\\\n# Draw the agents\\\\nax.add_artist(agent1)\\\\nax.add_artist(agent2)\\\\n\\\\n# Example dialog boxes\\\\nplt.text(0.28, 0.6, \\\\\"Hello!\\\\\", fontsize=12, bbox=dict(facecolor=\\'white\\', alpha=0.5))\\\\nplt.text(0.58, 0.6, \\\\\"Hi there!\\\\\", fontsize=12, bbox=dict(facecolor=\\'white\\', alpha=0.5))\\\\n\\\\n# Set the limits and remove axes\\\\nax.set_xlim(0, 1)\\\\nax.set_ylim(0, 1)\\\\nax.axis(\\'off\\')\\\\n\"\\n}', 'name': 'python'}, 'content': None, 'role': 'assistant'}, {'content': '(0.0, 1.0, 0.0, 1.0)', 'name': 'python', 'role': 'function'}, {'content': \"The two agents have been drawn, each represented as a circle, and an example of their dialogue is displayed above them. Since `plt.show()` was not to be included, the plot is not displayed here, but the agents along with their dialogue would appear within the figure's coordinate system, which extends from 0 to 1 on both the x and y axes.\", 'role': 'user'}, {'content': '', 'role': 'assistant'}, {'content': 'TERMINATE', 'role': 'user'}], summary='', cost=({'total_cost': 0.04767, 'gpt-4': {'cost': 0.04767, 'prompt_tokens': 973, 'completion_tokens': 308, 'total_tokens': 1281}}, {'total_cost': 0.04767, 'gpt-4': {'cost': 0.04767, 'prompt_tokens': 973, 'completion_tokens': 308, 'total_tokens': 1281}}), human_input=[])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "llm_config = {\n", - " \"model\": \"gpt-4-1106-preview\",\n", - " \"functions\": [\n", - " {\n", - " \"name\": \"python\",\n", - " \"description\": \"run cell in ipython and return the execution result.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"cell\": {\n", - " \"type\": \"string\",\n", - " \"description\": \"Valid Python cell to execute.\",\n", - " }\n", - " },\n", - " \"required\": [\"cell\"],\n", - " },\n", - " },\n", - " {\n", - " \"name\": \"sh\",\n", - " \"description\": \"run a shell script and return the execution result.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"script\": {\n", - " \"type\": \"string\",\n", - " \"description\": \"Valid shell script to execute.\",\n", - " }\n", - " },\n", - " \"required\": [\"script\"],\n", - " },\n", - " },\n", - " ],\n", - " \"config_list\": config_list,\n", - " \"timeout\": 120,\n", - "}\n", - "\n", - "chatbot = CompressibleAgent(\n", - " name=\"chatbot\",\n", - " system_message=\"For coding tasks, only use the functions you have been provided with. Reply TERMINATE when the task is done.\",\n", - " llm_config=llm_config,\n", - " compress_config={\n", - " \"mode\": \"COMPRESS\",\n", - " \"trigger_count\": 600, # set this to a large number for less frequent compression\n", - " \"verbose\": True, # set this to False to suppress the compression log\n", - " \"leave_last_n\": 2,\n", - " },\n", - ")\n", - "\n", - "# create a UserProxyAgent instance named \"user_proxy\"\n", - "user_proxy = autogen.UserProxyAgent(\n", - " name=\"user_proxy\",\n", - " is_termination_msg=lambda x: x.get(\"content\", \"\") and x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n", - " human_input_mode=\"NEVER\",\n", - " max_consecutive_auto_reply=10,\n", - " code_execution_config={\n", - " \"work_dir\": \"coding\",\n", - " \"use_docker\": False,\n", - " }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n", - ")\n", - "\n", - "\n", - "def exec_python(cell):\n", - " ipython = get_ipython()\n", - " result = ipython.run_cell(cell)\n", - " log = str(result.result)\n", - " if result.error_before_exec is not None:\n", - " log += f\"\\n{result.error_before_exec}\"\n", - " if result.error_in_exec is not None:\n", - " log += f\"\\n{result.error_in_exec}\"\n", - " return log\n", - "\n", - "\n", - "def exec_sh(script):\n", - " return user_proxy.execute_code_blocks([(\"sh\", script)])\n", - "\n", - "\n", - "# register the functions\n", - "user_proxy.register_function(\n", - " function_map={\n", - " \"python\": exec_python,\n", - " \"sh\": exec_sh,\n", - " }\n", - ")\n", - "\n", - "# start the conversation\n", - "user_proxy.initiate_chat(\n", - " chatbot,\n", - " message=\"Draw two agents chatting with each other with an example dialog. Don't add plt.show().\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example 3\n", - "This example is from [agent_chat_web_info.ipynb](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_web_info.ipynb). \n", - "We use this example to demonstrate how to pass in a customized compression function. We pass in an compression function `constrain_num_messages`, which constrains the number of messages to be 3 or less. \n", - "The customized function should accept a list of messages as input and return a tuple of `(is_success: bool, compressed_messages: List[Dict])`." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "Show me the YTD gain of 10 largest technology companies as of today.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "To compute the Year-To-Date (YTD) gains of the 10 largest technology companies, I can fetch the latest stock price and the closing price from the last trading day of the previous year. Then calculate the percentage increase for each company.\n", - "\n", - "First, we should fetch the current stock prices and the closing prices as of the last trading day of the previous year for these companies. For this, we can use a financial data API like Alpha Vantage, Yahoo Finance, or similar, which would require an API key and internet access, but I can't perform actions that require internet access.\n", - "\n", - "Instead, I will provide you with Python code that you'd need to run on your local machine. This code utilizes the `yfinance` Python library, which is widely used for retrieving historical market data from Yahoo Finance. If you don't have `yfinance` installed, you'll need to install it by running `pip install yfinance` in your command line.\n", - "\n", - "Here is the code you'll need to execute:\n", - "\n", - "```python\n", - "# filename: ytd_gains.py\n", - "\n", - "import yfinance as yf\n", - "from datetime import datetime, timedelta\n", - "\n", - "# Define the ticker symbols for the 10 largest tech companies.\n", - "# This is a sample list and may not represent the current top 10 companies.\n", - "# You would need to replace this with the actual tickers of the top 10 tech companies.\n", - "tech_companies = [\"AAPL\", \"MSFT\", \"GOOGL\", \"AMZN\", \"FB\", \"TSLA\", \"NVDA\", \"V\", \"ADBE\", \"INTC\"]\n", - "\n", - "# Compute the last day of the last year\n", - "end_of_last_year = datetime(datetime.now().year - 1, 12, 31)\n", - "\n", - "# Retrieve the data and calculate YTD gain for each company\n", - "ytd_gains = {}\n", - "for symbol in tech_companies:\n", - " try:\n", - " # Fetch historical data\n", - " stock = yf.Ticker(symbol)\n", - " last_price = stock.history(period=\"1d\")['Close'][-1]\n", - " prev_close = stock.history(start=end_of_last_year, end=end_of_last_year + timedelta(days=1))['Close'][0]\n", - "\n", - " # Calculate YTD gain\n", - " ytd_gain = ((last_price - prev_close) / prev_close) * 100\n", - " ytd_gains[symbol] = ytd_gain\n", - " except Exception as e:\n", - " # Handle errors by skipping the company and printing an error message\n", - " print(f\"Error retrieving data for {symbol}: {e}\")\n", - "\n", - "# Print the YTD gains\n", - "for symbol, gain in ytd_gains.items():\n", - " print(f\"{symbol}: {gain:.2f}% YTD Gain\")\n", - "\n", - "```\n", - "\n", - "Make sure that `yfinance` is installed and then run this Python script (`ytd_gains.py`). The script will print out the YTD gains for the listed technology companies as a percentage.\n", - "\n", - "Note that the list of the 10 largest technology companies must be updated to reflect the current market situation. If you do not have the updated list, let me know, and I can attempt to retrieve this information for you using different methods.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Error retrieving data for AAPL: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for MSFT: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for GOOGL: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for AMZN: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for FB: index -1 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for TSLA: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for NVDA: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for V: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for ADBE: index 0 is out of bounds for axis 0 with size 0\n", - "Error retrieving data for INTC: index 0 is out of bounds for axis 0 with size 0\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "It seems that the script encountered an error when trying to fetch data for the provided ticker symbols. This might be because either the market is closed and the latest data is not yet available, or because the dates specified do not have available data (e.g., the end of last year might have been a weekend or holiday).\n", - "\n", - "Let's adjust the code to be more robust by checking for valid trading days around the end of last year and to handle cases where the stock data might not be available. Here's the updated code:\n", - "\n", - "```python\n", - "# filename: ytd_gains.py\n", - "\n", - "import yfinance as yf\n", - "from datetime import datetime\n", - "\n", - "def get_last_valid_trading_day(year):\n", - " last_day = datetime(year, 12, 31)\n", - " # Start moving backwards from December 31st to find the last valid trading day of the year\n", - " while True:\n", - " stock_data = yf.download('SPY', start=last_day.strftime('%Y-%m-%d'), end=(last_day + timedelta(days=1)).strftime('%Y-%m-%d'))\n", - " if not stock_data.empty:\n", - " return last_day\n", - " last_day -= timedelta(days=1)\n", - "\n", - "# Define the ticker symbols for the 10 largest tech companies.\n", - "tech_companies = [\"AAPL\", \"MSFT\", \"GOOGL\", \"AMZN\", \"FB\", \"TSLA\", \"NVDA\", \"V\", \"ADBE\", \"INTC\"]\n", - "\n", - "# Compute the last valid trading day of the last year\n", - "end_of_last_year = get_last_valid_trading_day(datetime.now().year - 1)\n", - "\n", - "# Retrieve the data and calculate YTD gain for each company\n", - "ytd_gains = {}\n", - "for symbol in tech_companies:\n", - " try:\n", - " # Fetch historical data\n", - " stock = yf.Ticker(symbol)\n", - " last_price = stock.history(period='1d')['Close'].iloc[-1]\n", - " prev_close = stock.history(start=end_of_last_year, end=end_of_last_year + timedelta(days=1))['Close'].iloc[0]\n", - "\n", - " # Calculate YTD gain\n", - " ytd_gain = ((last_price - prev_close) / prev_close) * 100\n", - " ytd_gains[symbol] = ytd_gain\n", - " except Exception as e:\n", - " # Handle errors by skipping the company and printing an error message\n", - " print(f\"Error retrieving data for {symbol}: {e}\")\n", - "\n", - "# Print the YTD gains\n", - "for symbol, gain in ytd_gains.items():\n", - " print(f\"{symbol}: {gain:.2f}% YTD Gain\")\n", - "```\n", - "\n", - "Please execute the updated `ytd_gains.py` script. The function `get_last_valid_trading_day` will find the last valid trading day of the previous year to handle non-trading days correctly, and the fetch for current price will get the latest available data point even if the market has not yet closed for today.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 1 (execution failed)\n", - "Code output: \n", - "Traceback (most recent call last):\n", - " File \"ytd_gains.py\", line 19, in \n", - " end_of_last_year = get_last_valid_trading_day(datetime.now().year - 1)\n", - " File \"ytd_gains.py\", line 10, in get_last_valid_trading_day\n", - " stock_data = yf.download('SPY', start=last_day.strftime('%Y-%m-%d'), end=(last_day + timedelta(days=1)).strftime('%Y-%m-%d'))\n", - "NameError: name 'timedelta' is not defined\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[35mToken Count (including 468 tokens from system msg and function descriptions). Before compression : 2115 | After: 1223\u001b[0m\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "It seems I forgot to import the `timedelta` class from the `datetime` module. I will correct this oversight and provide the updated code. Here is the corrected code including the missed import:\n", - "\n", - "```python\n", - "# filename: ytd_gains.py\n", - "\n", - "import yfinance as yf\n", - "from datetime import datetime, timedelta\n", - "\n", - "def get_last_valid_trading_day(year):\n", - " last_day = datetime(year, 12, 31)\n", - " # Start moving backwards from December 31st to find the last valid trading day of the year\n", - " while True:\n", - " stock_data = yf.download('SPY', start=last_day.strftime('%Y-%m-%d'), end=(last_day + timedelta(days=1)).strftime('%Y-%m-%d'))\n", - " if not stock_data.empty:\n", - " return last_day\n", - " last_day -= timedelta(days=1)\n", - "\n", - "# Define the ticker symbols for the 10 largest tech companies.\n", - "tech_companies = [\"AAPL\", \"MSFT\", \"GOOGL\", \"AMZN\", \"FB\", \"TSLA\", \"NVDA\", \"V\", \"ADBE\", \"INTC\"]\n", - "\n", - "# Compute the last valid trading day of the last year\n", - "end_of_last_year = get_last_valid_trading_day(datetime.now().year - 1)\n", - "\n", - "# Retrieve the data and calculate YTD gain for each company\n", - "ytd_gains = {}\n", - "for symbol in tech_companies:\n", - " try:\n", - " # Fetch historical data\n", - " stock = yf.Ticker(symbol)\n", - " last_price = stock.history(period='1d')['Close'].iloc[-1]\n", - " prev_close = stock.history(start=end_of_last_year, end=end_of_last_year + timedelta(days=1))['Close'].iloc[0]\n", - "\n", - " # Calculate YTD gain\n", - " ytd_gain = ((last_price - prev_close) / prev_close) * 100\n", - " ytd_gains[symbol] = ytd_gain\n", - " except Exception as e:\n", - " # Handle errors by skipping the company and printing an error message\n", - " print(f\"Error retrieving data for {symbol}: {e}\")\n", - "\n", - "# Print the YTD gains\n", - "for symbol, gain in ytd_gains.items():\n", - " print(f\"{symbol}: {gain:.2f}% YTD Gain\")\n", - "```\n", - "\n", - "Please execute the corrected `ytd_gains.py` script. This should resolve the `NameError` and provide you with the YTD gain for each company.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Error retrieving data for FB: single positional indexer is out-of-bounds\n", - "AAPL: -6.57% YTD Gain\n", - "MSFT: 10.70% YTD Gain\n", - "GOOGL: -1.83% YTD Gain\n", - "AMZN: 17.30% YTD Gain\n", - "TSLA: -18.45% YTD Gain\n", - "NVDA: 66.15% YTD Gain\n", - "V: 8.96% YTD Gain\n", - "ADBE: -4.30% YTD Gain\n", - "INTC: -12.54% YTD Gain\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[35mToken Count (including 468 tokens from system msg and function descriptions). Before compression : 1865 | After: 1133\u001b[0m\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "It looks like an error occurred while retrieving data for FB, which stands for Facebook. This can happen if the ticker symbol has changed or if there was an issue with the data for the date range we were trying to fetch. Facebook, Inc. has been renamed to Meta Platforms, Inc., and its stock symbol is now \"META.\" \n", - "\n", - "I will update the list of technology companies to include \"META\" instead of \"FB\" and run the code again:\n", - "\n", - "```python\n", - "# filename: ytd_gains_updated.py\n", - "\n", - "import yfinance as yf\n", - "from datetime import datetime, timedelta\n", - "\n", - "def get_last_valid_trading_day(year):\n", - " last_day = datetime(year, 12, 31)\n", - " # Start moving backwards from December 31st to find the last valid trading day of the year\n", - " while True:\n", - " stock_data = yf.download('SPY', start=last_day.strftime('%Y-%m-%d'), end=(last_day + timedelta(days=1)).strftime('%Y-%m-%d'))\n", - " if not stock_data.empty:\n", - " return last_day\n", - " last_day -= timedelta(days=1)\n", - "\n", - "# Define the ticker symbols for the 10 largest tech companies with FB replaced by META.\n", - "tech_companies = [\"AAPL\", \"MSFT\", \"GOOGL\", \"AMZN\", \"META\", \"TSLA\", \"NVDA\", \"V\", \"ADBE\", \"INTC\"]\n", - "\n", - "# Compute the last valid trading day of the last year\n", - "end_of_last_year = get_last_valid_trading_day(datetime.now().year - 1)\n", - "\n", - "# Retrieve the data and calculate YTD gain for each company\n", - "ytd_gains = {}\n", - "for symbol in tech_companies:\n", - " try:\n", - " # Fetch historical data\n", - " stock = yf.Ticker(symbol)\n", - " last_price = stock.history(period='1d')['Close'].iloc[-1]\n", - " prev_close = stock.history(start=end_of_last_year, end=end_of_last_year + timedelta(days=1))['Close'].iloc[0]\n", - "\n", - " # Calculate YTD gain\n", - " ytd_gain = ((last_price - prev_close) / prev_close) * 100\n", - " ytd_gains[symbol] = ytd_gain\n", - " except Exception as e:\n", - " # Handle errors by skipping the company and printing an error message\n", - " print(f\"Error retrieving data for {symbol}: {e}\")\n", - "\n", - "# Print the YTD gains\n", - "for symbol, gain in ytd_gains.items():\n", - " print(f\"{symbol}: {gain:.2f}% YTD Gain\")\n", - "```\n", - "\n", - "Please execute the updated `ytd_gains_updated.py` script to get the YTD gain of Meta Platforms, Inc. along with the other technology companies.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33muser_proxy\u001b[0m (to assistant):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "AAPL: -6.57% YTD Gain\n", - "MSFT: 10.70% YTD Gain\n", - "GOOGL: -1.83% YTD Gain\n", - "AMZN: 17.30% YTD Gain\n", - "META: 42.06% YTD Gain\n", - "TSLA: -18.45% YTD Gain\n", - "NVDA: 66.15% YTD Gain\n", - "V: 8.96% YTD Gain\n", - "ADBE: -4.30% YTD Gain\n", - "INTC: -12.54% YTD Gain\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[35mToken Count (including 468 tokens from system msg and function descriptions). Before compression : 1828 | After: 1186\u001b[0m\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to user_proxy):\n", - "\n", - "The YTD (Year-To-Date) gain for the 10 largest technology companies as of today are as follows:\n", - "\n", - "1. Apple Inc. (AAPL): -6.57% YTD Gain\n", - "2. Microsoft Corporation (MSFT): 10.70% YTD Gain\n", - "3. Alphabet Inc. (GOOGL): -1.83% YTD Gain\n", - "4. Amazon.com, Inc. (AMZN): 17.30% YTD Gain\n", - "5. Meta Platforms, Inc. (META, formerly FB): 42.06% YTD Gain\n", - "6. Tesla, Inc. (TSLA): -18.45% YTD Gain\n", - "7. NVIDIA Corporation (NVDA): 66.15% YTD Gain\n", - "8. Visa Inc. (V): 8.96% YTD Gain\n", - "9. Adobe Inc. (ADBE): -4.30% YTD Gain\n", - "10. Intel Corporation (INTC): -12.54% YTD Gain\n", - "\n", - "These YTD gains reflect the percentage change in the stock price of each company from the last trading day of the previous year to the most recent available trading data.\n", - "\n", - "If you need any further assistance, please let me know.\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> NO HUMAN INPUT RECEIVED.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'Show me the YTD gain of 10 largest technology companies as of today.', 'role': 'assistant'}, {'content': 'It looks like an error occurred while retrieving data for FB, which stands for Facebook. This can happen if the ticker symbol has changed or if there was an issue with the data for the date range we were trying to fetch. Facebook, Inc. has been renamed to Meta Platforms, Inc., and its stock symbol is now \"META.\" \\n\\nI will update the list of technology companies to include \"META\" instead of \"FB\" and run the code again:\\n\\n```python\\n# filename: ytd_gains_updated.py\\n\\nimport yfinance as yf\\nfrom datetime import datetime, timedelta\\n\\ndef get_last_valid_trading_day(year):\\n last_day = datetime(year, 12, 31)\\n # Start moving backwards from December 31st to find the last valid trading day of the year\\n while True:\\n stock_data = yf.download(\\'SPY\\', start=last_day.strftime(\\'%Y-%m-%d\\'), end=(last_day + timedelta(days=1)).strftime(\\'%Y-%m-%d\\'))\\n if not stock_data.empty:\\n return last_day\\n last_day -= timedelta(days=1)\\n\\n# Define the ticker symbols for the 10 largest tech companies with FB replaced by META.\\ntech_companies = [\"AAPL\", \"MSFT\", \"GOOGL\", \"AMZN\", \"META\", \"TSLA\", \"NVDA\", \"V\", \"ADBE\", \"INTC\"]\\n\\n# Compute the last valid trading day of the last year\\nend_of_last_year = get_last_valid_trading_day(datetime.now().year - 1)\\n\\n# Retrieve the data and calculate YTD gain for each company\\nytd_gains = {}\\nfor symbol in tech_companies:\\n try:\\n # Fetch historical data\\n stock = yf.Ticker(symbol)\\n last_price = stock.history(period=\\'1d\\')[\\'Close\\'].iloc[-1]\\n prev_close = stock.history(start=end_of_last_year, end=end_of_last_year + timedelta(days=1))[\\'Close\\'].iloc[0]\\n\\n # Calculate YTD gain\\n ytd_gain = ((last_price - prev_close) / prev_close) * 100\\n ytd_gains[symbol] = ytd_gain\\n except Exception as e:\\n # Handle errors by skipping the company and printing an error message\\n print(f\"Error retrieving data for {symbol}: {e}\")\\n\\n# Print the YTD gains\\nfor symbol, gain in ytd_gains.items():\\n print(f\"{symbol}: {gain:.2f}% YTD Gain\")\\n```\\n\\nPlease execute the updated `ytd_gains_updated.py` script to get the YTD gain of Meta Platforms, Inc. along with the other technology companies.', 'role': 'user'}, {'content': 'exitcode: 0 (execution succeeded)\\nCode output: \\nAAPL: -6.57% YTD Gain\\nMSFT: 10.70% YTD Gain\\nGOOGL: -1.83% YTD Gain\\nAMZN: 17.30% YTD Gain\\nMETA: 42.06% YTD Gain\\nTSLA: -18.45% YTD Gain\\nNVDA: 66.15% YTD Gain\\nV: 8.96% YTD Gain\\nADBE: -4.30% YTD Gain\\nINTC: -12.54% YTD Gain\\n', 'role': 'assistant'}, {'content': 'The YTD (Year-To-Date) gain for the 10 largest technology companies as of today are as follows:\\n\\n1. Apple Inc. (AAPL): -6.57% YTD Gain\\n2. Microsoft Corporation (MSFT): 10.70% YTD Gain\\n3. Alphabet Inc. (GOOGL): -1.83% YTD Gain\\n4. Amazon.com, Inc. (AMZN): 17.30% YTD Gain\\n5. Meta Platforms, Inc. (META, formerly FB): 42.06% YTD Gain\\n6. Tesla, Inc. (TSLA): -18.45% YTD Gain\\n7. NVIDIA Corporation (NVDA): 66.15% YTD Gain\\n8. Visa Inc. (V): 8.96% YTD Gain\\n9. Adobe Inc. (ADBE): -4.30% YTD Gain\\n10. Intel Corporation (INTC): -12.54% YTD Gain\\n\\nThese YTD gains reflect the percentage change in the stock price of each company from the last trading day of the previous year to the most recent available trading data.\\n\\nIf you need any further assistance, please let me know.\\n\\nTERMINATE', 'role': 'user'}], summary='The YTD (Year-To-Date) gain for the 10 largest technology companies as of today are as follows:\\n\\n1. Apple Inc. (AAPL): -6.57% YTD Gain\\n2. Microsoft Corporation (MSFT): 10.70% YTD Gain\\n3. Alphabet Inc. (GOOGL): -1.83% YTD Gain\\n4. Amazon.com, Inc. (AMZN): 17.30% YTD Gain\\n5. Meta Platforms, Inc. (META, formerly FB): 42.06% YTD Gain\\n6. Tesla, Inc. (TSLA): -18.45% YTD Gain\\n7. NVIDIA Corporation (NVDA): 66.15% YTD Gain\\n8. Visa Inc. (V): 8.96% YTD Gain\\n9. Adobe Inc. (ADBE): -4.30% YTD Gain\\n10. Intel Corporation (INTC): -12.54% YTD Gain\\n\\nThese YTD gains reflect the percentage change in the stock price of each company from the last trading day of the previous year to the most recent available trading data.\\n\\nIf you need any further assistance, please let me know.\\n\\n', cost=({'total_cost': 0.31437, 'gpt-4': {'cost': 0.31437, 'prompt_tokens': 5401, 'completion_tokens': 2539, 'total_tokens': 7940}}, {'total_cost': 0.31437, 'gpt-4': {'cost': 0.31437, 'prompt_tokens': 5401, 'completion_tokens': 2539, 'total_tokens': 7940}}), human_input=[''])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def constrain_num_messages(messages):\n", - " \"\"\"Constrain the number of messages to 3.\n", - "\n", - " This is an example of a customized compression function.\n", - "\n", - " Returns:\n", - " bool: whether the compression is successful.\n", - " list: the compressed messages.\n", - " \"\"\"\n", - " if len(messages) <= 3:\n", - " # do nothing\n", - " return False, None\n", - "\n", - " # save the first and last two messages\n", - " return True, messages[:1] + messages[-2:]\n", - "\n", - "\n", - "# create a CompressibleAgent instance named \"assistant\"\n", - "assistant = CompressibleAgent(\n", - " name=\"assistant\",\n", - " llm_config={\n", - " \"timeout\": 600,\n", - " \"cache_seed\": 43,\n", - " \"config_list\": config_list,\n", - " \"model\": \"gpt-4-1106-preview\",\n", - " },\n", - " compress_config={\n", - " \"mode\": \"CUSTOMIZED\",\n", - " \"compress_function\": constrain_num_messages, # this is required for customized compression\n", - " \"trigger_count\": 1600,\n", - " },\n", - ")\n", - "\n", - "# create a UserProxyAgent instance named \"user_proxy\"\n", - "user_proxy = autogen.UserProxyAgent(\n", - " name=\"user_proxy\",\n", - " human_input_mode=\"TERMINATE\",\n", - " max_consecutive_auto_reply=10,\n", - " is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\")\n", - " or x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE.\"),\n", - " code_execution_config={\n", - " \"work_dir\": \"web\",\n", - " \"use_docker\": False,\n", - " }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n", - " system_message=\"\"\"Reply TERMINATE if the task has been solved at full satisfaction.\n", - "Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\"\"\",\n", - ")\n", - "\n", - "user_proxy.initiate_chat(\n", - " assistant,\n", - " message=\"\"\"Show me the YTD gain of 10 largest technology companies as of today.\"\"\",\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebook/agentchat_group_chat_with_llamaindex_agents.ipynb b/notebook/agentchat_group_chat_with_llamaindex_agents.ipynb index ddff0a01090e..aea134907b76 100644 --- a/notebook/agentchat_group_chat_with_llamaindex_agents.ipynb +++ b/notebook/agentchat_group_chat_with_llamaindex_agents.ipynb @@ -26,11 +26,7 @@ "metadata": {}, "outputs": [], "source": [ - "! pip install pyautogen\n", - "! pip install llama-index\n", - "! pip install llama-index-tools-wikipedia\n", - "! pip install llama-index-readers-wikipedia\n", - "! pip install wikipedia" + "%pip install pyautogen llama-index llama-index-tools-wikipedia llama-index-readers-wikipedia wikipedia" ] }, { @@ -50,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "dca301a4", "metadata": { "editable": true, @@ -65,7 +61,12 @@ "\n", "import autogen\n", "\n", - "config_list = [{\"model\": \"gpt-3.5-turbo-0125\", \"api_key\": os.getenv(\"OPENAI_API_KEY\")}]" + "config_list = autogen.config_list_from_json(\n", + " \"OAI_CONFIG_LIST\",\n", + " filter_dict={\"tags\": [\"gpt-3.5-turbo\"]}, # comment out to get all\n", + ")\n", + "# When using a single openai endpoint, you can use the following:\n", + "# config_list = [{\"model\": \"gpt-3.5-turbo\", \"api_key\": os.getenv(\"OPENAI_API_KEY\")}]" ] }, { @@ -78,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "2d3d298e", "metadata": {}, "outputs": [], @@ -90,7 +91,7 @@ "from llama_index.tools.wikipedia import WikipediaToolSpec\n", "\n", "llm = OpenAI(\n", - " model=\"gpt-3.5-turbo-0125\",\n", + " model=\"gpt-3.5-turbo\",\n", " temperature=0.0,\n", " api_key=os.environ.get(\"OPENAPI_API_KEY\", \"\"),\n", ")\n", @@ -125,12 +126,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "1a10c9fe-1fbc-40c6-b655-5d2256864ce8", "metadata": {}, "outputs": [], "source": [ - "from llamaindex_conversable_agent import LLamaIndexConversableAgent\n", + "from autogen.agentchat.contrib.llamaindex_conversable_agent import LLamaIndexConversableAgent\n", "\n", "llm_config = {\n", " \"temperature\": 0,\n", @@ -162,7 +163,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "354b4a8f-7a96-455b-9f17-cbc19d880462", "metadata": {}, "outputs": [], @@ -179,10 +180,182 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "d5518947", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mAdmin\u001b[0m (to chat_manager):\n", + "\n", + "\n", + "What can i find in Tokyo related to Hayao Miyazaki and its moveis like Spirited Away?.\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: trip_specialist\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "> Running step 4f4f291b-5e13-495f-9871-4207e4c4bcb9. Step input: \n", + "What can i find in Tokyo related to Hayao Miyazaki and its moveis like Spirited Away?.\n", + "\n", + "\u001b[1;3;38;5;200mThought: The current language of the user is: English. I need to use a tool to help me answer the question.\n", + "Action: search_data\n", + "Action Input: {'query': 'Hayao Miyazaki Tokyo'}\n", + "\u001b[0m\u001b[1;3;34mObservation: Hayao Miyazaki (宮崎 駿 or 宮﨑 駿, Miyazaki Hayao, Japanese: [mijaꜜzaki hajao]; born January 5, 1941) is a Japanese animator, filmmaker, and manga artist. A founder of Studio Ghibli, he has attained international acclaim as a masterful storyteller and creator of Japanese animated feature films, and is widely regarded as one of the most accomplished filmmakers in the history of animation.\n", + "Born in Tokyo City in the Empire of Japan, Miyazaki expressed interest in manga and animation from an early age, and he joined Toei Animation in 1963. During his early years at Toei Animation he worked as an in-between artist and later collaborated with director Isao Takahata. Notable films to which Miyazaki contributed at Toei include Doggie March and Gulliver's Travels Beyond the Moon. He provided key animation to other films at Toei, such as Puss in Boots and Animal Treasure Island, before moving to A-Pro in 1971, where he co-directed Lupin the Third Part I alongside Takahata. After moving to Zuiyō Eizō (later known as Nippon Animation) in 1973, Miyazaki worked as an animator on World Masterpiece Theater, and directed the television series Future Boy Conan (1978). He joined Tokyo Movie Shinsha in 1979 to direct his first feature film The Castle of Cagliostro as well as the television series Sherlock Hound. In the same period, he began writing and illustrating the manga Nausicaä of the Valley of the Wind (1982–1994) and directed the 1984 film adaptation produced by Topcraft.\n", + "Miyazaki co-founded Studio Ghibli in 1985. He directed numerous films with Ghibli, including Laputa: Castle in the Sky (1986), My Neighbor Totoro (1988), Kiki's Delivery Service (1989), and Porco Rosso (1992). The films were met with critical and commercial success in Japan. Miyazaki's film Princess Mononoke was the first animated film ever to win the Japan Academy Film Prize for Picture of the Year, and briefly became the highest-grossing film in Japan following its release in 1997; its distribution to the Western world greatly increased Ghibli's popularity and influence outside Japan. His 2001 film Spirited Away became the highest-grossing film in Japanese history, winning the Academy Award for Best Animated Feature, and is frequently ranked among the greatest films of the 21st century. Miyazaki's later films—Howl's Moving Castle (2004), Ponyo (2008), and The Wind Rises (2013)—also enjoyed critical and commercial success. Following the release of The Wind Rises, Miyazaki announced his retirement from feature films, though he later returned to write and direct his twelfth feature film The Boy and the Heron (2023), for which he won his second Academy Award for Best Animated Feature.\n", + "Miyazaki's works are characterized by the recurrence of themes such as humanity's relationship with nature and technology, the wholesomeness of natural and traditional patterns of living, the importance of art and craftsmanship, and the difficulty of maintaining a pacifist ethic in a violent world. The protagonists of his films are often strong girls or young women, and several of his films present morally ambiguous antagonists with redeeming qualities. Miyazaki's works have been highly praised and awarded; he was named a Person of Cultural Merit for outstanding cultural contributions in November 2012, and received the Academy Honorary Award for his impact on animation and cinema in November 2014. Miyazaki has frequently been cited as an inspiration for numerous animators, directors, and writers.\n", + "\n", + "\n", + "== Early life ==\n", + "Hayao Miyazaki was born on January 5, 1941, in Tokyo City, Empire of Japan, the second of four sons. His father, Katsuji Miyazaki (born 1915), was the director of Miyazaki Airplane, his brother's company, which manufactured rudders for fighter planes during World War II. The business allowed his family to remain affluent during Miyazaki's early life. Miyazaki's father enjoyed purchasing paintings and demonstrating them to guests, but otherwise had little known artistic understanding. He said that he was in the Imperial Japanese Army around 1940; after declaring to his commanding officer that he wished not to fight because of his wife and young child, he was discharged after a lecture about disloyalty. According to Miyazaki, his father often told him about his exploits, claiming that he continued to attend nightclubs after turning 70. Katsuji Miyazaki died on March 18, 1993. After his death, Miyazaki felt that he had often looked at his father negatively and that he had never said anything \"lofty or inspiring\". He regretted not having a serious discussion with his father, and felt that he had inherited his \"anarchistic feelings and his lack of concern about embracing contradictions\".\n", + "\n", + "Miyazaki has noted that some of his earliest memories are of \"bombed-out cities\". In 1944, when he was three years old, Miyazaki's family evacuated to Utsunomiya. After the bombing of Utsunomiya in July 1945, he and his family evacuated to Kanuma. The bombing left a lasting impression on Miyazaki, then aged four. As a child, Miyazaki suffered from digestive problems, and was told that he would not live beyond 20, making him feel like an outcast. From 1947 to 1955, Miyazaki's mother Yoshiko suffered from spinal tuberculosis; she spent the first few years in hospital before being nursed from home. Yoshiko was frugal, and described as a strict, intellectual woman who regularly questioned \"socially accepted norms\". She was closest with Miyazaki, and had a strong influence on him and his later work. Yoshiko Miyazaki died in July 1983 at the age of 72.\n", + "Miyazaki began school in 1947, at an elementary school in Utsunomiya, completing the first through third grades. After his family moved back to Suginami-ku, Miyazaki completed the fourth grade at Ōmiya Elementary School, and fifth grade at Eifuku Elementary School, which was newly established after splitting off from Ōmiya Elementary. After graduating from Eifuku as part of the first graduating class, he attended Ōmiya Junior High School. He aspired to become a manga artist, but discovered he could not draw people; instead, he only drew planes, tanks, and battleships for several years. Miyazaki was influenced by several manga artists, such as Tetsuji Fukushima, Soji Yamakawa and Osamu Tezuka. Miyazaki destroyed much of his early work, believing it was \"bad form\" to copy Tezuka's style as it was hindering his own development as an artist. Around this time, Miyazaki would often see movies with his father, who was an avid moviegoer; memorable films for Miyazaki include Meshi (1951) and Tasogare Sakaba (1955).\n", + "After graduating from Ōmiya Junior High, Miyazaki attended Toyotama High School. During his third and final year, Miyazaki's interest in animation was sparked by Panda and the Magic Serpent (1958), Japan's first feature-length animated film in color; he had sneaked out to watch the film instead of studying for his entrance exams. Miyazaki later recounted that he fell in love with the film's heroine, Bai-Niang, and that the film moved him to tears and left a profound impression; he wrote that he was \"moved to the depths of [his] soul\" and that the \"pure, earnest world of the film\" affirmed a side of him that \"yearned desperately to affirm the world rather than negate it\". After graduating from Toyotama, Miyazaki attended Gakushuin University in the department of political economy, majoring in Japanese Industrial Theory. He joined the \"Children's Literature Research Club\", the \"closest thing back then to a comics club\"; he was sometimes the sole member of the club. In his free time, Miyazaki would visit his art teacher from middle school and sketch in his studio, where the two would drink and \"talk about politics, life, all sorts of things\". Around this time, he also drew manga; he never completed any stories, but accumulated thousands of pages of the beginnings of stories. He also frequently approached manga publishers to rent their stories. In 1960, Miyazaki was a bystander during the Anpo protests, having developed an interest after seeing photographs in Asahi Graph; by that point, he was too late to participate in the demonstrations. Miyazaki graduated from Gakushuin in 1963 with degrees in political science and economics.\n", + "\n", + "\n", + "== Career ==\n", + "\n", + "\n", + "=== Early career ===\n", + "\n", + "In 1963, Miyazaki was employed at Toei Animation; this was the last year the company hired regularly. After gaining employment, he began renting a four-and-a-half tatami (7.4 m2; 80 sq ft) apartment in Nerima, Tokyo; the rent was ¥6,000. His salary at Toei was ¥19,500. Miyazaki worked as an in-between artist on the theatrical feature anime Doggie March and the television anime Wolf Boy Ken (both 1963). He also worked on Gulliver's Travels Beyond the Moon (1965). He was a leader in a labor dispute soon after his arrival, and became chief secretary of Toei's labor union in 1964. Miyazaki later worked as chief animator, concept artist, and scene designer on The Great Adventure of Horus, Prince of the Sun (1968). Throughout the film's production, Miyazaki worked closely with his mentor, Yasuo Ōtsuka, whose approach to animation profoundly influenced Miyazaki's work. Directed by Isao Takahata, with whom Miyazaki would continue to collaborate for the remainder of his career, the film was highly praised, and deemed a pivotal work in the evolution of animation. Miyazaki moved to a residence in Ōizumigakuenchō in April 1969, after the birth of his second son.\n", + "Miyazaki provided key animation for The Wonderful World of Puss 'n Boots (1969), directed by Kimio Yabuki. He created a 12-chapter manga series as a promotional tie-in for the film; the series ran in the Sunday edition of Tokyo Shimbun from January to March 1969. Miyazaki later proposed scenes in the screenplay for Flying Phantom Ship (1969), in which military tanks would cause mass hysteria in downtown Tokyo, and was hired to storyboard and animate the scenes. Under the pseudonym Akitsu Saburō (秋津 三朗), Miyazaki wrote and illustrated the manga People of the Desert, published in 26 installments between September 1969 and March 1970 in Boys and Girls Newspaper (少年少女新聞, Shōnen shōjo shinbun). He was influenced by illustrated stories such as Fukushima's Evil Lord of the Desert (沙漠の魔王, Sabaku no maō). In 1970, Miyazaki moved residence to Tokorozawa. In 1971, he developed structure, characters and designs for Hiroshi Ikeda's adaptation of Animal Treasure Island; he created the 13-part manga adaptation, printed in Tokyo Shimbun from January to March 1971. Miyazaki also provided key animation for Ali Baba and the Forty Thieves.\n", + "Miyazaki left Toei Animation in August 1971, and was hired at A-Pro, where he directed, or co-directed with Takahata, 23 episodes of Lupin the Third Part I, often using the pseudonym Teruki Tsutomu (照樹 務). The two also began pre-production on a series based on Astrid Lindgren's Pippi Longstocking books, designing extensive storyboards; the series was canceled after Miyazaki and Takahata were unable to meet with Lindgren, and permission was refused to complete the project. In 1972 and 1973, Miyazaki wrote, designed and animated two Panda! Go, Panda! shorts, directed by Takahata. After moving from A-Pro to Zuiyō Eizō in June 1973, Miyazaki and Takahata worked on World Masterpiece Theater, which featured their animation series Heidi, Girl of the Alps, an adaptation of Johanna Spyri's Heidi. Zuiyō Eizō continued as Nippon Animation in July 1975. Miyazaki also directed the television series Future Boy Conan (1978), an adaptation of Alexander Key's The Incredible Tide.\n", + "\n", + "\n", + "=== Breakthrough films ===\n", + "Miyazaki left Nippon Animation in 1979, during the production of Anne of Green Gables; he provided scene design and organization on the first fifteen episodes. He moved to Telecom Animation Film, a subsidiary of TMS Entertainment, to direct his first feature anime film, The Castle of Cagliostro (1979), a Lupin III film. In his role at Telecom, Miyazaki helped train the second wave of employees. Miyazaki directed six episodes of Sherlock Hound in 1981, until issues with Sir Arthur Conan Doyle's estate led to a suspension in production; Miyazaki was busy with other projects by the time the issues were resolved, and the remaining episodes were directed by Kyosuke Mikuriya. They were broadcast from November 1984 to May 1985. Miyazaki also wrote the graphic novel The Journey of Shuna, inspired by the Tibetan folk tale \"Prince who became a dog\". The novel was published by Tokuma Shoten in June 1983, dramatized for radio broadcast in 1987, and published in English as Shuna's Journey in 2022. Hayao Miyazaki's Daydream Data Notes was also irregularly published from November 1984 to October 1994 in Model Graphix; selections of the stories received radio broadcast in 1995.\n", + "After the release of The Castle of Cagliostro, Miyazaki began working on his ideas for an animated film adaptation of Richard Corben's comic book Rowlf and pitched the idea to Yutaka Fujioka at TMS. In November 1980, a proposal was drawn up to acquire the film rights. Around that time, Miyazaki was also approached for a series of magazine articles by the editorial staff of Animage. During subsequent conversations, he showed his sketchbooks and discussed basic outlines for envisioned animation projects with editors Toshio Suzuki and Osamu Kameyama, who saw the potential for collaboration on their development into animation. Two projects were proposed: Warring States Demon Castle (戦国魔城, Sengoku ma-jō), to be set in the Sengoku period; and the adaptation of Corben's Rowlf. Both were rejected, as the company was unwilling to fund anime projects not based on existing manga, and the rights for the adaptation of Rowlf could not be secured. An agreement was reached that Miyazaki could start developing his sketches and ideas into a manga for the magazine with the proviso that it would never be made into a film. The manga—titled Nausicaä of the Valley of the Wind—ran from February 1982 to March 1994. The story, as re-printed in the tankōbon volumes, spans seven volumes for a combined total of 1060 pages. Miyazaki drew the episodes primarily in pencil, and it was printed monochrome in sepia-toned ink. Miyazaki resigned from Telecom Animation Film in November 1982.\n", + "\n", + "Following the completion of Nausicaä of the Valley of the Wind's first two volumes, Suzuki and the other editors of Animage encouraged Miyazaki to work on a film adaptation; some documentaries claim he began writing the manga after his film pitch was rejected, but Miyazaki said the manga came first. Miyazaki's imagination was sparked by the mercury poisoning of Minamata Bay and how nature responded and thrived in a poisoned environment, using it to create the film's polluted world. By this time, Miyazaki had moved to the animation studio Topcraft and was finding some of the staff to be unreliable. He eventually decided to bring on several of his previous collaborators for the film's production, including Takahata who would serve as producer. Pre-production began on May 31, 1983; Miyazaki encountered difficulties in creating the screenplay, with only sixteen chapters of the manga to work with. Takahata enlisted experimental and minimalist musician Joe Hisaishi to compose the film's score. Nausicaä of the Valley of the Wind was released on March 11, 1984. It grossed ¥1.48 billion at the box office, and made an additional ¥742 million in distribution income. It is often seen as Miyazaki's pivotal work, cementing his reputation as an animator. It was lauded for its positive portrayal of women, particularly that of main character Nausicaä. Several critics have labeled Nausicaä of the Valley of the Wind as possessing anti-war and feminist themes; Miyazaki argues otherwise, stating that he only wishes to entertain. The successful cooperation on the creation of the manga and the film laid the foundation for other collaborative projects. In April 1984, Miyazaki opened his own office in Suginami Ward, naming it Nibariki.\n", + "\n", + "\n", + "=== Studio Ghibli ===\n", + "\n", + "\n", + "==== Early films (1985–1996) ====\n", + "On June 15, 1985, Miyazaki and Takahata founded the animation production company Studio Ghibli as a subsidiary of Tokuma Shoten. Studio Ghibli's first film was Laputa: Castle in the Sky (1986), directed by Miyazaki. Some of the architecture in the film was also inspired by a Welsh mining town; Miyazaki witnessed the mining strike upon his first visit to Wales in 1984 and admired the miners' dedication to their work and community. Laputa was released on August 2, 1986, by the Toei Company. It sold around 775,000 tickets; Miyazaki and Suzuki expressed their disappointment with the film's box office figures. Miyazaki's following film, My Neighbor Totoro, was released alongside Takahata's Grave of the Fireflies in April 1988 to ensure Studio Ghibli's financial status. My Neighbor Totoro features the theme of the relationship between the environment and humanity, showing that harmony is the result of respecting the environment. While the film received critical acclaim, it was commercially unsuccessful at the box office. However, merchandising was successful, and the film was labeled as a cult classic.\n", + "In 1987, Studio Ghibli acquired the rights to create a film adaptation of Eiko Kadono's novel Kiki's Delivery Service. Miyazaki's work on My Neighbor Totoro prevented him from directing the adaptation; Sunao Katabuchi was chosen as director, and Nobuyuki Isshiki was hired as script writer. Miyazaki's dissatisfaction of Isshiki's first draft led him to make changes to the project, ultimately taking the role of director. Kadono was unhappy with the differences between the book and the screenplay. Miyazaki and Suzuki visited Kadono and invited her to the studio; she allowed the project to continue. The film was originally intended to be a 60-minute special, but expanded into a feature film after Miyazaki completed the storyboards and screenplay. Kiki's Delivery Service premiered on July 29, 1989. It earned ¥2.15 billion at the box office, and was the highest-grossing film in Japan in 1989.\n", + "From March to May 1989, Miyazaki's manga Hikōtei Jidai was published in the magazine Model Graphix. Miyazaki began production on a 45-minute in-flight film for Japan Airlines based on the manga; Suzuki ultimately extended the film into the feature-length film, titled Porco Rosso, as expectations grew. The outbreak of the Yugoslav Wars in 1991 affected Miyazaki, prompting a more sombre tone for the film; Miyazaki would later refer to the film as \"foolish\", as its mature tones were unsuitable for children. The film featured anti-war themes, which Miyazaki would later revisit. The airline remained a major investor in the film, resulting in its initial premiere as an in-flight film, prior to its theatrical release on July 18, 1992. The film was commercially successful and remained one of the highest-grossing films in Japan for several years.\n", + "Studio Ghibli set up its headquarters in Koganei, Tokyo in August 1992. In November 1992, two television spots directed by Miyazaki were broadcast by Nippon Television Network (NTV): Sora Iro no Tane, a 90-second spot adapted from the illustrated story Sora Iro no Tane by Rieko Nakagawa and Yuriko Omura; and Nandarou, a series of five advertisements featuring an undefinable creature. Miyazaki designed the storyboards and wrote the screenplay for Whisper of the Heart (1995), directed by Yoshifumi Kondō.\n", + "\n", + "\n", + "==== Global emergence (1997–2008) ====\n", + "Miyazaki began work on the initial storyboards for Princess Mononoke in August 1994, based on preliminary thoughts and sketches from the late 1970s. While experiencing writer's block during production, Miyazaki accepted a request for the creation of On Your Mark, a music video for the song of the same name by Chage and Aska. In the production of the video, Miyazaki experimented with computer animation to supplement traditional animation. On Your Mark premiered as a short before Whisper of the Heart. Despite the video's popularity, Suzuki said that it was not given \"100 percent\" focus.\n", + "\n", + "In May 1995, Miyazaki took a group of artists and animators to the ancient forests of Yakushima and the mountains of Shirakami-Sanchi, taking photographs and making sketches. The landscapes in the film were inspired by Yakushima. In Princess Mononoke, Miyazaki revisited the ecological and political themes of Nausicaä of the Valley of the Wind. Miyazaki supervised the 144,000 cels in the film, about 80,000 of which were key animation. Princess Mononoke was produced with an estimated budget of ¥2.35 billion (approximately US$23.5 million), making it the most expensive Japanese animated film at the time. Approximately fifteen minutes of the film uses computer animation: about five minutes uses techniques such as 3D rendering, digital composition, and texture mapping; the remaining ten minutes uses digital ink and paint. While the original intention was to digitally paint 5,000 of the film's frames, time constraints doubled this, though it remained below ten percent of the final film.\n", + "Upon its premiere on July 12, 1997, Princess Mononoke was critically acclaimed, becoming the first animated film to win the Japan Academy Film Prize for Picture of the Year. The film was also commercially successful, becoming the highest-grossing film in Japan for several months. Miramax Films purchased the film's distributions rights for North America; while it was largely unsuccessful at the box office, grossing about US$2.3 million, it was seen as the introduction of Studio Ghibli to global markets. Miyazaki claimed Princess Mononoke would be his final film. Tokuma Shoten merged with Studio Ghibli in June 1997. Miyazaki left Studio Ghibli on January 14, 1998, to create a new studio called Butaya, to be succeeded by Kondō; however, Kondō's death impacted Miyazaki, and he returned to Studio Ghibli on January 16, 1999.\n", + "Miyazaki's next film was conceived while on vacation at a mountain cabin with his family and five young girls who were family friends. Miyazaki realized that he had not created a film for 10-year-old girls, and set out to do so. He read shōjō manga magazines like Nakayoshi and Ribon for inspiration, but felt they only offered subjects on \"crushes and romance\", which is not what the girls \"held dear in their hearts\". He decided to produce the film about a female heroine whom they could look up to. Production of the film, titled Spirited Away, commenced in 2000 on a budget of ¥1.9 billion (US$15 million). As with Princess Mononoke, the staff experimented with computer animation, but kept the technology at a level to enhance the story, not to \"steal the show\". Spirited Away deals with symbols of human greed, symbolizing the 1980s Japanese asset price bubble, and a liminal journey through the realm of spirits. The film was released on July 20, 2001; it received critical acclaim, and is considered among the greatest films of the 2000s. It won the Japan Academy Film Prize for Picture of the Year, and the Academy Award for Best Animated Feature. The film was also commercially successful, earning ¥30.4 billion (US$289.1 million) at the box office. It became the highest-grossing film in Japan, a record it maintained for almost 20 years. Following the death of Tokuma in September 2000, Miyazaki served as the head of his funeral committee.\n", + "In September 2001, Studio Ghibli announced the production of Howl's Moving Castle, based on the novel by Diana Wynne Jones. Mamoru Hosoda of Toei Animation was originally selected to direct the film, but disagreements between Hosoda and Studio Ghibli executives led to the project's abandonment. After six months, Studio Ghibli resurrected the project. Miyazaki was inspired to direct the film upon reading Jones' novel, and was struck by the image of a castle moving around the countryside; the novel does not explain how the castle moved, which led to Miyazaki's designs. He traveled to Colmar and Riquewihr in Alsace, France, to study the architecture and the surroundings for the film's setting. Additional inspiration came from the concepts of future technology in Albert Robida's work. It was released on November 20, 2004, and received widespread critical acclaim. The film received the Osella Award for Technical Excellence at the 61st Venice International Film Festival, and was nominated for the Academy Award for Best Animated Feature. In Japan, the film grossed a record $14.5 million in its first week of release. It remains among the highest-grossing films in Japan, with a worldwide gross of over ¥19.3 billion. Miyazaki received the honorary Golden Lion for Lifetime Achievement award at the 62nd Venice International Film Festival in 2005.\n", + "In March 2005, Studio Ghibli split from Tokuma Shoten. In the 1980s, Miyazaki had contacted Ursula K. Le Guin expressing interest in producing an adaptation of her Earthsea novels; unaware of Miyazaki's work, Le Guin declined. Upon watching My Neighbor Totoro several years later, Le Guin expressed approval to the concept of the adaptation. She met with Suzuki in August 2005, who wanted Miyazaki's son Goro to direct the film, as Miyazaki had wished to retire. Disappointed that Miyazaki was not directing, but under the impression that he would supervise his son's work, Le Guin approved of the film's production. Miyazaki later publicly opposed and criticized Gorō's appointment as director. Upon Miyazaki's viewing of the film, he wrote a message for his son: \"It was made honestly, so it was good\".\n", + "Miyazaki designed the covers for several manga novels in 2006, including A Trip to Tynemouth; he also worked as editor, and created a short manga for the book. Miyazaki's next film, Ponyo, began production in May 2006. It was initially inspired by \"The Little Mermaid\" by Hans Christian Andersen, though began to take its own form as production continued. Miyazaki aimed for the film to celebrate the innocence and cheerfulness of a child's universe. He intended for it to only use traditional animation, and was intimately involved with the artwork. He preferred to draw the sea and waves himself, as he enjoyed experimenting. Ponyo features 170,000 frames—a record for Miyazaki. The film's seaside village was inspired by Tomonoura, a town in Setonaikai National Park, where Miyazaki stayed in 2005. The main character, Sōsuke, is based on Gorō. Following its release on July 19, 2008, Ponyo was critically acclaimed, receiving Animation of the Year at the 32nd Japan Academy Film Prize. The film was also a commercial success, earning ¥10 billion (US$93.2 million) in its first month and ¥15.5 billion by the end of 2008, placing it among the highest-grossing films in Japan.\n", + "\n", + "\n", + "==== Later films (2009–present) ====\n", + "\n", + "In early 2009, Miyazaki began writing a manga called Kaze Tachinu (風立ちぬ, The Wind Rises), telling the story of Mitsubishi A6M Zero fighter designer Jiro Horikoshi. The manga was first published in two issues of the Model Graphix magazine, published on February 25 and March 25, 2009. Miyazaki later co-wrote the screenplay for Arrietty (2010) and From Up on Poppy Hill (2011), directed by Hiromasa Yonebayashi and Gorō Miyazaki respectively. Miyazaki wanted his next film to be a sequel to Ponyo, but Suzuki convinced him to instead adapt Kaze Tachinu to film. In November 2012, Studio Ghibli announced the production of The Wind Rises, based on Kaze Tachinu, to be released alongside Takahata's The Tale of the Princess Kaguya.\n", + "Miyazaki was inspired to create The Wind Rises after reading a quote from Horikoshi: \"All I wanted to do was to make something beautiful\". Several scenes in The Wind Rises were inspired by Tatsuo Hori's novel The Wind Has Risen (風立ちぬ), in which Hori wrote about his life experiences with his fiancée before she died from tuberculosis. The female lead character's name, Naoko Satomi, was borrowed from Hori's novel Naoko (菜穂子). The Wind Rises continues to reflect Miyazaki's pacifist stance, continuing the themes of his earlier works, despite stating that condemning war was not the intention of the film. The film premiered on July 20, 2013, and received critical acclaim; it was named Animation of the Year at the 37th Japan Academy Film Prize, and was nominated for Best Animated Feature at the 86th Academy Awards. It was also commercially successful, grossing ¥11.6 billion (US$110 million) at the Japanese box office, becoming the highest-grossing film in Japan in 2013.\n", + "In September 2013, Miyazaki announced that he was retiring from the production of feature films due to his age, but wished to continue working on the displays at the Studio Ghibli Museum. Miyazaki was awarded the Academy Honorary Award at the Governors Awards in November 2014. He developed Boro the Caterpillar, an animated short film which was first discussed during pre-production for Princess Mononoke. It was screened exclusively at the Studio Ghibli Museum in July 2017. Around this time, Miyazaki was working on a manga titled Teppo Samurai. In February 2019, a four-part documentary was broadcast on the NHK network titled 10 Years with Hayao Miyazaki, documenting production of his films in his private studio. In 2019, Miyazaki approved a musical adaptation of Nausicaä of the Valley of the Wind, as it was performed by a kabuki troupe.\n", + "In August 2016, Miyazaki proposed a new feature-length film, Kimi-tachi wa Dō Ikiru ka (titled The Boy and the Heron in English), on which he began animation work without receiving official approval. The film opened in Japanese theaters on July 14, 2023. It was preceded by a minimal marketing campaign, forgoing trailers, commercials, and advertisements, a response from Suzuki to his perceived oversaturation of marketing materials in mainstream films. Despite claims that The Boy and the Heron would be Miyazaki's final film, Studio Ghibli vice president Junichi Nishioka said in September 2023 that Miyazaki continued to attend the office daily to plan his next film. Suzuki said he could no longer convince Miyazaki to retire. The Boy and the Heron won Miyazaki his second Academy Award for Best Animated Feature at the 96th Academy Awards, becoming the oldest director to do so; Miyazaki did not attend the show due to his advanced age.\n", + "\n", + "\n", + "== Views ==\n", + "\n", + "Miyazaki has often criticized the state of the animation industry, stating that some animators lack a foundational understanding of their subjects and do not prioritize realism. He is particularly critical of Japanese animation, saying that anime is \"produced by humans who can't stand looking at other humans ... that's why the industry is full of otaku !\". He has frequently criticized otaku, including \"fanatics\" of guns and fighter aircraft, declaring it a \"fetish\" and refusing to identify himself as such. He bemoaned the state of Disney animated films in 1988, saying \"they show nothing but contempt for the audience\".\n", + "In 2013, Miyazaki criticized Japanese Prime Minister Shinzo Abe's policies and the proposed Constitutional amendment that would allow Abe to revise the clause outlawing war as a means to settle international disputes. Miyazaki felt Abe wished to \"leave his name in history as a great man who revised the Constitution and its interpretation\", describing it as \"despicable\" and stating \"People who don't think enough shouldn't meddle with the constitution\". In 2015, Miyazaki disapproved Abe's denial of Japan's military aggression, stating Japan \"should clearly say that [they] inflicted enormous damage on China and express deep remorse over it\". He felt the government should give a \"proper apology\" to Korean comfort women who were forced to service the Japanese army during World War II and suggested the Senkaku Islands be \"split in half\" or controlled by both Japan and China. After the release of The Wind Rises in 2013, some online critics labeled Miyazaki a \"traitor\" and \"anti-Japanese\", describing the film as overly \"left-wing\"; Miyazaki recognized leftist values in his movies, citing his influence by and appreciation of communism as defined by Karl Marx, but criticized the Soviet Union's political system.\n", + "In 2003, Miyazaki refused to attend the 75th Academy Awards in Hollywood in protest of the United States's involvement in the Iraq War, and later said he \"didn't want to visit a country that was bombing Iraq\". He did not publicly express this opinion at the request of his producer until 2009 when he lifted his boycott and attended San Diego Comic Con International as a favor to his friend John Lasseter. Miyazaki also expressed his opinion about the terrorist attack at the offices of the French satirical magazine Charlie Hebdo, criticizing the magazine's decision to publish the content cited as the catalyst for the incident; he felt caricatures should be made of politicians, not cultures. In November 2016, Miyazaki stated that he believed \"many of the people who voted for Brexit and Trump\" were affected by the increase in unemployment due to companies \"building cars in Mexico because of low wages and [selling] them in the US\". He did not think that Donald Trump would be elected president, calling it \"a terrible thing\", and said that Trump's political opponent Hillary Clinton was \"terrible as well\".\n", + "\n", + "\n", + "== Themes ==\n", + "Miyazaki's works are characterized by the recurrence of themes such as feminism, environmentalism, pacifism, love, and family. His narratives are also notable for not pitting a hero against an unsympathetic antagonist; Miyazaki felt Spirited Away's Chihiro \"manages not because she has destroyed the 'evil', but because she has acquired the ability to survive\".\n", + "Miyazaki's films often emphasize environmentalism and the Earth's fragility. Margaret Talbot stated that Miyazaki dislikes modern technology, and believes much of modern culture is \"thin and shallow and fake\"; he anticipates a time with \"no more high-rises\". Miyazaki felt frustrated growing up in the Shōwa period from 1955 to 1965 because \"nature—the mountains and rivers—was being destroyed in the name of economic progress\". Peter Schellhase of The Imaginative Conservative identified that several antagonists of Miyazaki's films \"attempt to dominate nature in pursuit of political domination, and are ultimately destructive to both nature and human civilization\". Miyazaki is critical of exploitation under both communism and capitalism, as well as globalization and its effects on modern life, believing that \"a company is common property of the people that work there\". Ram Prakash Dwivedi identified values of Mahatma Gandhi in the films of Miyazaki.\n", + "Several of Miyazaki's films feature anti-war themes. Daisuke Akimoto of Animation Studies categorized Porco Rosso as \"anti-war propaganda\" and felt the protagonist, Porco, transforms into a pig partly due to his extreme distaste of militarism. Akimoto also argues that The Wind Rises reflects Miyazaki's \"antiwar pacifism\", despite the latter stating that the film does not attempt to \"denounce\" war. Schellhase also identifies Princess Mononoke as a pacifist film due to the protagonist, Ashitaka; instead of joining the campaign of revenge against humankind, as his ethnic history would lead him to do, Ashitaka strives for peace. David Loy and Linda Goodhew argue that both Nausicaä of the Valley of the Wind and Princess Mononoke do not depict traditional evil, but the Buddhist roots of evil: greed, ill will, and delusion; according to Buddhism, the roots of evil must transform into \"generosity, loving-kindness and wisdom\" in order to overcome suffering, and both Nausicaä and Ashitaka accomplish this. When characters in Miyazaki's films are forced to engage in violence, it is shown as being a difficult task; in Howl's Moving Castle, Howl is forced to fight an inescapable battle in defense of those he loves, and it almost destroys him, though he is ultimately saved by Sophie's love and bravery.\n", + "Suzuki described Miyazaki as a feminist in reference to his attitude to female workers. Miyazaki has described his female characters as \"brave, self-sufficient girls that don't think twice about fighting for what they believe in with all their heart\", stating that they may \"need a friend, or a supporter, but never a saviour\" and that \"any woman is just as capable of being a hero as any man\". Nausicaä of the Valley of the Wind was lauded for its positive portrayal of women, particularly protagonist Nausicaä. Schellhase noted that the female characters in Miyazaki's films are not objectified or sexualized, and possess complex and individual characteristics absent from Hollywood productions. Schellhase also identified a \"coming of age\" element for the heroines in Miyazaki's films, as they each discover \"individual personality and strengths\". Gabrielle Bellot of The Atlantic wrote that, in his films, Miyazaki \"shows a keen understanding of the complexities of what it might mean to be a woman\". In particular, Bellot cites Nausicaä of the Valley of the Wind, praising the film's challenging of gender expectations, and the strong and independent nature of Nausicaä. Bellot also noted that Princess Mononoke's San represents the \"conflict between selfhood and expression\".\n", + "Miyazaki is concerned with the sense of wonder in young people, seeking to maintain themes of love and family in his films. Michael Toscano of Curator found that Miyazaki \"fears Japanese children are dimmed by a culture of overconsumption, overprotection, utilitarian education, careerism, techno-industrialism, and a secularism that is swallowing Japan's native animism\". Schellhase wrote that several of Miyazaki's works feature themes of love and romance, but felt emphasis is placed on \"the way lonely and vulnerable individuals are integrated into relationships of mutual reliance and responsibility, which generally benefit everyone around them\". He also found that many of the protagonists in Miyazaki's films present an idealized image of families, whereas others are dysfunctional.\n", + "\n", + "\n", + "== Creation process and influences ==\n", + "Miyazaki forgoes traditional screenplays in his productions, instead developing the film's narrative as he designs the storyboards. \"We never know where the story will go but we just keep working on the film as it develops,\" he said. In each of his films, Miyazaki has employed traditional animation methods, drawing each frame by hand; computer-generated imagery has been employed in several of his later films, beginning with Princess Mononoke, to \"enrich the visual look\", though he ensures that each film can \"retain the right ratio between working by hand and computer ... and still be able to call my films 2D\". He oversees every frame of his films. For character designs, Miyazaki draws original drafts used by animation directors to create reference sheets, which are then corrected by Miyazaki in his style.\n", + "Miyazaki has cited several Japanese artists as his influences, including Sanpei Shirato, Osamu Tezuka, Soji Yamakawa, and Isao Takahata. A number of Western authors have also influenced his works, including Frédéric Back, Lewis Carroll, Roald Dahl, Jean Giraud, Paul Grimault, Ursula K. Le Guin, and Yuri Norstein, as well as animation studio Aardman Animations (specifically the works of Nick Park). Specific works that have influenced Miyazaki include Animal Farm (1945), The Snow Queen (1957), and The King and the Mockingbird (1980); The Snow Queen is said to be the true catalyst for Miyazaki's filmography, influencing his training and work. When animating young children, Miyazaki often takes inspiration from his friends' children, as well as memories of his own childhood.\n", + "\n", + "\n", + "== Personal life ==\n", + "\n", + "Miyazaki married fellow animator Akemi Ōta in October 1965; the two had met while colleagues at Toei Animation. The couple have two sons: Goro, born in January 1967, and Keisuke, born in April 1969. Miyazaki felt that becoming a father changed him, as he tried to produce work that would please his children. Miyazaki initially fulfilled a promise to his wife that they would both continue to work after Goro's birth, dropping him off at preschool for the day; however, upon seeing Goro's exhaustion walking home one day, Miyazaki decided that they could not continue, and his wife stayed at home to raise their children. Miyazaki's dedication to his work harmed his relationship with his children, as he was often absent. Goro watched his father's works in an attempt to \"understand\" him, since the two rarely talked. Miyazaki said that he \"tried to be a good father, but in the end [he] wasn't a very good parent\". During the production of Tales from Earthsea in 2006, Goro said that his father \"gets zero marks as a father but full marks as a director of animated films\".\n", + "Goro worked at a landscape design firm before beginning to work at the Ghibli Museum; he designed the garden on its rooftop and eventually became its curator. Keisuke studied forestry at Shinshu University and works as a wood artist; he designed a woodcut print that appears in Whisper of the Heart. Miyazaki's niece, Mei Okuyama, who was the inspiration behind the character Mei in My Neighbor Totoro, is married to animation artist Daisuke Tsutsumi.\n", + "\n", + "\n", + "== Legacy ==\n", + "Miyazaki was described as the \"godfather of animation in Japan\" by BBC's Tessa Wong in 2016, citing his craftsmanship and humanity, the themes of his films, and his inspiration to younger artists. Courtney Lanning of Arkansas Democrat-Gazette named him one of the world's greatest animators, comparing him to Osamu Tezuka and Walt Disney. Swapnil Dhruv Bose of Far Out Magazine wrote that Miyazaki's work \"has shaped not only the future of animation but also filmmaking in general\", and that it helped \"generation after generation of young viewers to observe the magic that exists in the mundane\". Richard James Havis of South China Morning Post called him a \"genius ... who sets exacting standards for himself, his peers and studio staff\". Paste's Toussaint Egan described Miyazaki as \"one of anime's great auteurs\", whose \"stories of such singular thematic vision and unmistakable aesthetic\" captured viewers otherwise unfamiliar with anime. Miyazaki became the subject of an exhibit at the Academy Museum of Motion Pictures in Los Angeles in 2021, featuring over 400 objects from his films.\n", + "Miyazaki has frequently been cited as an inspiration to numerous animators, directors and writers around the world, including Wes Anderson, James Cameron, Dean DeBlois, Guillermo del Toro, Pete Docter, Mamoru Hosoda, Bong Joon-ho, Travis Knight, John Lasseter, Nick Park, Henry Selick, Makoto Shinkai, and Steven Spielberg. Glen Keane said Miyazaki is a \"huge influence\" on Walt Disney Animation Studios and has been \"part of our heritage\" ever since The Rescuers Down Under (1990). The Disney Renaissance era was also prompted by competition with the development of Miyazaki's films. Artists from Pixar and Aardman Studios signed a tribute stating, \"You're our inspiration, Miyazaki-san!\" He has also been cited as inspiration for video game designers including Shigeru Miyamoto on The Legend of Zelda and Hironobu Sakaguchi on Final Fantasy, as well as the television series Avatar: The Last Airbender, and the video game Ori and the Blind Forest (2015).\n", + "Studio Ghibli has searched for some time for Miyazaki and Suzuki's successor to lead the studio; Kondō, the director of Whisper of the Heart, was initially considered, but died from a sudden heart attack in 1998. Some candidates were considered by 2023—including Miyazaki's son Goro, who declined—but the studio was not able to find a successor.\n", + "\n", + "\n", + "== Selected filmography ==\n", + "\n", + "The Castle of Cagliostro (1979)\n", + "Nausicaä of the Valley of the Wind (1984)\n", + "Laputa: Castle in the Sky (1986)\n", + "My Neighbor Totoro (1988)\n", + "Kiki's Delivery Service (1989)\n", + "Porco Rosso (1992)\n", + "Princess Mononoke (1997)\n", + "Spirited Away (2001)\n", + "Howl's Moving Castle (2004)\n", + "Ponyo (2008)\n", + "The Wind Rises (2013)\n", + "The Boy and the Heron (2023)\n", + "\n", + "\n", + "== Awards and nominations ==\n", + "\n", + "Miyazaki won the Ōfuji Noburō Award at the Mainichi Film Awards for The Castle of Cagliostro (1979), Nausicaä of the Valley of the Wind (1984), Laputa: Castle in the Sky (1986), and My Neighbor Totoro (1988), and the Mainichi Film Award for Best Animation Film for Kiki's Delivery Service (1989), Porco Rosso (1992), Princess Mononoke (1997), Spirited Away (2001), and Whale Hunt (2001). Spirited Away and The Boy and the Heron were awarded the Academy Award for Best Animated Feature, while Howl's Moving Castle (2004) and The Wind Rises (2013) received nominations. He was named a Person of Cultural Merit by the Japanese government in November 2012, for outstanding cultural contributions. In 2024, Time named him one of the 100 most influential people in the world, and Gold House honored him on its Most Impactful Asians A100 list. His other accolades include several Annie Awards, Japan Academy Film Prizes, Kinema Junpo Awards, and Tokyo Anime Awards.\n", + "\n", + "\n", + "== Notes ==\n", + "\n", + "\n", + "== References ==\n", + "\n", + "\n", + "== Sources ==\n", + "\n", + "\n", + "== External links ==\n", + "\n", + "Studio Ghibli (in Japanese)\n", + "Hayao Miyazaki at Anime News Network's encyclopedia\n", + "Hayao Miyazaki at IMDb\n", + "Hayao Miyazaki at Library of Congress, with 14 library catalogue records\n", + "\u001b[0m> Running step 015c82f1-c7ff-4de0-9930-ab2467424c1c. Step input: None\n", + "\u001b[1;3;38;5;200mThought: I have found detailed information about Hayao Miyazaki, his works, and his life in Tokyo. Now, I can provide a summary of the relevant details related to Tokyo and his movies like \"Spirited Away.\"\n", + "Answer: Hayao Miyazaki, a renowned Japanese animator, filmmaker, and manga artist, was born in Tokyo City, Japan. He expressed interest in manga and animation from a young age and joined Toei Animation in 1963. Miyazaki has contributed to various notable films and series, including \"The Castle of Cagliostro,\" \"Princess Mononoke,\" \"Spirited Away,\" and more. He co-founded Studio Ghibli in 1985, where he directed several successful films like \"My Neighbor Totoro,\" \"Kiki's Delivery Service,\" and \"Porco Rosso.\" His films often explore themes such as humanity's relationship with nature, the importance of art and craftsmanship, and the challenges of maintaining pacifism in a violent world.\n", + "\n", + "One of Miyazaki's most famous works, \"Spirited Away,\" became the highest-grossing film in Japanese history and won the Academy Award for Best Animated Feature. The film is known for its rich storytelling and captivating animation. Miyazaki's works are characterized by strong female protagonists, morally ambiguous antagonists, and themes of environmentalism, love, and family. His films have received critical acclaim and commercial success, influencing a generation of animators and filmmakers worldwide.\n", + "\n", + "Miyazaki's legacy extends beyond his films, as he has been recognized for his craftsmanship, thematic depth, and influence on the animation industry. His dedication to hand-drawn animation and storytelling has inspired numerous artists and filmmakers globally.\n", + "\u001b[0m\u001b[33mtrip_specialist\u001b[0m (to chat_manager):\n", + "\n", + "Hayao Miyazaki, a renowned Japanese animator, filmmaker, and manga artist, was born in Tokyo City, Japan. He expressed interest in manga and animation from a young age and joined Toei Animation in 1963. Miyazaki has contributed to various notable films and series, including \"The Castle of Cagliostro,\" \"Princess Mononoke,\" \"Spirited Away,\" and more. He co-founded Studio Ghibli in 1985, where he directed several successful films like \"My Neighbor Totoro,\" \"Kiki's Delivery Service,\" and \"Porco Rosso.\" His films often explore themes such as humanity's relationship with nature, the importance of art and craftsmanship, and the challenges of maintaining pacifism in a violent world.\n", + "\n", + "One of Miyazaki's most famous works, \"Spirited Away,\" became the highest-grossing film in Japanese history and won the Academy Award for Best Animated Feature. The film is known for its rich storytelling and captivating animation. Miyazaki's works are characterized by strong female protagonists, morally ambiguous antagonists, and themes of environmentalism, love, and family. His films have received critical acclaim and commercial success, influencing a generation of animators and filmmakers worldwide.\n", + "\n", + "Miyazaki's legacy extends beyond his films, as he has been recognized for his craftsmanship, thematic depth, and influence on the animation industry. His dedication to hand-drawn animation and storytelling has inspired numerous artists and filmmakers globally.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Admin\n", + "\u001b[0m\n" + ] + } + ], "source": [ "chat_result = user_proxy.initiate_chat(\n", " manager,\n", @@ -217,7 +390,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/notebook/agentchat_groupchat_RAG.ipynb b/notebook/agentchat_groupchat_RAG.ipynb index 1057deabf924..e18bd99c1511 100644 --- a/notebook/agentchat_groupchat_RAG.ipynb +++ b/notebook/agentchat_groupchat_RAG.ipynb @@ -35,14 +35,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "LLM models: ['gpt4-1106-preview', 'gpt-35-turbo', 'gpt-35-turbo-0613']\n" + "LLM models: ['gpt-35-turbo', 'gpt4-1106-preview', 'gpt-4o']\n" ] } ], @@ -75,18 +75,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 16, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages/transformers/utils/generic.py:311: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.\n", - " torch.utils._pytree._register_pytree_node(\n" - ] - } - ], + "outputs": [], "source": [ "def termination_msg(x):\n", " return isinstance(x, dict) and \"TERMINATE\" == str(x.get(\"content\", \"\"))[-9:].upper()\n", @@ -205,15 +196,9 @@ " n_results: Annotated[int, \"number of results\"] = 3,\n", " ) -> str:\n", " boss_aid.n_results = n_results # Set the number of results to be retrieved.\n", - " # Check if we need to update the context.\n", - " update_context_case1, update_context_case2 = boss_aid._check_update_context(message)\n", - " if (update_context_case1 or update_context_case2) and boss_aid.update_context:\n", - " boss_aid.problem = message if not hasattr(boss_aid, \"problem\") else boss_aid.problem\n", - " _, ret_msg = boss_aid._generate_retrieve_user_reply(message)\n", - " else:\n", - " _context = {\"problem\": message, \"n_results\": n_results}\n", - " ret_msg = boss_aid.message_generator(boss_aid, None, _context)\n", - " return ret_msg if ret_msg else message\n", + " _context = {\"problem\": message, \"n_results\": n_results}\n", + " ret_msg = boss_aid.message_generator(boss_aid, None, _context)\n", + " return ret_msg or message\n", "\n", " boss_aid.human_input_mode = \"NEVER\" # Disable human input for boss_aid since it only retrieves content.\n", "\n", @@ -255,7 +240,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -266,137 +251,130 @@ "\n", "How to use spark for parallel training in FLAML? Give me sample code.\n", "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", - "\n", - "To use Spark for parallel training in FLAML (Fast and Lightweight AutoML), you would need to set up a Spark cluster and utilize the `spark` backend for joblib, which FLAML uses internally for parallel training. Here’s an example of how you might set up and use Spark with FLAML for AutoML tasks:\n", - "\n", - "Firstly, ensure that you have the Spark cluster set up and the `pyspark` and `joblib-spark` packages installed in your environment. You can install the required packages using pip if they are not already installed:\n", + "--------------------------------------------------------------------------------\n", + "How to use spark for parallel training in FLAML? Give me sample code.\n", "\n", - "```python\n", - "!pip install flaml pyspark joblib-spark\n", - "```\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", "\n", - "Here's a sample code snippet that demonstrates how to use FLAML with Spark for parallel training:\n", + "To use Spark for parallel training in FLAML, you need to install `pyspark` package and set up a Spark cluster. Here's some sample code for using Spark in FLAML:\n", "\n", "```python\n", "from flaml import AutoML\n", "from pyspark.sql import SparkSession\n", - "from sklearn.datasets import load_digits\n", - "from joblibspark import register_spark\n", "\n", - "# Initialize a Spark session\n", - "spark = SparkSession.builder \\\n", - " .master(\"local[*]\") \\\n", - " .appName(\"FLAML_Spark_Example\") \\\n", - " .getOrCreate()\n", + "# create a SparkSession\n", + "spark = SparkSession.builder.appName(\"FLAML-Spark\").getOrCreate()\n", "\n", - "# Register the joblib spark backend\n", - "register_spark() # This registers the backend for parallel processing\n", - "\n", - "# Load sample data\n", - "X, y = load_digits(return_X_y=True)\n", - "\n", - "# Initialize an AutoML instance\n", + "# create a FLAML AutoML object with Spark backend\n", "automl = AutoML()\n", "\n", - "# Define the settings for the AutoML run\n", + "# load data from Spark DataFrame\n", + "data = spark.read.format(\"csv\").option(\"header\", \"true\").load(\"data.csv\")\n", + "\n", + "# specify the target column and task type\n", "settings = {\n", - " \"time_budget\": 60, # Total running time in seconds\n", - " \"metric\": 'accuracy', # Primary metric for evaluation\n", - " \"task\": 'classification', # Task type\n", - " \"n_jobs\": -1, # Number of jobs to run in parallel (use -1 for all)\n", - " \"estimator_list\": ['lgbm', 'rf', 'xgboost'], # List of estimators to consider\n", - " \"log_file_name\": \"flaml_log.txt\", # Log file name\n", + " \"time_budget\": 60, # time budget in seconds\n", + " \"metric\": 'accuracy',\n", + " \"task\": 'classification',\n", "}\n", "\n", - "# Run the AutoML search with Spark backend\n", - "automl.fit(X_train=X, y_train=y, **settings)\n", + "# train and validate models in parallel using Spark\n", + "best_model = automl.fit(data, **settings)\n", "\n", - "# Output the best model and its performance\n", - "print(f\"Best ML model: {automl.model}\")\n", - "print(f\"Best ML model's accuracy: {automl.best_loss}\")\n", + "# print the best model and its metadata\n", + "print(automl.model_name)\n", + "print(automl.best_model)\n", + "print(automl.best_config)\n", "\n", - "# Stop the Spark session\n", + "# stop the SparkSession\n", "spark.stop()\n", + "\n", + "# terminate the code execution\n", + "TERMINATE\n", "```\n", "\n", - "The `register_spark()` function from `joblib-spark` is used to register the Spark backend with joblib, which is utilized for parallel training within FLAML. The `n_jobs=-1` parameter tells FLAML to use all available Spark executors for parallel training.\n", + "Note that this is just a sample code, you may need to modify it to fit your specific use case.\n", "\n", - "Please note that the actual process of setting up a Spark cluster can be complex and might involve additional steps such as configuring Spark workers, allocating resources, and more, which are beyond the scope of this code snippet.\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Code_Reviewer\n", + "\u001b[0m\n", + "\u001b[33mCode_Reviewer\u001b[0m (to chat_manager):\n", "\n", - "If you encounter any issues or need to adjust configurations for your specific Spark setup, please refer to the Spark and FLAML documentation for more details.\n", "\n", - "When you run the code, ensure that your Spark cluster is properly configured and accessible from your Python environment. Adjust the `.master(\"local[*]\")` to point to your Spark master's URL if you are running a cluster that is not local.\n", "\n", "--------------------------------------------------------------------------------\n", - "To use Spark for parallel training in FLAML (Fast and Lightweight AutoML), you would need to set up a Spark cluster and utilize the `spark` backend for joblib, which FLAML uses internally for parallel training. Here’s an example of how you might set up and use Spark with FLAML for AutoML tasks:\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", - "Firstly, ensure that you have the Spark cluster set up and the `pyspark` and `joblib-spark` packages installed in your environment. You can install the required packages using pip if they are not already installed:\n", + "Do you have any questions related to the code sample?\n", "\n", - "```python\n", - "!pip install flaml pyspark joblib-spark\n", - "```\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", "\n", - "Here's a sample code snippet that demonstrates how to use FLAML with Spark for parallel training:\n", + "No, I don't have any questions related to the code sample.\n", "\n", - "```python\n", - "from flaml import AutoML\n", - "from pyspark.sql import SparkSession\n", - "from sklearn.datasets import load_digits\n", - "from joblibspark import register_spark\n", - "\n", - "# Initialize a Spark session\n", - "spark = SparkSession.builder \\\n", - " .master(\"local[*]\") \\\n", - " .appName(\"FLAML_Spark_Example\") \\\n", - " .getOrCreate()\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", - "# Register the joblib spark backend\n", - "register_spark() # This registers the backend for parallel processing\n", + "Great, let me know if you need any further assistance.\n", "\n", - "# Load sample data\n", - "X, y = load_digits(return_X_y=True)\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", "\n", - "# Initialize an AutoML instance\n", - "automl = AutoML()\n", + "Sure, will do. Thank you!\n", "\n", - "# Define the settings for the AutoML run\n", - "settings = {\n", - " \"time_budget\": 60, # Total running time in seconds\n", - " \"metric\": 'accuracy', # Primary metric for evaluation\n", - " \"task\": 'classification', # Task type\n", - " \"n_jobs\": -1, # Number of jobs to run in parallel (use -1 for all)\n", - " \"estimator_list\": ['lgbm', 'rf', 'xgboost'], # List of estimators to consider\n", - " \"log_file_name\": \"flaml_log.txt\", # Log file name\n", - "}\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", - "# Run the AutoML search with Spark backend\n", - "automl.fit(X_train=X, y_train=y, **settings)\n", + "You're welcome! Have a great day ahead!\n", "\n", - "# Output the best model and its performance\n", - "print(f\"Best ML model: {automl.model}\")\n", - "print(f\"Best ML model's accuracy: {automl.best_loss}\")\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", "\n", - "# Stop the Spark session\n", - "spark.stop()\n", - "```\n", + "You too, have a great day ahead!\n", "\n", - "The `register_spark()` function from `joblib-spark` is used to register the Spark backend with joblib, which is utilized for parallel training within FLAML. The `n_jobs=-1` parameter tells FLAML to use all available Spark executors for parallel training.\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", - "Please note that the actual process of setting up a Spark cluster can be complex and might involve additional steps such as configuring Spark workers, allocating resources, and more, which are beyond the scope of this code snippet.\n", + "Thank you! Goodbye!\n", "\n", - "If you encounter any issues or need to adjust configurations for your specific Spark setup, please refer to the Spark and FLAML documentation for more details.\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", "\n", - "When you run the code, ensure that your Spark cluster is properly configured and accessible from your Python environment. Adjust the `.master(\"local[*]\")` to point to your Spark master's URL if you are running a cluster that is not local.\n", + "Goodbye!\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Code_Reviewer\n", + "\u001b[0m\n", "\u001b[33mCode_Reviewer\u001b[0m (to chat_manager):\n", "\n", "TERMINATE\n", @@ -420,38 +398,38 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "2024-04-07 18:26:04,562 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `groupchat`.\u001b[0m\n" + "Trying to create collection.\n" ] }, { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Trying to create collection.\n" + "2024-08-14 06:59:09,583 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `groupchat`.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "2024-04-07 18:26:05,485 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 1 chunks.\u001b[0m\n", - "Number of requested results 3 is greater than number of elements in index 1, updating n_results = 1\n", - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + "2024-08-14 06:59:09,902 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n", + "2024-08-14 06:59:09,912 - autogen.agentchat.contrib.vectordb.chromadb - INFO - No content embedding is provided. Will use the VectorDB's embedding function to generate the content embedding.\u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", + "VectorDB returns doc_ids: [['bdfbc921', 'b2c1ec51', '0e57e70f']]\n", "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[32mAdding content of doc b2c1ec51 to context.\u001b[0m\n", "\u001b[33mBoss_Assistant\u001b[0m (to chat_manager):\n", "\n", "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", @@ -595,10 +573,90 @@ "```\n", "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", + "# Integrate - Spark\n", + "\n", + "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", + "\n", + "- Use Spark ML estimators for AutoML.\n", + "- Use Spark to run training in parallel spark jobs.\n", + "\n", + "## Spark ML Estimators\n", + "\n", + "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", + "\n", + "### Data\n", + "\n", + "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", + "\n", + "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", + "\n", + "This function also accepts optional arguments `index_col` and `default_index_type`.\n", + "\n", + "- `index_col` is the column name to use as the index, default is None.\n", + "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", + "\n", + "Here is an example code snippet for Spark Data:\n", + "\n", + "```python\n", + "import pandas as pd\n", + "from flaml.automl.spark.utils import to_pandas_on_spark\n", + "\n", + "# Creating a dictionary\n", + "data = {\n", + " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", + " \"Age_Years\": [20, 15, 10, 7, 25],\n", + " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", + "}\n", + "\n", + "# Creating a pandas DataFrame\n", + "dataframe = pd.DataFrame(data)\n", + "label = \"Price\"\n", + "\n", + "# Convert to pandas-on-spark dataframe\n", + "psdf = to_pandas_on_spark(dataframe)\n", + "```\n", + "\n", + "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", + "\n", + "Here is an example of how to use it:\n", + "\n", + "```python\n", + "from pyspark.ml.feature import VectorAssembler\n", + "\n", + "columns = psdf.columns\n", + "feature_cols = [col for col in columns if col != label]\n", + "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", + "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "```\n", + "\n", + "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", + "\n", + "### Estimators\n", + "\n", + "#### Model List\n", + "\n", + "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", + "\n", + "#### Usage\n", + "\n", + "First, prepare your data in the required format as described in the previous section.\n", + "\n", + "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", + "\n", + "Here is an example code snippet using SparkML models in AutoML:\n", + "\n", + "```python\n", + "import flaml\n", + "\n", + "# prepare your data in pandas-on-spark format as we previously mentioned\n", "\n", "\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[32mAdding content of doc b2c1ec51 to context.\u001b[0m\n", "\u001b[33mBoss_Assistant\u001b[0m (to chat_manager):\n", "\n", "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", @@ -742,58 +800,188 @@ "```\n", "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", + "# Integrate - Spark\n", "\n", + "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", "\n", + "- Use Spark ML estimators for AutoML.\n", + "- Use Spark to run training in parallel spark jobs.\n", "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", + "## Spark ML Estimators\n", + "\n", + "FLAML integrates estimators based on Spark ML models. These models are trained in parallel using Spark, so we called them Spark estimators. To use these models, you first need to organize your data in the required format.\n", + "\n", + "### Data\n", + "\n", + "For Spark estimators, AutoML only consumes Spark data. FLAML provides a convenient function `to_pandas_on_spark` in the `flaml.automl.spark.utils` module to convert your data into a pandas-on-spark (`pyspark.pandas`) dataframe/series, which Spark estimators require.\n", + "\n", + "This utility function takes data in the form of a `pandas.Dataframe` or `pyspark.sql.Dataframe` and converts it into a pandas-on-spark dataframe. It also takes `pandas.Series` or `pyspark.sql.Dataframe` and converts it into a [pandas-on-spark](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/index.html) series. If you pass in a `pyspark.pandas.Dataframe`, it will not make any changes.\n", + "\n", + "This function also accepts optional arguments `index_col` and `default_index_type`.\n", + "\n", + "- `index_col` is the column name to use as the index, default is None.\n", + "- `default_index_type` is the default index type, default is \"distributed-sequence\". More info about default index type could be found on Spark official [documentation](https://spark.apache.org/docs/latest/api/python/user_guide/pandas_on_spark/options.html#default-index-type)\n", + "\n", + "Here is an example code snippet for Spark Data:\n", "\n", "```python\n", - "from flaml.automl import AutoML\n", - "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "from pyspark.ml.feature import VectorAssembler\n", "import pandas as pd\n", + "from flaml.automl.spark.utils import to_pandas_on_spark\n", "\n", - "# Sample data in a dictionary\n", + "# Creating a dictionary\n", "data = {\n", " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", " \"Age_Years\": [20, 15, 10, 7, 25],\n", " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", "}\n", "\n", - "# Convert dictionary to pandas DataFrame\n", + "# Creating a pandas DataFrame\n", "dataframe = pd.DataFrame(data)\n", "label = \"Price\"\n", "\n", - "# Convert pandas DataFrame to pandas-on-spark DataFrame\n", + "# Convert to pandas-on-spark dataframe\n", "psdf = to_pandas_on_spark(dataframe)\n", + "```\n", + "\n", + "To use Spark ML models you need to format your data appropriately. Specifically, use [`VectorAssembler`](https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.ml.feature.VectorAssembler.html) to merge all feature columns into a single vector column.\n", + "\n", + "Here is an example of how to use it:\n", + "\n", + "```python\n", + "from pyspark.ml.feature import VectorAssembler\n", "\n", - "# Use VectorAssembler to merge feature columns into a single vector column\n", - "feature_cols = [col for col in psdf.columns if col != label]\n", + "columns = psdf.columns\n", + "feature_cols = [col for col in columns if col != label]\n", "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", - "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\", label]\n", + "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", + "```\n", + "\n", + "Later in conducting the experiment, use your pandas-on-spark data like non-spark data and pass them using `X_train, y_train` or `dataframe, label`.\n", + "\n", + "### Estimators\n", + "\n", + "#### Model List\n", + "\n", + "- `lgbm_spark`: The class for fine-tuning Spark version LightGBM models, using [SynapseML](https://microsoft.github.io/SynapseML/docs/features/lightgbm/about/) API.\n", + "\n", + "#### Usage\n", + "\n", + "First, prepare your data in the required format as described in the previous section.\n", + "\n", + "By including the models you intend to try in the `estimators_list` argument to `flaml.automl`, FLAML will start trying configurations for these models. If your input is Spark data, FLAML will also use estimators with the `_spark` postfix by default, even if you haven't specified them.\n", + "\n", + "Here is an example code snippet using SparkML models in AutoML:\n", + "\n", + "```python\n", + "import flaml\n", + "\n", + "# prepare your data in pandas-on-spark format as we previously mentioned\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", + "\n", + "```python\n", + "from flaml import AutoML\n", + "\n", + "# Assuming psdf is the pandas-on-spark dataframe and label is the name of the target variable\n", + "# Presuming that the data conversion and feature vectorization have been done as shown in the context\n", "\n", - "# Initialize AutoML instance\n", "automl = AutoML()\n", "\n", - "# AutoML settings\n", - "automl_settings = {\n", - " \"time_budget\": 30, # Total running time in seconds\n", - " \"metric\": \"r2\", # Evaluation metric\n", - " \"task\": \"regression\",\n", - " \"n_concurrent_trials\": 2, # Number of concurrent Spark jobs\n", - " \"use_spark\": True, # Enable Spark for parallel training\n", - " \"force_cancel\": True, # Force cancel Spark jobs if they exceed the time budget\n", - " \"estimator_list\": [\"lgbm_spark\"] # Optional: Specific estimator to use\n", + "settings = {\n", + " \"time_budget\": 120, # for example, set the time budget to 2 minutes\n", + " \"metric\": \"accuracy\", # assuming a classification problem, change to \"r2\" for regression\n", + " \"estimator_list\": [\"lgbm_spark\"], # specify the Spark estimator\n", + " \"task\": \"classification\", # assuming a classification problem, change to \"regression\" for regression\n", + " \"n_concurrent_trials\": 2, # number of concurrent Spark jobs\n", + " \"use_spark\": True, # enable distributed training using Spark\n", "}\n", "\n", - "# Run AutoML fit with pandas-on-spark dataframe\n", - "automl.fit(\n", - " dataframe=psdf,\n", - " label=label,\n", - " **automl_settings,\n", - ")\n", + "automl.fit(dataframe=psdf, label=label, **settings)\n", + "```\n", + "Please adjust the `metric`, `task`, and other settings according to your specific problem and requirements. This code snippet sets up FLAML with Spark for parallel training using the LightGBM Spark estimator, with two concurrent trials. Make sure your Spark environment is properly configured to run the distributed training.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n", + "\u001b[33mSenior_Python_Engineer\u001b[0m (to chat_manager):\n", + "\n", + "```python\n", + "from flaml import AutoML\n", + "\n", + "# Assuming psdf is the pandas-on-spark dataframe and label is the name of the target variable\n", + "# Presuming that the data conversion and feature vectorization have been done as shown in the context\n", + "\n", + "automl = AutoML()\n", + "\n", + "settings = {\n", + " \"time_budget\": 120, # for example, set the time budget to 2 minutes\n", + " \"metric\": \"accuracy\", # assuming a classification problem, change to \"r2\" for regression\n", + " \"estimator_list\": [\"lgbm_spark\"], # specify the Spark estimator\n", + " \"task\": \"classification\", # assuming a classification problem, change to \"regression\" for regression\n", + " \"n_concurrent_trials\": 2, # number of concurrent Spark jobs\n", + " \"use_spark\": True, # enable distributed training using Spark\n", + "}\n", + "\n", + "automl.fit(dataframe=psdf, label=label, **settings)\n", + "```\n", + "Please adjust the `metric`, `task`, and other settings according to your specific problem and requirements. This code snippet sets up FLAML with Spark for parallel training using the LightGBM Spark estimator, with two concurrent trials. Make sure your Spark environment is properly configured to run the distributed training.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Code_Reviewer\n", + "\u001b[0m\n", + "\u001b[33mCode_Reviewer\u001b[0m (to chat_manager):\n", + "\n", + "The provided code snippet is mostly correct and follows the guidelines provided in the context. However, there is one minor issue: if we are using the pandas-on-spark DataFrame `psdf`, the `fit` method should be called with `dataframe` and `label` arguments, not `X_train` and `y_train`.\n", + "\n", + "This is because, with FLAML and Spark integration, the `fit` method expects the entire data as a single pandas-on-spark DataFrame along with the name of the target variable as `label`, rather than being provided with separate feature and target data as it would expect with standard pandas DataFrames.\n", + "\n", + "Here's the correct code snippet reflecting this:\n", + "\n", + "```python\n", + "from flaml import AutoML\n", + "\n", + "# Assuming psdf is the pandas-on-spark dataframe and label is the name of the target variable\n", + "# Presuming that the data conversion and feature vectorization have been done as shown in the context\n", + "\n", + "automl = AutoML()\n", + "\n", + "settings = {\n", + " \"time_budget\": 120, # for example, set the time budget to 2 minutes\n", + " \"metric\": \"accuracy\", # assuming a classification problem, change to \"r2\" for regression\n", + " \"estimator_list\": [\"lgbm_spark\"], # specify the Spark estimator\n", + " \"task\": \"classification\", # assuming a classification problem, change to \"regression\" for regression\n", + " \"n_concurrent_trials\": 2, # number of concurrent Spark jobs\n", + " \"use_spark\": True, # enable distributed training using Spark\n", + "}\n", + "\n", + "# Use dataframe and label parameters to fit the model\n", + "automl.fit(dataframe=psdf, label=label, **settings)\n", "```\n", + "\n", + "Please ensure that your Spark cluster is correctly configured to support distributed training, and adjust the `metric`, `task`, and other settings as needed for your specific use case.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Boss_Assistant\n", + "\u001b[0m\n", + "\u001b[33mBoss_Assistant\u001b[0m (to chat_manager):\n", + "\n", + "Reply `TERMINATE` if the task is done.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", + "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", + "\n", "TERMINATE\n", "\n", "--------------------------------------------------------------------------------\n" @@ -816,7 +1004,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -828,14 +1016,26 @@ "How to use spark for parallel training in FLAML? Give me sample code.\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", "\u001b[32m***** Suggested function call: retrieve_content *****\u001b[0m\n", "Arguments: \n", - "{\"message\":\"using Apache Spark for parallel training in FLAML with sample code\"}\n", + "{\"message\":\"How to use spark for parallel training in FLAML? Give me sample code.\",\"n_results\":3}\n", "\u001b[32m*****************************************************\u001b[0m\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Boss\n", + "\u001b[0m\n", "\u001b[35m\n", ">>>>>>>> EXECUTING FUNCTION retrieve_content...\u001b[0m\n" ] @@ -844,16 +1044,19 @@ "name": "stderr", "output_type": "stream", "text": [ - "Number of requested results 3 is greater than number of elements in index 1, updating n_results = 1\n", - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" + "2024-08-14 07:09:05,717 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - \u001b[32mUse the existing collection `groupchat`.\u001b[0m\n", + "2024-08-14 07:09:05,845 - autogen.agentchat.contrib.retrieve_user_proxy_agent - INFO - Found 2 chunks.\u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "VectorDB returns doc_ids: [['bdfbc921']]\n", + "Trying to create collection.\n", + "VectorDB returns doc_ids: [['bdfbc921', 'b2c1ec51', '0e57e70f']]\n", "\u001b[32mAdding content of doc bdfbc921 to context.\u001b[0m\n", + "\u001b[32mAdding content of doc b2c1ec51 to context.\u001b[0m\n", + "\u001b[32mAdding content of doc 0e57e70f to context.\u001b[0m\n", "\u001b[33mBoss\u001b[0m (to chat_manager):\n", "\n", "\u001b[32m***** Response from calling function (retrieve_content) *****\u001b[0m\n", @@ -867,7 +1070,7 @@ "# your code\n", "```\n", "\n", - "User's question is: using Apache Spark for parallel training in FLAML with sample code\n", + "User's question is: How to use spark for parallel training in FLAML? Give me sample code.\n", "\n", "Context is: # Integrate - Spark\n", "\n", @@ -998,27 +1201,7 @@ "```\n", "\n", "[Link to notebook](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb) | [Open in colab](https://colab.research.google.com/github/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb)\n", - "\n", - "\n", - "\u001b[32m*************************************************************\u001b[0m\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mBoss\u001b[0m (to chat_manager):\n", - "\n", - "\u001b[32m***** Response from calling function (retrieve_content) *****\u001b[0m\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: using Apache Spark for parallel training in FLAML with sample code\n", - "\n", - "Context is: # Integrate - Spark\n", + "# Integrate - Spark\n", "\n", "FLAML has integrated Spark for distributed training. There are two main aspects of integration with Spark:\n", "\n", @@ -1094,7 +1277,6 @@ "import flaml\n", "\n", "# prepare your data in pandas-on-spark format as we previously mentioned\n", - "\n", "automl = flaml.AutoML()\n", "settings = {\n", " \"time_budget\": 30,\n", @@ -1152,84 +1334,87 @@ "\u001b[32m*************************************************************\u001b[0m\n", "\n", "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Product_Manager\n", + "\u001b[0m\n", "\u001b[33mProduct_Manager\u001b[0m (to chat_manager):\n", "\n", - "To use Apache Spark for parallel training in FLAML, you can follow these steps:\n", + "To use Spark for parallel training in FLAML, follow these steps:\n", "\n", - "1. Ensure your data is in the required pandas-on-spark format.\n", - "2. Use Spark ML estimators by including them in the `estimator_list`.\n", - "3. Set `use_spark` to `True` for parallel tuning.\n", + "## Steps:\n", "\n", - "Here's a sample code demonstrating how to use Spark for parallel training in FLAML:\n", + "1. **Prepare Your Data:**\n", + " Convert your data into a pandas-on-spark DataFrame using `to_pandas_on_spark` function.\n", + "\n", + "2. **Configure Spark Settings:**\n", + " Set the `use_spark` parameter to `True` to enable Spark for parallel training jobs.\n", + "\n", + "3. **Run the AutoML Experiment:**\n", + " Configure the AutoML settings and run the experiment.\n", + "\n", + "## Sample Code:\n", "\n", "```python\n", + "import pandas as pd\n", "import flaml\n", "from flaml.automl.spark.utils import to_pandas_on_spark\n", - "import pandas as pd\n", - "from pyspark.ml.feature import VectorAssembler\n", "\n", - "# Sample data in a pandas DataFrame\n", + "# Prepare your data\n", "data = {\n", " \"Square_Feet\": [800, 1200, 1800, 1500, 850],\n", " \"Age_Years\": [20, 15, 10, 7, 25],\n", " \"Price\": [100000, 200000, 300000, 240000, 120000],\n", "}\n", - "label = \"Price\"\n", "\n", - "# Creating a pandas DataFrame\n", "dataframe = pd.DataFrame(data)\n", + "label = \"Price\"\n", "\n", "# Convert to pandas-on-spark dataframe\n", "psdf = to_pandas_on_spark(dataframe)\n", "\n", - "# Prepare features using VectorAssembler\n", + "# Use VectorAssembler to format data for Spark ML\n", + "from pyspark.ml.feature import VectorAssembler\n", + "\n", "columns = psdf.columns\n", "feature_cols = [col for col in columns if col != label]\n", "featurizer = VectorAssembler(inputCols=feature_cols, outputCol=\"features\")\n", "psdf = featurizer.transform(psdf.to_spark(index_col=\"index\"))[\"index\", \"features\"]\n", "\n", - "# Initialize AutoML\n", + "# Configure AutoML settings\n", "automl = flaml.AutoML()\n", - "\n", - "# Configure settings for AutoML\n", - "settings = {\n", - " \"time_budget\": 30, # time budget in seconds\n", + "automl_settings = {\n", + " \"time_budget\": 30,\n", " \"metric\": \"r2\",\n", - " \"estimator_list\": [\"lgbm_spark\"], # using Spark ML estimators\n", " \"task\": \"regression\",\n", - " \"n_concurrent_trials\": 2, # number of parallel trials\n", - " \"use_spark\": True, # enable parallel training using Spark\n", - " \"force_cancel\": True, # force cancel Spark jobs if time_budget is exceeded\n", + " \"n_concurrent_trials\": 2,\n", + " \"use_spark\": True,\n", + " \"force_cancel\": True, # Optionally force cancel jobs that exceed time budget\n", "}\n", "\n", - "# Start the training\n", - "automl.fit(dataframe=psdf, label=label, **settings)\n", + "# Run the AutoML experiment\n", + "automl.fit(\n", + " dataframe=psdf,\n", + " label=label,\n", + " **automl_settings,\n", + ")\n", "```\n", "\n", - "In this code snippet:\n", - "- The `to_pandas_on_spark` function is used to convert the pandas DataFrame to a pandas-on-spark DataFrame.\n", - "- `VectorAssembler` is used to transform feature columns into a single vector column.\n", - "- The `AutoML` object is created, and settings are configured for the AutoML run, including setting `use_spark` to `True` for parallel training.\n", - "- The `fit` method is called to start the automated machine learning process.\n", + "This code demonstrates how to prepare your data, configure Spark settings for parallel training, and run the AutoML experiment using FLAML with Spark.\n", "\n", - "By using these settings, FLAML will train the models in parallel using Spark, which can accelerate the training process on large models and datasets.\n", + "You can find more information and examples in the [FLAML documentation](https://github.com/microsoft/FLAML/blob/main/notebook/integrate_spark.ipynb).\n", "\n", "TERMINATE\n", "\n", - "--------------------------------------------------------------------------------\n" + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Senior_Python_Engineer\n", + "\u001b[0m\n" ] } ], "source": [ "call_rag_chat()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1256,7 +1441,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.12.4" } }, "nbformat": 4, diff --git a/notebook/agentchat_memory_using_mem0.ipynb b/notebook/agentchat_memory_using_mem0.ipynb new file mode 100644 index 000000000000..d590002164b9 --- /dev/null +++ b/notebook/agentchat_memory_using_mem0.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "A3guIYLG6PnX" + }, + "source": [ + "# Agent with memory using Mem0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "di1opljX6sQT" + }, + "source": [ + "This notebook demonstrates an intelligent customer service chatbot system that combines:\n", + "\n", + "- PyAutoGen for conversational agents\n", + "- Mem0 for memory management\n", + "\n", + "[Mem0](https://www.mem0.ai/) provides a smart, self-improving memory layer for Large Language Models (LLMs), enabling developers to create personalized AI experiences that evolve with each user interaction. Refer [docs](https://docs.mem0.ai/overview) for more information.\n", + "\n", + "\n", + "Mem0 uses a hybrid database approach, combining vector, key-value, and graph databases to efficiently store and retrieve different types of information. It associates memories with unique identifiers, extracts relevant facts and preferences when storing, and uses a sophisticated retrieval process that considers relevance, importance, and recency.\n", + "\n", + "Key features of Mem0 include:\n", + "1. Comprehensive Memory Management: Easily manage long-term, short-term, semantic, and episodic memories for individual users, agents, and sessions through robust APIs.\n", + "2. Self-Improving Memory: An adaptive system that continuously learns from user interactions, refining its understanding over time.\n", + "3. Cross-Platform Consistency: Ensures a unified user experience across various AI platforms and applications.\n", + "4. Centralized Memory Control: Simplifies storing, updating, and deleting memories.\n", + "\n", + "This approach allows for maintaining context across sessions, adaptive personalization, and dynamic updates, making it more powerful than traditional Retrieval-Augmented Generation (RAG) approaches for creating context-aware AI applications.\n", + "\n", + "\n", + "**The implementation showcases how to initialize agents, manage conversation memory, and facilitate multi-agent conversations for enhanced problem-solving in customer support scenarios.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EAKSBJcLAf0h" + }, + "source": [ + "## Requirements\n", + "\n", + "````{=mdx}\n", + ":::info Requirements\n", + "Some extra dependencies are needed for this notebook, which can be installed via pip:\n", + "\n", + "```bash\n", + "pip install pyautogen mem0ai\n", + "```\n", + "\n", + "For more information, please refer to the [installation guide](/docs/installation/).\n", + ":::\n", + "````" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TA1-sGxD9-zZ" + }, + "source": [ + "## Get API Keys\n", + "\n", + "Please get `MEM0_API_KEY` from [Mem0 Platform](https://app.mem0.ai/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4nQWyJ-n9qOB", + "outputId": "de897f4f-32fd-4359-f021-c3510467b69c" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from mem0 import MemoryClient\n", + "\n", + "from autogen import ConversableAgent\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"your_api_key\"\n", + "os.environ[\"MEM0_API_KEY\"] = \"your_api_key\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6GAZAkGmAjsT" + }, + "source": [ + "## Initialize Agent and Memory\n", + "\n", + "The conversational agent is set up using the 'gpt-4o' model and a mem0 client. We'll utilize the client's methods for storing and accessing memories.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "xWSEHMKT9qLz" + }, + "outputs": [], + "source": [ + "agent = ConversableAgent(\n", + " \"chatbot\",\n", + " llm_config={\"config_list\": [{\"model\": \"gpt-4o\", \"api_key\": os.environ.get(\"OPENAI_API_KEY\")}]},\n", + " code_execution_config=False,\n", + " function_map=None,\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "\n", + "memory = MemoryClient()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c7vGQoMg_KhS" + }, + "source": [ + "Initialize a conversation history for a Best Buy customer service chatbot. It contains a list of message exchanges between the user and the assistant, structured as dictionaries with 'role' and 'content' keys. The entire conversation is then stored in memory using the `memory.add()` method, associated with the identifier \"customer_service_bot\"." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "b6ghSobs-mYo", + "outputId": "18a6e657-49ba-41cd-8e51-2e76cf668967" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'message': 'ok'}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation = [\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Hi, I'm Best Buy's chatbot!\\n\\nThanks for being a My Best Buy TotalTM member.\\n\\nWhat can I help you with?\",\n", + " },\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": 'Seeing horizontal lines on our tv. TV model: Sony - 77\" Class BRAVIA XR A80K OLED 4K UHD Smart Google TV',\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Thanks for being a My Best Buy Total™ member. I can connect you to an expert immediately - just one perk of your membership!\\n\\nSelect the button below when you're ready to chat.\",\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Good evening, thank you for choosing Best Buy, Fnu. My name is Lovely. I hope you are doing well. I'm sorry to hear that you're seeing horizontal lines on your TV.\\n\\nI'm absolutely committed to exploring all possible ways to assist you to fix this issue.\\n\\nTo ensure that we are on the right account, may I please have your email address registered with your Best Buy account?\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"dd@gmail.com\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Perfect! Thank you for providing all the details, surely you have made my job easier by doing this. I really appreciate it.\\n\\nI also want to take a moment to express our heartfelt appreciation for your trust and loyalty. Thank you for being an amazing customer of BestBuy Total.\\n\\nCould you please help me with the order number or product's details to check it quickly?\\n\\nSamsung - 49\\\" Odyssey OLED G9 (G95SC) DQHD 240Hz 0.03ms G-Sync Compatible Curved Smart Gaming Monitor - Silver - just to confirm this is the item, right?\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"Order number: 112217629\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Superb! Thank you for confirmation.\\n\\nThank you for your patience. After exploring all possible solutions, I can help you to arrange a home repair appointment for your device. Our Geek Squad experts will visit your home to inspect and fix your device.\\n\\nIt's great that you have a protection plan - rest assured, we've got your back! As a valued Total member, you can avail this service at a minimal service fee. This fee, applicable to all repairs, covers the cost of diagnosing the issue and any small parts needed for the repair. It's part of our 24-month free protection plan.\\n\\nPlease click here to review the service fee and plan coverage details -\\n\\nhttps://www.bestbuy.com/site/best-buy-membership/best-buy-protection/pcmcat1608643232014.c?id=pcmcat1608643232014#jl-servicefees\\n\\nFnu - just to confirm shall I proceed to schedule the appointment?\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"Yes please\"},\n", + " {\"role\": \"assistant\", \"content\": \"When should I schedule the appointment?\"},\n", + " {\"role\": \"user\", \"content\": \"Schedule it for tomorrow please\"},\n", + "]\n", + "\n", + "memory.add(messages=conversation, user_id=\"customer_service_bot\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hlxxncxkAvck" + }, + "source": [ + "## Agent Inference\n", + "\n", + "We ask a question to the agent, utilizing mem0 to retrieve relevant memories. The agent then formulates a response based on both the question and the retrieved contextual information." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "wYdDkuKc-Xc8", + "outputId": "b50f55d3-a280-4c0d-82e5-43fc0589109b" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sure, your order number is 112217629.\n" + ] + } + ], + "source": [ + "data = \"I forgot the order numnber, can you quickly tell me?\"\n", + "\n", + "relevant_memories = memory.search(data, user_id=\"customer_service_bot\")\n", + "flatten_relevant_memories = \"\\n\".join([m[\"memory\"] for m in relevant_memories])\n", + "\n", + "prompt = f\"\"\"Answer the user question considering the memories. Keep answers clear and concise.\n", + "Memories:\n", + "{flatten_relevant_memories}\n", + "\\n\\n\n", + "Question: {data}\n", + "\"\"\"\n", + "\n", + "reply = agent.generate_reply(messages=[{\"content\": prompt, \"role\": \"user\"}])\n", + "print(reply)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sryX0gfdBGGD" + }, + "source": [ + "## Multi Agent Conversation\n", + "\n", + "Initialize two AI agents: a \"manager\" for resolving customer issues and a \"customer_bot\" for gathering information on customer problems, both using GPT-4. It then retrieves relevant memories for a given question, combining them with the question into a prompt. This prompt can be used by either the manager or customer_bot to generate a contextually informed response." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "Vq5BFL2l-XZl" + }, + "outputs": [], + "source": [ + "manager = ConversableAgent(\n", + " \"manager\",\n", + " system_message=\"You are a manager who helps in resolving customer issues.\",\n", + " llm_config={\"config_list\": [{\"model\": \"gpt-4\", \"temperature\": 0, \"api_key\": os.environ.get(\"OPENAI_API_KEY\")}]},\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "\n", + "customer_bot = ConversableAgent(\n", + " \"customer_bot\",\n", + " system_message=\"You are a customer service bot who gathers information on issues customers are facing. Keep answers clear and concise.\",\n", + " llm_config={\"config_list\": [{\"model\": \"gpt-4\", \"temperature\": 0, \"api_key\": os.environ.get(\"OPENAI_API_KEY\")}]},\n", + " human_input_mode=\"NEVER\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "iX4ehmc6Fbib" + }, + "outputs": [], + "source": [ + "data = \"When is the appointment?\"\n", + "\n", + "relevant_memories = memory.search(data, user_id=\"customer_service_bot\")\n", + "flatten_relevant_memories = \"\\n\".join([m[\"memory\"] for m in relevant_memories])\n", + "\n", + "prompt = f\"\"\"\n", + "Context:\n", + "{flatten_relevant_memories}\n", + "\\n\\n\n", + "Question: {data}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "D3paRZWvCIzt", + "outputId": "15eadb7c-5973-44f1-de43-5e6cdebe88de" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "manager (to customer_bot):\n", + "\n", + "\n", + "Context:\n", + "Scheduled an appointment for a home repair for tomorrow\n", + "Order number is 112217629\n", + "TV model is Sony - 77\" Class BRAVIA XR A80K OLED 4K UHD Smart Google TV\n", + "User's email address is dd@gmail.com\n", + "\n", + "\n", + "\n", + "Question: When is the appointment?\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "customer_bot (to manager):\n", + "\n", + "The appointment is scheduled for tomorrow.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "result = manager.send(prompt, customer_bot, request_reply=True)" + ] + } + ], + "metadata": { + "front_matter": { + "description": "Use Mem0 to create agents with memory.", + "tags": [ + "memory" + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebook/agentchat_microsoft_fabric.ipynb b/notebook/agentchat_microsoft_fabric.ipynb index e4c2a7119cf9..1aaee58f0921 100644 --- a/notebook/agentchat_microsoft_fabric.ipynb +++ b/notebook/agentchat_microsoft_fabric.ipynb @@ -19,18 +19,16 @@ "[Microsoft Fabric](https://learn.microsoft.com/en-us/fabric/get-started/microsoft-fabric-overview) is an all-in-one analytics solution for enterprises that covers everything from data movement to data science, Real-Time Analytics, and business intelligence. It offers a comprehensive suite of services, including data lake, data engineering, and data integration, all in one place. Its pre-built AI models include GPT-x models such as `gpt-4o`, `gpt-4-turbo`, `gpt-4`, `gpt-4-8k`, `gpt-4-32k`, `gpt-35-turbo`, `gpt-35-turbo-16k` and `gpt-35-turbo-instruct`, etc. It's important to note that the Azure Open AI service is not supported on trial SKUs and only paid SKUs (F64 or higher, or P1 or higher) are supported.\n", "\n", "In this notebook, we demonstrate several examples:\n", + "- 0. How to access pre-built LLM endpoints with AutoGen in Microsoft Fabric.\n", "- 1. How to use `AssistantAgent` and `UserProxyAgent` to write code and execute the code.\n", - "- 2. How to use `RetrieveAssistantAgent` and `RetrieveUserProxyAgent` to do Retrieval Augmented Generation (RAG) for QA and Code Generation.\n", + "- 2. How to use `AssistantAgent` and `RetrieveUserProxyAgent` to do Retrieval Augmented Generation (RAG) for QA and Code Generation.\n", "- 3. How to use `MultimodalConversableAgent` to chat with images.\n", "\n", - "### Requirements\n", + "#### Requirements\n", "\n", - "AutoGen requires `Python>=3.8`. To run this notebook example, please install:\n", - "```bash\n", - "pip install \"pyautogen[retrievechat,lmm]>=0.2.28\"\n", - "```\n", + "AutoGen requires `Python>=3.8`.\n", "\n", - "Also, this notebook depends on Microsoft Fabric pre-built LLM endpoints. Running it elsewhere may encounter errors." + "Also, this notebook depends on Microsoft Fabric pre-built LLM endpoints and Fabric runtime 1.2+. Running it elsewhere may encounter errors." ] }, { @@ -44,22 +42,167 @@ } }, "source": [ - "### Install AutoGen" + "### Example 0" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "#### Work with openai<1\n", + "\n", + "AutoGen can work with openai<1 in Microsoft Fabric. To access pre-built LLM endpoints with AutoGen, you can follow below example.\n", + "\n", + "This example can run in Fabric runtime 1.2+." ] }, { "cell_type": "code", "execution_count": null, - "id": "2", - "metadata": {}, + "id": "3", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, "outputs": [], "source": [ - "%pip install \"pyautogen[retrievechat,lmm]>=0.2.28\" -q" + "# pyautogen<=0.1.14 supports openai<1\n", + "%pip install \"pyautogen==0.1.14\" \"openai==0.28.1\" -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [], + "source": [ + "config_list = [\n", + " {\n", + " \"model\": \"gpt-4o\",\n", + " },\n", + "]\n", + "\n", + "# Set temperature, timeout and other LLM configurations\n", + "llm_config = {\n", + " \"config_list\": config_list,\n", + " \"temperature\": 0,\n", + " \"timeout\": 600,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [ + { + "data": { + "application/vnd.livy.statement-meta+json": { + "execution_finish_time": "2024-09-04T11:10:49.8449248Z", + "execution_start_time": "2024-09-04T11:09:05.6935167Z", + "livy_statement_state": "available", + "normalized_state": "finished", + "parent_msg_id": "5dacfdd5-af6c-4acd-9ece-60f9e0758174", + "queued_time": "2024-09-04T11:08:29.1314923Z", + "session_id": "d0eed0e3-107e-4be2-8da7-7d6026d85e62", + "session_start_time": null, + "spark_pool": null, + "state": "finished", + "statement_id": 11, + "statement_ids": [ + 11 + ] + }, + "text/plain": [ + "StatementMeta(, d0eed0e3-107e-4be2-8da7-7d6026d85e62, 11, Finished, Available, Finished)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-09-04 11:10:05.592463: I tensorflow/core/util/port.cc:113] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", + "2024-09-04 11:10:11.425719: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: SSE4.1 SSE4.2 AVX AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33muser\u001b[0m (to gpt-4o):\n", + "\n", + "Tell me a quick joke.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mgpt-4o\u001b[0m (to user):\n", + "\n", + "Why don't scientists trust atoms?\n", + "\n", + "Because they make up everything!\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "source": [ + "import autogen\n", + "\n", + "agent = autogen.agentchat.ConversableAgent(\n", + " name=config_list[0][\"model\"], llm_config=llm_config, max_consecutive_auto_reply=1, human_input_mode=\"NEVER\"\n", + ")\n", + "userproxy = autogen.agentchat.ConversableAgent(\n", + " name=\"user\",\n", + " max_consecutive_auto_reply=0,\n", + " llm_config=False,\n", + " default_auto_reply=\"TERMINATE\",\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "userproxy.initiate_chat(recipient=agent, message=\"Tell me a quick joke.\")" ] }, { "cell_type": "markdown", - "id": "3", + "id": "6", "metadata": { "nteract": { "transient": { @@ -68,13 +211,28 @@ } }, "source": [ - "### Set up config_list and llm_config" + "#### Work with openai>=1\n", + "\n", + "AutoGen can work with openai>=1 in Microsoft Fabric. To access pre-built LLM endpoints with AutoGen, you can follow below example.\n", + "\n", + "This example and below examples can only run in Fabric runtime 1.3+." ] }, { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# pyautogen>0.1.14 supports openai>=1\n", + "%pip install \"pyautogen>0.2\" \"openai>1\" -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", "metadata": { "jupyter": { "outputs_hidden": false, @@ -90,22 +248,23 @@ { "data": { "application/vnd.livy.statement-meta+json": { - "execution_finish_time": "2024-06-07T15:24:20.5752101Z", - "execution_start_time": "2024-06-07T15:24:03.7868628Z", + "execution_finish_time": "2024-09-04T11:21:01.4134396Z", + "execution_start_time": "2024-09-04T11:21:01.0206532Z", "livy_statement_state": "available", - "parent_msg_id": "bf8925aa-a2a2-4686-9388-3ec1eb12c5d7", - "queued_time": "2024-06-07T15:23:08.5880731Z", - "session_id": "1d5e9aec-2019-408c-a19a-5db9fb175ae2", + "normalized_state": "finished", + "parent_msg_id": "b01282de-e79f-4a3e-9205-73fa8ab87599", + "queued_time": "2024-09-04T11:21:00.5084Z", + "session_id": "d0eed0e3-107e-4be2-8da7-7d6026d85e62", "session_start_time": null, "spark_pool": null, "state": "finished", - "statement_id": 9, + "statement_id": 38, "statement_ids": [ - 9 + 38 ] }, "text/plain": [ - "StatementMeta(, 1d5e9aec-2019-408c-a19a-5db9fb175ae2, 9, Finished, Available)" + "StatementMeta(, d0eed0e3-107e-4be2-8da7-7d6026d85e62, 38, Finished, Available, Finished)" ] }, "metadata": {}, @@ -120,7 +279,7 @@ "\n", "import autogen\n", "\n", - "http_client = get_openai_httpx_sync_client()\n", + "http_client = get_openai_httpx_sync_client() # http_client is needed for openai>1\n", "http_client.__deepcopy__ = types.MethodType(\n", " lambda self, memo: self, http_client\n", ") # https://microsoft.github.io/autogen/docs/topics/llm_configuration#adding-http-client-in-llm_config-for-proxy\n", @@ -129,8 +288,8 @@ " {\n", " \"model\": \"gpt-4o\",\n", " \"http_client\": http_client,\n", - " \"api_type\": \"azure\",\n", " \"api_version\": \"2024-02-01\",\n", + " \"api_type\": \"azure\",\n", " },\n", "]\n", "\n", @@ -141,9 +300,95 @@ "}" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [ + { + "data": { + "application/vnd.livy.statement-meta+json": { + "execution_finish_time": "2024-09-04T11:21:05.2491512Z", + "execution_start_time": "2024-09-04T11:21:03.4015759Z", + "livy_statement_state": "available", + "normalized_state": "finished", + "parent_msg_id": "4f4e94fa-4626-445d-86d0-b8fa3e3ae0b5", + "queued_time": "2024-09-04T11:21:02.9666054Z", + "session_id": "d0eed0e3-107e-4be2-8da7-7d6026d85e62", + "session_start_time": null, + "spark_pool": null, + "state": "finished", + "statement_id": 39, + "statement_ids": [ + 39 + ] + }, + "text/plain": [ + "StatementMeta(, d0eed0e3-107e-4be2-8da7-7d6026d85e62, 39, Finished, Available, Finished)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33muser\u001b[0m (to gpt-4o):\n", + "\n", + "Tell me a joke about openai.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mgpt-4o\u001b[0m (to user):\n", + "\n", + "Why did the AI go to school?\n", + "\n", + "Because it wanted to improve its \"neural\" network!\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "text/plain": [ + "ChatResult(chat_id=None, chat_history=[{'content': 'Tell me a joke about openai.', 'role': 'assistant', 'name': 'user'}, {'content': 'Why did the AI go to school?\\n\\nBecause it wanted to improve its \"neural\" network!', 'role': 'user', 'name': 'gpt-4o'}], summary='Why did the AI go to school?\\n\\nBecause it wanted to improve its \"neural\" network!', cost={'usage_including_cached_inference': {'total_cost': 0.000435, 'gpt-4o-2024-05-13': {'cost': 0.000435, 'prompt_tokens': 27, 'completion_tokens': 20, 'total_tokens': 47}}, 'usage_excluding_cached_inference': {'total_cost': 0.000435, 'gpt-4o-2024-05-13': {'cost': 0.000435, 'prompt_tokens': 27, 'completion_tokens': 20, 'total_tokens': 47}}}, human_input=[])" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import autogen\n", + "\n", + "agent = autogen.agentchat.ConversableAgent(\n", + " name=config_list[0][\"model\"], llm_config=llm_config, max_consecutive_auto_reply=1, human_input_mode=\"NEVER\"\n", + ")\n", + "userproxy = autogen.agentchat.ConversableAgent(\n", + " name=\"user\",\n", + " max_consecutive_auto_reply=0,\n", + " llm_config=False,\n", + " default_auto_reply=\"TERMINATE\",\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "userproxy.initiate_chat(recipient=agent, message=\"Tell me a joke about openai.\")" + ] + }, { "cell_type": "markdown", - "id": "5", + "id": "10", "metadata": { "nteract": { "transient": { @@ -159,7 +404,71 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "11", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [], + "source": [ + "%pip install \"pyautogen[retrievechat,lmm]>=0.2.28\" -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [], + "source": [ + "import types\n", + "\n", + "from synapse.ml.fabric.credentials import get_openai_httpx_sync_client\n", + "\n", + "import autogen\n", + "\n", + "http_client = get_openai_httpx_sync_client() # http_client is needed for openai>1\n", + "http_client.__deepcopy__ = types.MethodType(\n", + " lambda self, memo: self, http_client\n", + ") # https://microsoft.github.io/autogen/docs/topics/llm_configuration#adding-http-client-in-llm_config-for-proxy\n", + "\n", + "config_list = [\n", + " {\n", + " \"model\": \"gpt-4o\",\n", + " \"http_client\": http_client,\n", + " \"api_version\": \"2024-02-01\",\n", + " \"api_type\": \"azure\",\n", + " },\n", + "]\n", + "\n", + "# Set temperature, timeout and other LLM configurations\n", + "llm_config = {\n", + " \"config_list\": config_list,\n", + " \"temperature\": 0,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "jupyter": { "outputs_hidden": false, @@ -300,6 +609,8 @@ } ], "source": [ + "import autogen\n", + "\n", "# create an AssistantAgent instance named \"assistant\"\n", "assistant = autogen.AssistantAgent(\n", " name=\"assistant\",\n", @@ -333,7 +644,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "14", "metadata": { "jupyter": { "outputs_hidden": false, @@ -385,7 +696,7 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "15", "metadata": { "nteract": { "transient": { @@ -395,7 +706,7 @@ }, "source": [ "### Example 2\n", - "How to use `RetrieveAssistantAgent` and `RetrieveUserProxyAgent` to do Retrieval Augmented Generation (RAG) for QA and Code Generation.\n", + "How to use `AssistantAgent` and `RetrieveUserProxyAgent` to do Retrieval Augmented Generation (RAG) for QA and Code Generation.\n", "\n", "Check out this [blog](https://microsoft.github.io/autogen/blog/2023/10/18/RetrieveChat) for more details." ] @@ -403,7 +714,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "16", "metadata": { "jupyter": { "outputs_hidden": false, @@ -459,7 +770,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "17", "metadata": { "jupyter": { "outputs_hidden": false, @@ -473,11 +784,11 @@ }, "outputs": [], "source": [ - "from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent\n", + "from autogen import AssistantAgent\n", "from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent\n", "\n", - "# 1. create an RetrieveAssistantAgent instance named \"assistant\"\n", - "assistant = RetrieveAssistantAgent(\n", + "# 1. create an AssistantAgent instance named \"assistant\"\n", + "assistant = AssistantAgent(\n", " name=\"assistant\",\n", " system_message=\"You are a helpful assistant.\",\n", " llm_config=llm_config,\n", @@ -504,7 +815,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "18", "metadata": { "nteract": { "transient": { @@ -557,7 +868,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "19", "metadata": { "jupyter": { "outputs_hidden": false, @@ -902,7 +1213,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "20", "metadata": { "jupyter": { "outputs_hidden": false, @@ -954,7 +1265,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "21", "metadata": { "nteract": { "transient": { @@ -1022,7 +1333,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "22", "metadata": { "jupyter": { "outputs_hidden": false, @@ -1753,7 +2064,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "23", "metadata": { "jupyter": { "outputs_hidden": false, @@ -1805,7 +2116,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "24", "metadata": { "nteract": { "transient": { @@ -1820,7 +2131,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "25", "metadata": { "jupyter": { "outputs_hidden": false, @@ -2906,7 +3217,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "26", "metadata": { "nteract": { "transient": { @@ -2923,7 +3234,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "27", "metadata": { "nteract": { "transient": { @@ -2938,7 +3249,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "28", "metadata": { "jupyter": { "outputs_hidden": false, @@ -3025,7 +3336,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "29", "metadata": { "jupyter": { "outputs_hidden": false, @@ -3081,7 +3392,8 @@ "name": "synapse_pyspark" }, "kernelspec": { - "display_name": "synapse_pyspark", + "display_name": "Synapse PySpark", + "language": "Python", "name": "synapse_pyspark" }, "language_info": { diff --git a/notebook/agentchat_nested_chats_chess_altmodels.ipynb b/notebook/agentchat_nested_chats_chess_altmodels.ipynb new file mode 100644 index 000000000000..69d3edbcfb50 --- /dev/null +++ b/notebook/agentchat_nested_chats_chess_altmodels.ipynb @@ -0,0 +1,584 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Conversational Chess using non-OpenAI clients\n", + "\n", + "This notebook provides tips for using non-OpenAI models when using functions/tools.\n", + "\n", + "The code is based on [this notebook](/docs/notebooks/agentchat_nested_chats_chess),\n", + "which provides a detailed look at nested chats for tool use. Please refer to that\n", + "notebook for more on nested chats as this will be concentrated on tweaks to\n", + "improve performance with non-OpenAI models.\n", + "\n", + "The notebook represents a chess game between two players with a nested chat to\n", + "determine the available moves and select a move to make.\n", + "\n", + "This game contains a couple of functions/tools that the LLMs must use correctly by the\n", + "LLMs:\n", + "- `get_legal_moves` to get a list of current legal moves.\n", + "- `make_move` to make a move.\n", + "\n", + "Two agents will be used to represent the white and black players, each associated with\n", + "a different LLM cloud provider and model:\n", + "- Anthropic's Sonnet 3.5 will be Player_White\n", + "- Mistral's Mixtral 8x7B (using Together.AI) will be Player_Black\n", + "\n", + "As this involves function calling, we use larger, more capable, models from these providers.\n", + "\n", + "The nested chat will be supported be a board proxy agent who is set up to execute\n", + "the tools and manage the game.\n", + "\n", + "Tips to improve performance with these non-OpenAI models will be noted throughout **in bold**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "First, you need to install the `pyautogen` and `chess` packages to use AutoGen. We'll include Anthropic and Together.AI libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "! pip install -qqq pyautogen[anthropic,together] chess" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up LLMs\n", + "\n", + "We'll use the Anthropic (`api_type` is `anthropic`) and Together.AI (`api_type` is `together`) client classes, with their respective models, which both support function calling." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import chess\n", + "import chess.svg\n", + "from IPython.display import display\n", + "from typing_extensions import Annotated\n", + "\n", + "from autogen import ConversableAgent, register_function\n", + "\n", + "# Let's set our two player configs, specifying clients and models\n", + "\n", + "# Anthropic's Sonnet for player white\n", + "player_white_config_list = [\n", + " {\n", + " \"api_type\": \"anthropic\",\n", + " \"model\": \"claude-3-5-sonnet-20240620\",\n", + " \"api_key\": os.getenv(\"ANTHROPIC_API_KEY\"),\n", + " \"cache_seed\": None,\n", + " },\n", + "]\n", + "\n", + "# Mistral's Mixtral 8x7B for player black (through Together.AI)\n", + "player_black_config_list = [\n", + " {\n", + " \"api_type\": \"together\",\n", + " \"model\": \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n", + " \"api_key\": os.environ.get(\"TOGETHER_API_KEY\"),\n", + " \"cache_seed\": None,\n", + " },\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll setup game variables and the two functions for getting the available moves and then making a move." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize the board.\n", + "board = chess.Board()\n", + "\n", + "# Keep track of whether a move has been made.\n", + "made_move = False\n", + "\n", + "\n", + "def get_legal_moves() -> Annotated[\n", + " str,\n", + " \"Call this tool to list of all legal chess moves on the board, output is a list in UCI format, e.g. e2e4,e7e5,e7e8q.\",\n", + "]:\n", + " return \"Possible moves are: \" + \",\".join([str(move) for move in board.legal_moves])\n", + "\n", + "\n", + "def make_move(\n", + " move: Annotated[\n", + " str,\n", + " \"Call this tool to make a move after you have the list of legal moves and want to make a move. Takes UCI format, e.g. e2e4 or e7e5 or e7e8q.\",\n", + " ]\n", + ") -> Annotated[str, \"Result of the move.\"]:\n", + " move = chess.Move.from_uci(move)\n", + " board.push_uci(str(move))\n", + " global made_move\n", + " made_move = True\n", + " # Display the board.\n", + " display(\n", + " chess.svg.board(board, arrows=[(move.from_square, move.to_square)], fill={move.from_square: \"gray\"}, size=200)\n", + " )\n", + " # Get the piece name.\n", + " piece = board.piece_at(move.to_square)\n", + " piece_symbol = piece.unicode_symbol()\n", + " piece_name = (\n", + " chess.piece_name(piece.piece_type).capitalize()\n", + " if piece_symbol.isupper()\n", + " else chess.piece_name(piece.piece_type)\n", + " )\n", + " return f\"Moved {piece_name} ({piece_symbol}) from {chess.SQUARE_NAMES[move.from_square]} to {chess.SQUARE_NAMES[move.to_square]}.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating agents\n", + "\n", + "Our main player agents are created next, with a few tweaks to help our models play:\n", + "\n", + "- Explicitly **telling agents their names** (as the name field isn't sent to the LLM).\n", + "- Providing simple instructions on the **order of functions** (not all models will need it).\n", + "- Asking the LLM to **include their name in the response** so the message content will include their names, helping the LLM understand who has made which moves.\n", + "- Ensure **no spaces are in the agent names** so that their name is distinguishable in the conversation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "player_white = ConversableAgent(\n", + " name=\"Player_White\",\n", + " system_message=\"You are a chess player and you play as white, your name is 'Player_White'. \"\n", + " \"First call the function get_legal_moves() to get list of legal moves. \"\n", + " \"Then call the function make_move(move) to make a move. \"\n", + " \"Then tell Player_Black you have made your move and it is their turn. \"\n", + " \"Make sure you tell Player_Black you are Player_White.\",\n", + " llm_config={\"config_list\": player_white_config_list, \"cache_seed\": None},\n", + ")\n", + "\n", + "player_black = ConversableAgent(\n", + " name=\"Player_Black\",\n", + " system_message=\"You are a chess player and you play as black, your name is 'Player_Black'. \"\n", + " \"First call the function get_legal_moves() to get list of legal moves. \"\n", + " \"Then call the function make_move(move) to make a move. \"\n", + " \"Then tell Player_White you have made your move and it is their turn. \"\n", + " \"Make sure you tell Player_White you are Player_Black.\",\n", + " llm_config={\"config_list\": player_black_config_list, \"cache_seed\": None},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we create a proxy agent that will be used to move the pieces on the board." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Check if the player has made a move, and reset the flag if move is made.\n", + "def check_made_move(msg):\n", + " global made_move\n", + " if made_move:\n", + " made_move = False\n", + " return True\n", + " else:\n", + " return False\n", + "\n", + "\n", + "board_proxy = ConversableAgent(\n", + " name=\"Board_Proxy\",\n", + " llm_config=False,\n", + " # The board proxy will only terminate the conversation if the player has made a move.\n", + " is_termination_msg=check_made_move,\n", + " # The auto reply message is set to keep the player agent retrying until a move is made.\n", + " default_auto_reply=\"Please make a move.\",\n", + " human_input_mode=\"NEVER\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our functions are then assigned to the agents so they can be passed to the LLM to choose from.\n", + "\n", + "We have tweaked the descriptions to provide **more guidance on when** to use it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "register_function(\n", + " make_move,\n", + " caller=player_white,\n", + " executor=board_proxy,\n", + " name=\"make_move\",\n", + " description=\"Call this tool to make a move after you have the list of legal moves.\",\n", + ")\n", + "\n", + "register_function(\n", + " get_legal_moves,\n", + " caller=player_white,\n", + " executor=board_proxy,\n", + " name=\"get_legal_moves\",\n", + " description=\"Call this to get a legal moves before making a move.\",\n", + ")\n", + "\n", + "register_function(\n", + " make_move,\n", + " caller=player_black,\n", + " executor=board_proxy,\n", + " name=\"make_move\",\n", + " description=\"Call this tool to make a move after you have the list of legal moves.\",\n", + ")\n", + "\n", + "register_function(\n", + " get_legal_moves,\n", + " caller=player_black,\n", + " executor=board_proxy,\n", + " name=\"get_legal_moves\",\n", + " description=\"Call this to get a legal moves before making a move.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Almost there, we now create nested chats between players and the board proxy agent to work out the available moves and make the move." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "player_white.register_nested_chats(\n", + " trigger=player_black,\n", + " chat_queue=[\n", + " {\n", + " # The initial message is the one received by the player agent from\n", + " # the other player agent.\n", + " \"sender\": board_proxy,\n", + " \"recipient\": player_white,\n", + " # The final message is sent to the player agent.\n", + " \"summary_method\": \"last_msg\",\n", + " }\n", + " ],\n", + ")\n", + "\n", + "player_black.register_nested_chats(\n", + " trigger=player_white,\n", + " chat_queue=[\n", + " {\n", + " # The initial message is the one received by the player agent from\n", + " # the other player agent.\n", + " \"sender\": board_proxy,\n", + " \"recipient\": player_black,\n", + " # The final message is sent to the player agent.\n", + " \"summary_method\": \"last_msg\",\n", + " }\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Playing the game\n", + "\n", + "Now the game can begin!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mPlayer_Black\u001b[0m (to Player_White):\n", + "\n", + "Let's play chess! Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[34mStarting a new chat....\u001b[0m\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_White):\n", + "\n", + "Let's play chess! Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_White\u001b[0m (to Board_Proxy):\n", + "\n", + "Certainly! I'd be happy to play chess with you. As White, I'll make the first move. Let me start by checking the legal moves available to me.\n", + "\u001b[32m***** Suggested tool call (toolu_015sLMucefMVqS5ZNyWVGjgu): get_legal_moves *****\u001b[0m\n", + "Arguments: \n", + "{}\n", + "\u001b[32m*********************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION get_legal_moves...\u001b[0m\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_White):\n", + "\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_White):\n", + "\n", + "\u001b[32m***** Response from calling tool (toolu_015sLMucefMVqS5ZNyWVGjgu) *****\u001b[0m\n", + "Possible moves are: g1h3,g1f3,b1c3,b1a3,h2h3,g2g3,f2f3,e2e3,d2d3,c2c3,b2b3,a2a3,h2h4,g2g4,f2f4,e2e4,d2d4,c2c4,b2b4,a2a4\n", + "\u001b[32m***********************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_White\u001b[0m (to Board_Proxy):\n", + "\n", + "Thank you for initiating a game of chess! As Player_White, I'll make the first move. After analyzing the legal moves, I've decided to make a classic opening move.\n", + "\u001b[32m***** Suggested tool call (toolu_01VjmBhHcGw5RTRKYC4Y5MeV): make_move *****\u001b[0m\n", + "Arguments: \n", + "{\"move\": \"e2e4\"}\n", + "\u001b[32m***************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION make_move...\u001b[0m\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r n b q k b n r\n",
+       "p p p p p p p p\n",
+       ". . . . . . . .\n",
+       ". . . . . . . .\n",
+       ". . . . P . . .\n",
+       ". . . . . . . .\n",
+       "P P P P . P P P\n",
+       "R N B Q K B N R
" + ], + "text/plain": [ + "'
r n b q k b n r\\np p p p p p p p\\n. . . . . . . .\\n. . . . . . . .\\n. . . . P . . .\\n. . . . . . . .\\nP P P P . P P P\\nR N B Q K B N R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoard_Proxy\u001b[0m (to Player_White):\n", + "\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_White):\n", + "\n", + "\u001b[32m***** Response from calling tool (toolu_01VjmBhHcGw5RTRKYC4Y5MeV) *****\u001b[0m\n", + "Moved pawn (♙) from e2 to e4.\n", + "\u001b[32m***********************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_White\u001b[0m (to Board_Proxy):\n", + "\n", + "Hello, Player_Black! I'm Player_White, and I've just made my move. I've chosen to play the classic opening move e2e4, moving my king's pawn forward two squares. This opens up lines for both my queen and king's bishop, and stakes a claim to the center of the board. It's now your turn to make a move. Good luck!\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer_White\u001b[0m (to Player_Black):\n", + "\n", + "Hello, Player_Black! I'm Player_White, and I've just made my move. I've chosen to play the classic opening move e2e4, moving my king's pawn forward two squares. This opens up lines for both my queen and king's bishop, and stakes a claim to the center of the board. It's now your turn to make a move. Good luck!\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[34mStarting a new chat....\u001b[0m\n", + "\u001b[34m\n", + "********************************************************************************\u001b[0m\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_Black):\n", + "\n", + "Hello, Player_Black! I'm Player_White, and I've just made my move. I've chosen to play the classic opening move e2e4, moving my king's pawn forward two squares. This opens up lines for both my queen and king's bishop, and stakes a claim to the center of the board. It's now your turn to make a move. Good luck!\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_Black\u001b[0m (to Board_Proxy):\n", + "\n", + "\u001b[32m***** Suggested tool call (call_z6jagiqn59m784w1n0zhmiop): get_legal_moves *****\u001b[0m\n", + "Arguments: \n", + "{}\n", + "\u001b[32m********************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION get_legal_moves...\u001b[0m\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_Black):\n", + "\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_Black):\n", + "\n", + "\u001b[32m***** Response from calling tool (call_z6jagiqn59m784w1n0zhmiop) *****\u001b[0m\n", + "Possible moves are: g8h6,g8f6,b8c6,b8a6,h7h6,g7g6,f7f6,e7e6,d7d6,c7c6,b7b6,a7a6,h7h5,g7g5,f7f5,e7e5,d7d5,c7c5,b7b5,a7a5\n", + "\u001b[32m**********************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_Black\u001b[0m (to Board_Proxy):\n", + "\n", + "\u001b[32m***** Suggested tool call (call_59t20pl0ab68z4xx2workgbc): make_move *****\u001b[0m\n", + "Arguments: \n", + "{\"move\":\"g8h6\"}\n", + "\u001b[32m**************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[35m\n", + ">>>>>>>> EXECUTING FUNCTION make_move...\u001b[0m\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r n b q k b . r\n",
+       "p p p p p p p p\n",
+       ". . . . . . . n\n",
+       ". . . . . . . .\n",
+       ". . . . P . . .\n",
+       ". . . . . . . .\n",
+       "P P P P . P P P\n",
+       "R N B Q K B N R
" + ], + "text/plain": [ + "'
r n b q k b . r\\np p p p p p p p\\n. . . . . . . n\\n. . . . . . . .\\n. . . . P . . .\\n. . . . . . . .\\nP P P P . P P P\\nR N B Q K B N R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoard_Proxy\u001b[0m (to Player_Black):\n", + "\n", + "\u001b[33mBoard_Proxy\u001b[0m (to Player_Black):\n", + "\n", + "\u001b[32m***** Response from calling tool (call_59t20pl0ab68z4xx2workgbc) *****\u001b[0m\n", + "Moved knight (♞) from g8 to h6.\n", + "\u001b[32m**********************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[33mPlayer_Black\u001b[0m (to Board_Proxy):\n", + "\n", + "\u001b[32m***** Suggested tool call (call_jwv1d86srs1fnvu33cky9tgv): make_move *****\u001b[0m\n", + "Arguments: \n", + "{\"move\":\"g8h6\"}\n", + "\u001b[32m**************************************************************************\u001b[0m\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer_Black\u001b[0m (to Player_White):\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> USING AUTO REPLY...\u001b[0m\n" + ] + } + ], + "source": [ + "# Clear the board.\n", + "board = chess.Board()\n", + "\n", + "chat_result = player_black.initiate_chat(\n", + " player_white,\n", + " message=\"Let's play chess! Your move.\",\n", + " max_turns=10,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this stage, it's hard to tell who's going to win, but they're playing well and using the functions correctly." + ] + } + ], + "metadata": { + "front_matter": { + "description": "LLM-backed agents playing chess with each other using nested chats.", + "tags": [ + "nested chat", + "tool use", + "orchestration" + ] + }, + "kernelspec": { + "display_name": "autogen", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook/agentchat_nested_sequential_chats.ipynb b/notebook/agentchat_nested_sequential_chats.ipynb index 4d013218d32f..3c4ca199484c 100644 --- a/notebook/agentchat_nested_sequential_chats.ipynb +++ b/notebook/agentchat_nested_sequential_chats.ipynb @@ -811,7 +811,9 @@ "front_matter": { "description": "Solve complex tasks with one or more sequence chats nested as inner monologue.", "tags": [ - "nested chat", "sequential chats", "orchestration" + "nested chat", + "sequential chats", + "orchestration" ] }, "kernelspec": { diff --git a/notebook/agentchat_nestedchat.ipynb b/notebook/agentchat_nestedchat.ipynb index 3cd4d0a99ed7..f81f20398591 100644 --- a/notebook/agentchat_nestedchat.ipynb +++ b/notebook/agentchat_nestedchat.ipynb @@ -100,7 +100,7 @@ " system_message=\"\"\"\n", " You are a professional writer, known for your insightful and engaging articles.\n", " You transform complex concepts into compelling narratives.\n", - " You should imporve the quality of the content based on the feedback from the user.\n", + " You should improve the quality of the content based on the feedback from the user.\n", " \"\"\",\n", ")\n", "\n", diff --git a/notebook/agentchat_qdrant_RetrieveChat.ipynb b/notebook/agentchat_qdrant_RetrieveChat.ipynb deleted file mode 100644 index 10d6b55e11c6..000000000000 --- a/notebook/agentchat_qdrant_RetrieveChat.ipynb +++ /dev/null @@ -1,1032 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using RetrieveChat with Qdrant for Retrieve Augmented Code Generation and Question Answering\n", - "\n", - "[Qdrant](https://qdrant.tech/) is a high-performance vector search engine/database.\n", - "\n", - "This notebook demonstrates the usage of `QdrantRetrieveUserProxyAgent` for RAG, based on [agentchat_RetrieveChat.ipynb](https://colab.research.google.com/github/microsoft/autogen/blob/main/notebook/agentchat_RetrieveChat.ipynb).\n", - "\n", - "\n", - "RetrieveChat is a conversational system for retrieve augmented code generation and question answering. In this notebook, we demonstrate how to utilize RetrieveChat to generate code and answer questions based on customized documentations that are not present in the LLM's training dataset. RetrieveChat uses the `RetrieveAssistantAgent` and `QdrantRetrieveUserProxyAgent`, which is similar to the usage of `AssistantAgent` and `UserProxyAgent` in other notebooks (e.g., [Automated Task Solving with Code Generation, Execution & Debugging](https://github.com/microsoft/autogen/blob/main/notebook/agentchat_auto_feedback_from_code_execution.ipynb)).\n", - "\n", - "We'll demonstrate usage of RetrieveChat with Qdrant for code generation and question answering w/ human feedback.\n", - "\n", - "````{=mdx}\n", - ":::info Requirements\n", - "Some extra dependencies are needed for this notebook, which can be installed via pip:\n", - "\n", - "```bash\n", - "pip install \"pyautogen[retrievechat-qdrant]\" \"flaml[automl]\"\n", - "```\n", - "\n", - "For more information, please refer to the [installation guide](/docs/installation/).\n", - ":::\n", - "````" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n", - "To disable this warning, you can either:\n", - "\t- Avoid using `tokenizers` before the fork if possible\n", - "\t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n", - "Requirement already satisfied: pyautogen>=0.2.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen[retrievechat]>=0.2.3) (0.2.21)\n", - "Requirement already satisfied: flaml[automl] in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (2.1.2)\n", - "Requirement already satisfied: qdrant_client[fastembed] in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (1.6.4)\n", - "Requirement already satisfied: openai>=1.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (1.12.0)\n", - "Requirement already satisfied: diskcache in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (5.6.3)\n", - "Requirement already satisfied: termcolor in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (2.3.0)\n", - "Requirement already satisfied: numpy<2,>=1.17.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (1.26.4)\n", - "Requirement already satisfied: python-dotenv in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (1.0.0)\n", - "Requirement already satisfied: tiktoken in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (0.5.1)\n", - "Requirement already satisfied: pydantic!=2.6.0,<3,>=1.10 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (2.6.4)\n", - "Requirement already satisfied: docker in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (7.0.0)\n", - "Requirement already satisfied: lightgbm>=2.3.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from flaml[automl]) (4.1.0)\n", - "Requirement already satisfied: xgboost>=0.90 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from flaml[automl]) (2.0.1)\n", - "Requirement already satisfied: scipy>=1.4.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from flaml[automl]) (1.10.1)\n", - "Requirement already satisfied: pandas>=1.1.4 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from flaml[automl]) (2.2.0)\n", - "Requirement already satisfied: scikit-learn>=0.24 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from flaml[automl]) (1.3.2)\n", - "Requirement already satisfied: grpcio>=1.41.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from qdrant_client[fastembed]) (1.60.0)\n", - "Requirement already satisfied: grpcio-tools>=1.41.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from qdrant_client[fastembed]) (1.59.2)\n", - "Requirement already satisfied: httpx>=0.14.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx[http2]>=0.14.0->qdrant_client[fastembed]) (0.25.1)\n", - "Requirement already satisfied: portalocker<3.0.0,>=2.7.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from qdrant_client[fastembed]) (2.8.2)\n", - "Requirement already satisfied: urllib3<2.0.0,>=1.26.14 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from qdrant_client[fastembed]) (1.26.18)\n", - "Requirement already satisfied: fastembed==0.1.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from qdrant_client[fastembed]) (0.1.1)\n", - "Requirement already satisfied: onnx<2.0,>=1.11 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastembed==0.1.1->qdrant_client[fastembed]) (1.15.0)\n", - "Requirement already satisfied: onnxruntime<2.0,>=1.15 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastembed==0.1.1->qdrant_client[fastembed]) (1.15.1)\n", - "Requirement already satisfied: requests<3.0,>=2.31 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastembed==0.1.1->qdrant_client[fastembed]) (2.31.0)\n", - "Requirement already satisfied: tokenizers<0.14,>=0.13 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastembed==0.1.1->qdrant_client[fastembed]) (0.13.3)\n", - "Requirement already satisfied: tqdm<5.0,>=4.65 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastembed==0.1.1->qdrant_client[fastembed]) (4.66.2)\n", - "Requirement already satisfied: chromadb in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen[retrievechat]>=0.2.3) (0.4.22)\n", - "Requirement already satisfied: sentence-transformers in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen[retrievechat]>=0.2.3) (2.3.1)\n", - "Requirement already satisfied: pypdf in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen[retrievechat]>=0.2.3) (4.0.1)\n", - "Requirement already satisfied: ipython in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyautogen[retrievechat]>=0.2.3) (8.17.2)\n", - "Requirement already satisfied: protobuf<5.0dev,>=4.21.6 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from grpcio-tools>=1.41.0->qdrant_client[fastembed]) (4.23.4)\n", - "Requirement already satisfied: setuptools in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from grpcio-tools>=1.41.0->qdrant_client[fastembed]) (68.2.2)\n", - "Requirement already satisfied: anyio in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx>=0.14.0->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (3.7.1)\n", - "Requirement already satisfied: certifi in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx>=0.14.0->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (2024.2.2)\n", - "Requirement already satisfied: httpcore in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx>=0.14.0->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (1.0.1)\n", - "Requirement already satisfied: idna in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx>=0.14.0->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (3.6)\n", - "Requirement already satisfied: sniffio in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx>=0.14.0->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (1.3.0)\n", - "Requirement already satisfied: h2<5,>=3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from httpx[http2]>=0.14.0->qdrant_client[fastembed]) (4.1.0)\n", - "Requirement already satisfied: distro<2,>=1.7.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from openai>=1.3->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (1.8.0)\n", - "Requirement already satisfied: typing-extensions<5,>=4.7 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from openai>=1.3->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (4.9.0)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pandas>=1.1.4->flaml[automl]) (2.8.2)\n", - "Requirement already satisfied: pytz>=2020.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pandas>=1.1.4->flaml[automl]) (2024.1)\n", - "Requirement already satisfied: tzdata>=2022.7 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pandas>=1.1.4->flaml[automl]) (2024.1)\n", - "Requirement already satisfied: annotated-types>=0.4.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (0.6.0)\n", - "Requirement already satisfied: pydantic-core==2.16.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (2.16.3)\n", - "Requirement already satisfied: joblib>=1.1.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from scikit-learn>=0.24->flaml[automl]) (1.3.2)\n", - "Requirement already satisfied: threadpoolctl>=2.0.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from scikit-learn>=0.24->flaml[automl]) (3.2.0)\n", - "Requirement already satisfied: build>=1.0.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (1.0.3)\n", - "Requirement already satisfied: chroma-hnswlib==0.7.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (0.7.3)\n", - "Requirement already satisfied: fastapi>=0.95.2 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (0.104.1)\n", - "Requirement already satisfied: uvicorn>=0.18.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (0.24.0)\n", - "Requirement already satisfied: posthog>=2.4.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (3.0.2)\n", - "Requirement already satisfied: pulsar-client>=3.1.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (3.3.0)\n", - "Requirement already satisfied: opentelemetry-api>=1.2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (1.20.0)\n", - "Requirement already satisfied: opentelemetry-exporter-otlp-proto-grpc>=1.2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (1.20.0)\n", - "Requirement already satisfied: opentelemetry-instrumentation-fastapi>=0.41b0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (0.41b0)\n", - "Requirement already satisfied: opentelemetry-sdk>=1.2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (1.20.0)\n", - "Requirement already satisfied: pypika>=0.48.9 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (0.48.9)\n", - "Requirement already satisfied: overrides>=7.3.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (7.4.0)\n", - "Requirement already satisfied: importlib-resources in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (6.1.1)\n", - "Requirement already satisfied: bcrypt>=4.0.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (4.0.1)\n", - "Requirement already satisfied: typer>=0.9.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (0.9.0)\n", - "Requirement already satisfied: kubernetes>=28.1.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (28.1.0)\n", - "Requirement already satisfied: tenacity>=8.2.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (8.2.3)\n", - "Requirement already satisfied: PyYAML>=6.0.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (6.0.1)\n", - "Requirement already satisfied: mmh3>=4.0.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from chromadb->pyautogen[retrievechat]>=0.2.3) (4.0.1)\n", - "Requirement already satisfied: packaging>=14.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from docker->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (23.2)\n", - "Requirement already satisfied: decorator in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (5.1.1)\n", - "Requirement already satisfied: jedi>=0.16 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (0.19.1)\n", - "Requirement already satisfied: matplotlib-inline in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (0.1.6)\n", - "Requirement already satisfied: prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (3.0.39)\n", - "Requirement already satisfied: pygments>=2.4.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (2.16.1)\n", - "Requirement already satisfied: stack-data in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (0.6.3)\n", - "Requirement already satisfied: traitlets>=5 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (5.14.2)\n", - "Requirement already satisfied: exceptiongroup in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (1.1.3)\n", - "Requirement already satisfied: pexpect>4.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from ipython->pyautogen[retrievechat]>=0.2.3) (4.8.0)\n", - "Requirement already satisfied: transformers<5.0.0,>=4.32.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (4.33.3)\n", - "Requirement already satisfied: torch>=1.11.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (2.2.0)\n", - "Requirement already satisfied: nltk in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (3.8.1)\n", - "Requirement already satisfied: sentencepiece in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (0.1.99)\n", - "Requirement already satisfied: huggingface-hub>=0.15.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (0.20.3)\n", - "Requirement already satisfied: Pillow in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sentence-transformers->pyautogen[retrievechat]>=0.2.3) (10.2.0)\n", - "Requirement already satisfied: regex>=2022.1.18 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from tiktoken->pyautogen>=0.2.3->pyautogen[retrievechat]>=0.2.3) (2023.12.25)\n", - "Requirement already satisfied: pyproject_hooks in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from build>=1.0.3->chromadb->pyautogen[retrievechat]>=0.2.3) (1.0.0)\n", - "Requirement already satisfied: tomli>=1.1.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from build>=1.0.3->chromadb->pyautogen[retrievechat]>=0.2.3) (2.0.1)\n", - "Requirement already satisfied: starlette<0.28.0,>=0.27.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from fastapi>=0.95.2->chromadb->pyautogen[retrievechat]>=0.2.3) (0.27.0)\n", - "Requirement already satisfied: hyperframe<7,>=6.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from h2<5,>=3->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (6.0.1)\n", - "Requirement already satisfied: hpack<5,>=4.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from h2<5,>=3->httpx[http2]>=0.14.0->qdrant_client[fastembed]) (4.0.0)\n", - "Requirement already satisfied: filelock in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from huggingface-hub>=0.15.1->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (3.13.1)\n", - "Requirement already satisfied: fsspec>=2023.5.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from huggingface-hub>=0.15.1->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (2024.2.0)\n", - "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from jedi>=0.16->ipython->pyautogen[retrievechat]>=0.2.3) (0.8.3)\n", - "Requirement already satisfied: six>=1.9.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.16.0)\n", - "Requirement already satisfied: google-auth>=1.0.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (2.23.4)\n", - "Requirement already satisfied: websocket-client!=0.40.0,!=0.41.*,!=0.42.*,>=0.32.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.6.4)\n", - "Requirement already satisfied: requests-oauthlib in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.3.1)\n", - "Requirement already satisfied: oauthlib>=3.2.2 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (3.2.2)\n", - "Requirement already satisfied: coloredlogs in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from onnxruntime<2.0,>=1.15->fastembed==0.1.1->qdrant_client[fastembed]) (15.0.1)\n", - "Requirement already satisfied: flatbuffers in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from onnxruntime<2.0,>=1.15->fastembed==0.1.1->qdrant_client[fastembed]) (23.5.26)\n", - "Requirement already satisfied: sympy in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from onnxruntime<2.0,>=1.15->fastembed==0.1.1->qdrant_client[fastembed]) (1.12)\n", - "Requirement already satisfied: deprecated>=1.2.6 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-api>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.2.14)\n", - "Requirement already satisfied: importlib-metadata<7.0,>=6.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-api>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (6.11.0)\n", - "Requirement already satisfied: backoff<3.0.0,>=1.10.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (2.2.1)\n", - "Requirement already satisfied: googleapis-common-protos~=1.52 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.61.0)\n", - "Requirement already satisfied: opentelemetry-exporter-otlp-proto-common==1.20.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.20.0)\n", - "Requirement already satisfied: opentelemetry-proto==1.20.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.20.0)\n", - "Requirement already satisfied: opentelemetry-instrumentation-asgi==0.41b0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.41b0)\n", - "Requirement already satisfied: opentelemetry-instrumentation==0.41b0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.41b0)\n", - "Requirement already satisfied: opentelemetry-semantic-conventions==0.41b0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.41b0)\n", - "Requirement already satisfied: opentelemetry-util-http==0.41b0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.41b0)\n", - "Requirement already satisfied: wrapt<2.0.0,>=1.0.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation==0.41b0->opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.16.0)\n", - "Requirement already satisfied: asgiref~=3.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from opentelemetry-instrumentation-asgi==0.41b0->opentelemetry-instrumentation-fastapi>=0.41b0->chromadb->pyautogen[retrievechat]>=0.2.3) (3.7.2)\n", - "Requirement already satisfied: ptyprocess>=0.5 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pexpect>4.3->ipython->pyautogen[retrievechat]>=0.2.3) (0.7.0)\n", - "Requirement already satisfied: monotonic>=1.5 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from posthog>=2.4.0->chromadb->pyautogen[retrievechat]>=0.2.3) (1.6)\n", - "Requirement already satisfied: wcwidth in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30->ipython->pyautogen[retrievechat]>=0.2.3) (0.2.9)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from requests<3.0,>=2.31->fastembed==0.1.1->qdrant_client[fastembed]) (3.3.2)\n", - "Requirement already satisfied: networkx in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (3.2.1)\n", - "Requirement already satisfied: jinja2 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (3.1.3)\n", - "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.1.105 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.105)\n", - "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.1.105 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.105)\n", - "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.1.105 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.105)\n", - "Requirement already satisfied: nvidia-cudnn-cu12==8.9.2.26 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (8.9.2.26)\n", - "Requirement already satisfied: nvidia-cublas-cu12==12.1.3.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.3.1)\n", - "Requirement already satisfied: nvidia-cufft-cu12==11.0.2.54 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (11.0.2.54)\n", - "Requirement already satisfied: nvidia-curand-cu12==10.3.2.106 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (10.3.2.106)\n", - "Requirement already satisfied: nvidia-cusolver-cu12==11.4.5.107 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (11.4.5.107)\n", - "Requirement already satisfied: nvidia-cusparse-cu12==12.1.0.106 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.0.106)\n", - "Requirement already satisfied: nvidia-nccl-cu12==2.19.3 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (2.19.3)\n", - "Requirement already satisfied: nvidia-nvtx-cu12==12.1.105 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.1.105)\n", - "Requirement already satisfied: triton==2.2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (2.2.0)\n", - "Requirement already satisfied: nvidia-nvjitlink-cu12 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from nvidia-cusolver-cu12==11.4.5.107->torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (12.3.52)\n", - "Requirement already satisfied: safetensors>=0.3.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from transformers<5.0.0,>=4.32.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (0.3.2)\n", - "Requirement already satisfied: click<9.0.0,>=7.1.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from typer>=0.9.0->chromadb->pyautogen[retrievechat]>=0.2.3) (8.1.7)\n", - "Requirement already satisfied: h11>=0.8 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn>=0.18.3->uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (0.14.0)\n", - "Requirement already satisfied: httptools>=0.5.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (0.6.1)\n", - "Requirement already satisfied: uvloop!=0.15.0,!=0.15.1,>=0.14.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (0.19.0)\n", - "Requirement already satisfied: watchfiles>=0.13 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (0.21.0)\n", - "Requirement already satisfied: websockets>=10.4 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from uvicorn[standard]>=0.18.3->chromadb->pyautogen[retrievechat]>=0.2.3) (11.0.3)\n", - "Requirement already satisfied: executing>=1.2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from stack-data->ipython->pyautogen[retrievechat]>=0.2.3) (2.0.1)\n", - "Requirement already satisfied: asttokens>=2.1.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from stack-data->ipython->pyautogen[retrievechat]>=0.2.3) (2.4.1)\n", - "Requirement already satisfied: pure-eval in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from stack-data->ipython->pyautogen[retrievechat]>=0.2.3) (0.2.2)\n", - "Requirement already satisfied: cachetools<6.0,>=2.0.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (5.3.2)\n", - "Requirement already satisfied: pyasn1-modules>=0.2.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.3.0)\n", - "Requirement already satisfied: rsa<5,>=3.1.4 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (4.9)\n", - "Requirement already satisfied: zipp>=0.5 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from importlib-metadata<7.0,>=6.0->opentelemetry-api>=1.2.0->chromadb->pyautogen[retrievechat]>=0.2.3) (3.17.0)\n", - "Requirement already satisfied: humanfriendly>=9.1 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from coloredlogs->onnxruntime<2.0,>=1.15->fastembed==0.1.1->qdrant_client[fastembed]) (10.0)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from jinja2->torch>=1.11.0->sentence-transformers->pyautogen[retrievechat]>=0.2.3) (2.1.5)\n", - "Requirement already satisfied: mpmath>=0.19 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from sympy->onnxruntime<2.0,>=1.15->fastembed==0.1.1->qdrant_client[fastembed]) (1.3.0)\n", - "Requirement already satisfied: pyasn1<0.6.0,>=0.4.6 in /home/lijiang1/anaconda3/envs/autogen/lib/python3.10/site-packages (from pyasn1-modules>=0.2.1->google-auth>=1.0.1->kubernetes>=28.1.0->chromadb->pyautogen[retrievechat]>=0.2.3) (0.5.0)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install \"pyautogen[retrievechat-qdrant]\" \"flaml[automl]\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Set your API Endpoint\n", - "\n", - "The [`config_list_from_json`](https://microsoft.github.io/autogen/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "models to use: ['gpt4-1106-preview', 'gpt-35-turbo', 'gpt-35-turbo-0613']\n" - ] - } - ], - "source": [ - "from qdrant_client import QdrantClient\n", - "\n", - "import autogen\n", - "from autogen.agentchat.contrib.qdrant_retrieve_user_proxy_agent import QdrantRetrieveUserProxyAgent\n", - "from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent\n", - "\n", - "# Accepted file formats for that can be stored in\n", - "# a vector database instance\n", - "from autogen.retrieve_utils import TEXT_FORMATS\n", - "\n", - "config_list = autogen.config_list_from_json(\"OAI_CONFIG_LIST\")\n", - "\n", - "assert len(config_list) > 0\n", - "print(\"models to use: \", [config_list[i][\"model\"] for i in range(len(config_list))])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "````{=mdx}\n", - ":::tip\n", - "Learn more about configuring LLMs for agents [here](/docs/topics/llm_configuration).\n", - ":::\n", - "````" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Accepted file formats for `docs_path`:\n", - "['yml', 'ppt', 'org', 'doc', 'epub', 'rst', 'log', 'docx', 'htm', 'html', 'tsv', 'csv', 'json', 'yaml', 'xlsx', 'pptx', 'rtf', 'msg', 'odt', 'pdf', 'jsonl', 'md', 'xml', 'txt']\n" - ] - } - ], - "source": [ - "print(\"Accepted file formats for `docs_path`:\")\n", - "print(TEXT_FORMATS)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Construct agents for RetrieveChat\n", - "\n", - "We start by initializing the `RetrieveAssistantAgent` and `QdrantRetrieveUserProxyAgent`. The system message needs to be set to \"You are a helpful assistant.\" for RetrieveAssistantAgent. The detailed instructions are given in the user message. Later we will use the `QdrantRetrieveUserProxyAgent.generate_init_prompt` to combine the instructions and a retrieval augmented generation task for an initial prompt to be sent to the LLM assistant.\n", - "\n", - "### You can find the list of all the embedding models supported by Qdrant [here](https://qdrant.github.io/fastembed/examples/Supported_Models/)." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "# 1. create an RetrieveAssistantAgent instance named \"assistant\"\n", - "assistant = RetrieveAssistantAgent(\n", - " name=\"assistant\",\n", - " system_message=\"You are a helpful assistant.\",\n", - " llm_config={\n", - " \"timeout\": 600,\n", - " \"cache_seed\": 42,\n", - " \"config_list\": config_list,\n", - " },\n", - ")\n", - "\n", - "# 2. create the QdrantRetrieveUserProxyAgent instance named \"ragproxyagent\"\n", - "# By default, the human_input_mode is \"ALWAYS\", which means the agent will ask for human input at every step. We set it to \"NEVER\" here.\n", - "# `docs_path` is the path to the docs directory. It can also be the path to a single file, or the url to a single file. By default,\n", - "# it is set to None, which works only if the collection is already created.\n", - "#\n", - "# Here we generated the documentations from FLAML's docstrings. Not needed if you just want to try this notebook but not to reproduce the\n", - "# outputs. Clone the FLAML (https://github.com/microsoft/FLAML) repo and navigate to its website folder. Pip install and run `pydoc-markdown`\n", - "# and it will generate folder `reference` under `website/docs`.\n", - "#\n", - "# `task` indicates the kind of task we're working on. In this example, it's a `code` task.\n", - "# `chunk_token_size` is the chunk token size for the retrieve chat. By default, it is set to `max_tokens * 0.6`, here we set it to 2000.\n", - "# We use an in-memory QdrantClient instance here. Not recommended for production.\n", - "# Get the installation instructions here: https://qdrant.tech/documentation/guides/installation/\n", - "ragproxyagent = QdrantRetrieveUserProxyAgent(\n", - " name=\"ragproxyagent\",\n", - " human_input_mode=\"NEVER\",\n", - " max_consecutive_auto_reply=10,\n", - " retrieve_config={\n", - " \"task\": \"code\",\n", - " \"docs_path\": [\n", - " \"https://raw.githubusercontent.com/microsoft/flaml/main/README.md\",\n", - " \"https://raw.githubusercontent.com/microsoft/FLAML/main/website/docs/Research.md\",\n", - " ], # change this to your own path, such as https://raw.githubusercontent.com/microsoft/autogen/main/README.md\n", - " \"chunk_token_size\": 2000,\n", - " \"model\": config_list[0][\"model\"],\n", - " \"client\": QdrantClient(\":memory:\"),\n", - " \"embedding_model\": \"BAAI/bge-small-en-v1.5\",\n", - " },\n", - " code_execution_config=False,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "### Example 1\n", - "\n", - "[back to top](#toc)\n", - "\n", - "Use RetrieveChat to answer a question and ask for human-in-loop feedbacks.\n", - "\n", - "Problem: Is there a function named `tune_automl` in FLAML?" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Trying to create collection.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-04-07 18:30:12,489 - autogen.agentchat.contrib.qdrant_retrieve_user_proxy_agent - INFO - Found 3 chunks.\u001b[0m\n", - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32mAdding content of doc 0 to context.\u001b[0m\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Is there a function called tune_automl?\n", - "\n", - "Context is: [![PyPI version](https://badge.fury.io/py/FLAML.svg)](https://badge.fury.io/py/FLAML)\n", - "![Conda version](https://img.shields.io/conda/vn/conda-forge/flaml)\n", - "[![Build](https://github.com/microsoft/FLAML/actions/workflows/python-package.yml/badge.svg)](https://github.com/microsoft/FLAML/actions/workflows/python-package.yml)\n", - "![Python Version](https://img.shields.io/badge/3.8%20%7C%203.9%20%7C%203.10-blue)\n", - "[![Downloads](https://pepy.tech/badge/flaml)](https://pepy.tech/project/flaml)\n", - "[![](https://img.shields.io/discord/1025786666260111483?logo=discord&style=flat)](https://discord.gg/Cppx2vSPVP)\n", - "\n", - "\n", - "\n", - "# A Fast Library for Automated Machine Learning & Tuning\n", - "\n", - "

\n", - " \n", - "
\n", - "

\n", - "\n", - ":fire: Heads-up: We have migrated [AutoGen](https://microsoft.github.io/autogen/) into a dedicated [github repository](https://github.com/microsoft/autogen). Alongside this move, we have also launched a dedicated [Discord](https://discord.gg/pAbnFJrkgZ) server and a [website](https://microsoft.github.io/autogen/) for comprehensive documentation.\n", - "\n", - ":fire: The automated multi-agent chat framework in [AutoGen](https://microsoft.github.io/autogen/) is in preview from v2.0.0.\n", - "\n", - ":fire: FLAML is highlighted in OpenAI's [cookbook](https://github.com/openai/openai-cookbook#related-resources-from-around-the-web).\n", - "\n", - ":fire: [autogen](https://microsoft.github.io/autogen/) is released with support for ChatGPT and GPT-4, based on [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673).\n", - "\n", - ":fire: FLAML supports Code-First AutoML & Tuning – Private Preview in [Microsoft Fabric Data Science](https://learn.microsoft.com/en-us/fabric/data-science/).\n", - "\n", - "## What is FLAML\n", - "\n", - "FLAML is a lightweight Python library for efficient automation of machine\n", - "learning and AI operations. It automates workflow based on large language models, machine learning models, etc.\n", - "and optimizes their performance.\n", - "\n", - "- FLAML enables building next-gen GPT-X applications based on multi-agent conversations with minimal effort. It simplifies the orchestration, automation and optimization of a complex GPT-X workflow. It maximizes the performance of GPT-X models and augments their weakness.\n", - "- For common machine learning tasks like classification and regression, it quickly finds quality models for user-provided data with low computational resources. It is easy to customize or extend. Users can find their desired customizability from a smooth range.\n", - "- It supports fast and economical automatic tuning (e.g., inference hyperparameters for foundation models, configurations in MLOps/LMOps workflows, pipelines, mathematical/statistical models, algorithms, computing experiments, software configurations), capable of handling large search space with heterogeneous evaluation cost and complex constraints/guidance/early stopping.\n", - "\n", - "FLAML is powered by a series of [research studies](https://microsoft.github.io/FLAML/docs/Research/) from Microsoft Research and collaborators such as Penn State University, Stevens Institute of Technology, University of Washington, and University of Waterloo.\n", - "\n", - "FLAML has a .NET implementation in [ML.NET](http://dot.net/ml), an open-source, cross-platform machine learning framework for .NET.\n", - "\n", - "## Installation\n", - "\n", - "FLAML requires **Python version >= 3.8**. It can be installed from pip:\n", - "\n", - "```bash\n", - "pip install flaml\n", - "```\n", - "\n", - "Minimal dependencies are installed without extra options. You can install extra options based on the feature you need. For example, use the following to install the dependencies needed by the [`autogen`](https://microsoft.github.io/autogen/) package.\n", - "\n", - "```bash\n", - "pip install \"flaml[autogen]\"\n", - "```\n", - "\n", - "Find more options in [Installation](https://microsoft.github.io/FLAML/docs/Installation).\n", - "Each of the [`notebook examples`](https://github.com/microsoft/FLAML/tree/main/notebook) may require a specific option to be installed.\n", - "\n", - "## Quickstart\n", - "\n", - "- (New) The [autogen](https://microsoft.github.io/autogen/) package enables the next-gen GPT-X applications with a generic multi-agent conversation framework.\n", - " It offers customizable and conversable agents which integrate LLMs, tools and human.\n", - " By automating chat among multiple capable agents, one can easily make them collectively perform tasks autonomously or with human feedback, including tasks that require using tools via code. For example,\n", - "\n", - "```python\n", - "from flaml import autogen\n", - "\n", - "assistant = autogen.AssistantAgent(\"assistant\")\n", - "user_proxy = autogen.UserProxyAgent(\"user_proxy\")\n", - "user_proxy.initiate_chat(\n", - " assistant,\n", - " message=\"Show me the YTD gain of 10 largest technology companies as of today.\",\n", - ")\n", - "# This initiates an automated chat between the two agents to solve the task\n", - "```\n", - "\n", - "Autogen also helps maximize the utility out of the expensive LLMs such as ChatGPT and GPT-4. It offers a drop-in replacement of `openai.Completion` or `openai.ChatCompletion` with powerful functionalites like tuning, caching, templating, filtering. For example, you can optimize generations by LLM with your own tuning data, success metrics and budgets.\n", - "\n", - "```python\n", - "# perform tuning\n", - "config, analysis = autogen.Completion.tune(\n", - " data=tune_data,\n", - " metric=\"success\",\n", - " mode=\"max\",\n", - " eval_func=eval_func,\n", - " inference_budget=0.05,\n", - " optimization_budget=3,\n", - " num_samples=-1,\n", - ")\n", - "# perform inference for a test instance\n", - "response = autogen.Completion.create(context=test_instance, **config)\n", - "```\n", - "\n", - "- With three lines of code, you can start using this economical and fast\n", - " AutoML engine as a [scikit-learn style estimator](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML).\n", - "\n", - "```python\n", - "from flaml import AutoML\n", - "\n", - "automl = AutoML()\n", - "automl.fit(X_train, y_train, task=\"classification\")\n", - "```\n", - "\n", - "- You can restrict the learners and use FLAML as a fast hyperparameter tuning\n", - " tool for XGBoost, LightGBM, Random Forest etc. or a [customized learner](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML#estimator-and-search-space).\n", - "\n", - "```python\n", - "automl.fit(X_train, y_train, task=\"classification\", estimator_list=[\"lgbm\"])\n", - "```\n", - "\n", - "- You can also run generic hyperparameter tuning for a [custom function](https://microsoft.github.io/FLAML/docs/Use-Cases/Tune-User-Defined-Function).\n", - "\n", - "```python\n", - "from flaml import tune\n", - "tune.run(evaluation_function, config={…}, low_cost_partial_config={…}, time_budget_s=3600)\n", - "```\n", - "\n", - "- [Zero-shot AutoML](https://microsoft.github.io/FLAML/docs/Use-Cases/Zero-Shot-AutoML) allows using the existing training API from lightgbm, xgboost etc. while getting the benefit of AutoML in choosing high-performance hyperparameter configurations per task.\n", - "\n", - "```python\n", - "from flaml.default import LGBMRegressor\n", - "\n", - "# Use LGBMRegressor in the same way as you use lightgbm.LGBMRegressor.\n", - "estimator = LGBMRegressor()\n", - "# The hyperparameters are automatically set according to the training data.\n", - "estimator.fit(X_train, y_train)\n", - "```\n", - "\n", - "## Documentation\n", - "\n", - "You can find a detailed documentation about FLAML [here](https://microsoft.github.io/FLAML/).\n", - "\n", - "In addition, you can find:\n", - "\n", - "- [Research](https://microsoft.github.io/FLAML/docs/Research) and [blogposts](https://microsoft.github.io/FLAML/blog) around FLAML.\n", - "\n", - "- [Discord](https://discord.gg/Cppx2vSPVP).\n", - "\n", - "- [Contributing guide](https://microsoft.github.io/FLAML/docs/Contribute).\n", - "\n", - "- ML.NET documentation and tutorials for [Model Builder](https://learn.microsoft.com/dotnet/machine-learning/tutorials/predict-prices-with-model-builder), [ML.NET CLI](https://learn.microsoft.com/dotnet/machine-learning/tutorials/sentiment-analysis-cli), and [AutoML API](https://learn.microsoft.com/dotnet/machine-learning/how-to-guides/how-to-use-the-automl-api).\n", - "\n", - "## Contributing\n", - "\n", - "This project welcomes contributions and suggestions. Most contributions require you to agree to a\n", - "Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\n", - "the rights to use your contribution. For details, visit .\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "No, there is no function called `tune_automl` specifically mentioned in the context provided. However, FLAML does offer general hyperparameter tuning capabilities which could be related to this. In the context of FLAML, there is a generic function called `tune.run()` that can be used for hyperparameter tuning.\n", - "\n", - "Here's a short example of how to use FLAML's tune for a user-defined function based on the given context:\n", - "\n", - "```python\n", - "from flaml import tune\n", - "\n", - "def evaluation_function(config):\n", - " # evaluation logic that returns a metric score\n", - " ...\n", - "\n", - "# define the search space for hyperparameters\n", - "config_search_space = {\n", - " 'max_depth': tune.randint(lower=3, upper=10),\n", - " 'learning_rate': tune.loguniform(lower=1e-4, upper=1e-1),\n", - "}\n", - "\n", - "# run hyperparameter tuning\n", - "tune.run(\n", - " evaluation_function,\n", - " config=config_search_space,\n", - " low_cost_partial_config={'max_depth': 3},\n", - " time_budget_s=3600\n", - ")\n", - "```\n", - "\n", - "Please note that if you are referring to a different kind of function or use case, you might need to specify more details or check the official documentation or source code of the FLAML library.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "UPDATE CONTEXT\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32mAdding content of doc 2 to context.\u001b[0m\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32mAdding content of doc 1 to context.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Is there a function called tune_automl?\n", - "\n", - "Context is: # Research\n", - "\n", - "For technical details, please check our research publications.\n", - "\n", - "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2021flaml,\n", - " title={FLAML: A Fast and Lightweight AutoML Library},\n", - " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", - " year={2021},\n", - " booktitle={MLSys},\n", - "}\n", - "```\n", - "\n", - "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021cfo,\n", - " title={Frugal Optimization for Cost-related Hyperparameters},\n", - " author={Qingyun Wu and Chi Wang and Silu Huang},\n", - " year={2021},\n", - " booktitle={AAAI},\n", - "}\n", - "```\n", - "\n", - "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2021blendsearch,\n", - " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", - " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", - " year={2021},\n", - " booktitle={ICLR},\n", - "}\n", - "```\n", - "\n", - "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{liuwang2021hpolm,\n", - " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", - " author={Susan Xueqing Liu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ACL},\n", - "}\n", - "```\n", - "\n", - "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021chacha,\n", - " title={ChaCha for Online AutoML},\n", - " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", - " year={2021},\n", - " booktitle={ICML},\n", - "}\n", - "```\n", - "\n", - "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", - "\n", - "```bibtex\n", - "@inproceedings{wuwang2021fairautoml,\n", - " title={Fair AutoML},\n", - " author={Qingyun Wu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ArXiv preprint arXiv:2111.06495},\n", - "}\n", - "```\n", - "\n", - "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", - "\n", - "```bibtex\n", - "@inproceedings{kayaliwang2022default,\n", - " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", - " author={Moe Kayali and Chi Wang},\n", - " year={2022},\n", - " booktitle={ArXiv preprint arXiv:2202.09927},\n", - "}\n", - "```\n", - "\n", - "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", - "\n", - "```bibtex\n", - "@inproceedings{zhang2023targeted,\n", - " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", - " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", - " booktitle={International Conference on Learning Representations},\n", - " year={2023},\n", - " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", - "}\n", - "```\n", - "\n", - "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2023EcoOptiGen,\n", - " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", - " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2303.04673},\n", - "}\n", - "```\n", - "\n", - "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2023empirical,\n", - " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", - " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2306.01337},\n", - "}\n", - "```\n", - "If you are new to GitHub [here](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) is a detailed help source on getting involved with development on GitHub.\n", - "\n", - "When you submit a pull request, a CLA bot will automatically determine whether you need to provide\n", - "a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\n", - "provided by the bot. You will only need to do this once across all repos using our CLA.\n", - "\n", - "This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\n", - "For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\n", - "contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "UPDATE CONTEXT\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32mUpdating context and resetting conversation.\u001b[0m\n", - "\u001b[32mNo more context, will terminate.\u001b[0m\n", - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': 'TERMINATE', 'role': 'assistant'}], summary='', cost=({'total_cost': 0.19977, 'gpt-4': {'cost': 0.19977, 'prompt_tokens': 6153, 'completion_tokens': 253, 'total_tokens': 6406}}, {'total_cost': 0.19977, 'gpt-4': {'cost': 0.19977, 'prompt_tokens': 6153, 'completion_tokens': 253, 'total_tokens': 6406}}), human_input=[])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# reset the assistant. Always reset the assistant before starting a new conversation.\n", - "assistant.reset()\n", - "\n", - "qa_problem = \"Is there a function called tune_automl?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Example 2\n", - "\n", - "[back to top](#toc)\n", - "\n", - "Use RetrieveChat to answer a question that is not related to code generation.\n", - "\n", - "Problem: Who is the author of FLAML?" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32mAdding content of doc 2 to context.\u001b[0m\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Model gpt4-1106-preview not found. Using cl100k_base encoding.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mragproxyagent\u001b[0m (to assistant):\n", - "\n", - "You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\n", - "context provided by the user.\n", - "If you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\n", - "For code generation, you must obey the following rules:\n", - "Rule 1. You MUST NOT install any packages because all the packages needed are already installed.\n", - "Rule 2. You must follow the formats below to write your code:\n", - "```language\n", - "# your code\n", - "```\n", - "\n", - "User's question is: Who is the author of FLAML?\n", - "\n", - "Context is: # Research\n", - "\n", - "For technical details, please check our research publications.\n", - "\n", - "- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2021flaml,\n", - " title={FLAML: A Fast and Lightweight AutoML Library},\n", - " author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\n", - " year={2021},\n", - " booktitle={MLSys},\n", - "}\n", - "```\n", - "\n", - "- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021cfo,\n", - " title={Frugal Optimization for Cost-related Hyperparameters},\n", - " author={Qingyun Wu and Chi Wang and Silu Huang},\n", - " year={2021},\n", - " booktitle={AAAI},\n", - "}\n", - "```\n", - "\n", - "- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2021blendsearch,\n", - " title={Economical Hyperparameter Optimization With Blended Search Strategy},\n", - " author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\n", - " year={2021},\n", - " booktitle={ICLR},\n", - "}\n", - "```\n", - "\n", - "- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{liuwang2021hpolm,\n", - " title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\n", - " author={Susan Xueqing Liu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ACL},\n", - "}\n", - "```\n", - "\n", - "- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2021chacha,\n", - " title={ChaCha for Online AutoML},\n", - " author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\n", - " year={2021},\n", - " booktitle={ICML},\n", - "}\n", - "```\n", - "\n", - "- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\n", - "\n", - "```bibtex\n", - "@inproceedings{wuwang2021fairautoml,\n", - " title={Fair AutoML},\n", - " author={Qingyun Wu and Chi Wang},\n", - " year={2021},\n", - " booktitle={ArXiv preprint arXiv:2111.06495},\n", - "}\n", - "```\n", - "\n", - "- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\n", - "\n", - "```bibtex\n", - "@inproceedings{kayaliwang2022default,\n", - " title={Mining Robust Default Configurations for Resource-constrained AutoML},\n", - " author={Moe Kayali and Chi Wang},\n", - " year={2022},\n", - " booktitle={ArXiv preprint arXiv:2202.09927},\n", - "}\n", - "```\n", - "\n", - "- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\n", - "\n", - "```bibtex\n", - "@inproceedings{zhang2023targeted,\n", - " title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\n", - " author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\n", - " booktitle={International Conference on Learning Representations},\n", - " year={2023},\n", - " url={https://openreview.net/forum?id=0Ij9_q567Ma},\n", - "}\n", - "```\n", - "\n", - "- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wang2023EcoOptiGen,\n", - " title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\n", - " author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2303.04673},\n", - "}\n", - "```\n", - "\n", - "- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\n", - "\n", - "```bibtex\n", - "@inproceedings{wu2023empirical,\n", - " title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\n", - " author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\n", - " year={2023},\n", - " booktitle={ArXiv preprint arXiv:2306.01337},\n", - "}\n", - "```\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33massistant\u001b[0m (to ragproxyagent):\n", - "\n", - "The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.\n", - "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "data": { - "text/plain": [ - "ChatResult(chat_id=None, chat_history=[{'content': \"You're a retrieve augmented coding assistant. You answer user's questions based on your own knowledge and the\\ncontext provided by the user.\\nIf you can't answer the question with or without the current context, you should reply exactly `UPDATE CONTEXT`.\\nFor code generation, you must obey the following rules:\\nRule 1. You MUST NOT install any packages because all the packages needed are already installed.\\nRule 2. You must follow the formats below to write your code:\\n```language\\n# your code\\n```\\n\\nUser's question is: Who is the author of FLAML?\\n\\nContext is: # Research\\n\\nFor technical details, please check our research publications.\\n\\n- [FLAML: A Fast and Lightweight AutoML Library](https://www.microsoft.com/en-us/research/publication/flaml-a-fast-and-lightweight-automl-library/). Chi Wang, Qingyun Wu, Markus Weimer, Erkang Zhu. MLSys 2021.\\n\\n```bibtex\\n@inproceedings{wang2021flaml,\\n title={FLAML: A Fast and Lightweight AutoML Library},\\n author={Chi Wang and Qingyun Wu and Markus Weimer and Erkang Zhu},\\n year={2021},\\n booktitle={MLSys},\\n}\\n```\\n\\n- [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.\\n\\n```bibtex\\n@inproceedings{wu2021cfo,\\n title={Frugal Optimization for Cost-related Hyperparameters},\\n author={Qingyun Wu and Chi Wang and Silu Huang},\\n year={2021},\\n booktitle={AAAI},\\n}\\n```\\n\\n- [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.\\n\\n```bibtex\\n@inproceedings{wang2021blendsearch,\\n title={Economical Hyperparameter Optimization With Blended Search Strategy},\\n author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},\\n year={2021},\\n booktitle={ICLR},\\n}\\n```\\n\\n- [An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models](https://aclanthology.org/2021.acl-long.178.pdf). Susan Xueqing Liu, Chi Wang. ACL 2021.\\n\\n```bibtex\\n@inproceedings{liuwang2021hpolm,\\n title={An Empirical Study on Hyperparameter Optimization for Fine-Tuning Pre-trained Language Models},\\n author={Susan Xueqing Liu and Chi Wang},\\n year={2021},\\n booktitle={ACL},\\n}\\n```\\n\\n- [ChaCha for Online AutoML](https://www.microsoft.com/en-us/research/publication/chacha-for-online-automl/). Qingyun Wu, Chi Wang, John Langford, Paul Mineiro and Marco Rossi. ICML 2021.\\n\\n```bibtex\\n@inproceedings{wu2021chacha,\\n title={ChaCha for Online AutoML},\\n author={Qingyun Wu and Chi Wang and John Langford and Paul Mineiro and Marco Rossi},\\n year={2021},\\n booktitle={ICML},\\n}\\n```\\n\\n- [Fair AutoML](https://arxiv.org/abs/2111.06495). Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2111.06495 (2021).\\n\\n```bibtex\\n@inproceedings{wuwang2021fairautoml,\\n title={Fair AutoML},\\n author={Qingyun Wu and Chi Wang},\\n year={2021},\\n booktitle={ArXiv preprint arXiv:2111.06495},\\n}\\n```\\n\\n- [Mining Robust Default Configurations for Resource-constrained AutoML](https://arxiv.org/abs/2202.09927). Moe Kayali, Chi Wang. ArXiv preprint arXiv:2202.09927 (2022).\\n\\n```bibtex\\n@inproceedings{kayaliwang2022default,\\n title={Mining Robust Default Configurations for Resource-constrained AutoML},\\n author={Moe Kayali and Chi Wang},\\n year={2022},\\n booktitle={ArXiv preprint arXiv:2202.09927},\\n}\\n```\\n\\n- [Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives](https://openreview.net/forum?id=0Ij9_q567Ma). Shaokun Zhang, Feiran Jia, Chi Wang, Qingyun Wu. ICLR 2023 (notable-top-5%).\\n\\n```bibtex\\n@inproceedings{zhang2023targeted,\\n title={Targeted Hyperparameter Optimization with Lexicographic Preferences Over Multiple Objectives},\\n author={Shaokun Zhang and Feiran Jia and Chi Wang and Qingyun Wu},\\n booktitle={International Conference on Learning Representations},\\n year={2023},\\n url={https://openreview.net/forum?id=0Ij9_q567Ma},\\n}\\n```\\n\\n- [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673). Chi Wang, Susan Xueqing Liu, Ahmed H. Awadallah. ArXiv preprint arXiv:2303.04673 (2023).\\n\\n```bibtex\\n@inproceedings{wang2023EcoOptiGen,\\n title={Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference},\\n author={Chi Wang and Susan Xueqing Liu and Ahmed H. Awadallah},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2303.04673},\\n}\\n```\\n\\n- [An Empirical Study on Challenging Math Problem Solving with GPT-4](https://arxiv.org/abs/2306.01337). Yiran Wu, Feiran Jia, Shaokun Zhang, Hangyu Li, Erkang Zhu, Yue Wang, Yin Tat Lee, Richard Peng, Qingyun Wu, Chi Wang. ArXiv preprint arXiv:2306.01337 (2023).\\n\\n```bibtex\\n@inproceedings{wu2023empirical,\\n title={An Empirical Study on Challenging Math Problem Solving with GPT-4},\\n author={Yiran Wu and Feiran Jia and Shaokun Zhang and Hangyu Li and Erkang Zhu and Yue Wang and Yin Tat Lee and Richard Peng and Qingyun Wu and Chi Wang},\\n year={2023},\\n booktitle={ArXiv preprint arXiv:2306.01337},\\n}\\n```\\n\\n\", 'role': 'assistant'}, {'content': 'The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.', 'role': 'user'}], summary='The authors of FLAML are Chi Wang, Qingyun Wu, Markus Weimer, and Erkang Zhu.', cost=({'total_cost': 0.04596, 'gpt-4': {'cost': 0.04596, 'prompt_tokens': 1486, 'completion_tokens': 23, 'total_tokens': 1509}}, {'total_cost': 0.04596, 'gpt-4': {'cost': 0.04596, 'prompt_tokens': 1486, 'completion_tokens': 23, 'total_tokens': 1509}}), human_input=[])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# reset the assistant. Always reset the assistant before starting a new conversation.\n", - "assistant.reset()\n", - "\n", - "qa_problem = \"Who is the author of FLAML?\"\n", - "ragproxyagent.initiate_chat(assistant, message=ragproxyagent.message_generator, problem=qa_problem)" - ] - } - ], - "metadata": { - "front_matter": { - "description": "This notebook demonstrates the usage of QdrantRetrieveUserProxyAgent for RAG.", - "tags": [ - "rag" - ] - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebook/agentchat_society_of_mind.ipynb b/notebook/agentchat_society_of_mind.ipynb index 79e5990a2aff..df3a6c543397 100644 --- a/notebook/agentchat_society_of_mind.ipynb +++ b/notebook/agentchat_society_of_mind.ipynb @@ -57,7 +57,7 @@ "\n", "### Example Group Chat with Two Agents\n", "\n", - "In this example, we will use an AssistantAgent and a UserProxy agent (configured for code execution) to work together to solve a problem. Executing code requires *at least* two conversation turns (one to write the code, and one to execute the code). If the code fails, or needs further refinement, then additional turns may also be needed. When will then wrap these agents in a SocietyOfMindAgent, hiding the internal discussion from other agents (though will still appear in the console), and ensuring that the response is suitable as a standalone message." + "In this example, we will use an AssistantAgent and a UserProxy agent (configured for code execution) to work together to solve a problem. Executing code requires *at least* two conversation turns (one to write the code, and one to execute the code). If the code fails, or needs further refinement, then additional turns may also be needed. We will then wrap these agents in a SocietyOfMindAgent, hiding the internal discussion from other agents (though will still appear in the console), and ensuring that the response is suitable as a standalone message." ] }, { diff --git a/notebook/autobuild_agent_library.ipynb b/notebook/autobuild_agent_library.ipynb index 43521a1d25f8..bde50355319f 100644 --- a/notebook/autobuild_agent_library.ipynb +++ b/notebook/autobuild_agent_library.ipynb @@ -1,926 +1,927 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "6264276d39875995", - "metadata": { - "collapsed": false - }, - "source": [ - "# Automatically Build Multi-agent System from Agent Library\n", - "\n", - "By: [Linxin Song](https://linxins97.github.io/), [Jieyu Zhang](https://jieyuz2.github.io/)\n", - "\n", - "In this notebook, we introduce a new feature for AutoBuild, `build_from_library`, which help users build an automatic task-solving process powered by a multi-agent system from a pre-defined agent library. \n", - "Specifically, in `build_from_library`, we prompt an LLM to explore useful agents from a pre-defined agent library, generating configurations for those agents for a group chat to solve the user's task." - ] - }, - { - "cell_type": "markdown", - "id": "ec78dda8e3826d8a", - "metadata": { - "collapsed": false - }, - "source": [ - "## Requirement\n", - "\n", - "AutoBuild require `pyautogen[autobuild]`, which can be installed by the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "e8e9ae50658be975", - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%pip install pyautogen[autobuild]" - ] - }, - { - "cell_type": "markdown", - "id": "176c200804af63f3", - "metadata": { - "collapsed": false - }, - "source": [ - "## Preparation and useful tools\n", - "We need to specify a `config_path`, `default_llm_config` that include backbone LLM configurations." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2505f029423b21ab", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-26T16:58:02.762702600Z", - "start_time": "2023-12-26T16:58:02.472073Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "import json\n", - "\n", - "import autogen\n", - "from autogen.agentchat.contrib.agent_builder import AgentBuilder\n", - "\n", - "config_file_or_env = \"OAI_CONFIG_LIST\" # modify path\n", - "llm_config = {\"temperature\": 0}\n", - "config_list = autogen.config_list_from_json(config_file_or_env, filter_dict={\"model\": [\"gpt-4-1106-preview\", \"gpt-4\"]})\n", - "\n", - "def start_task(execution_task: str, agent_list: list):\n", - " group_chat = autogen.GroupChat(agents=agent_list, messages=[], max_round=12)\n", - " manager = autogen.GroupChatManager(groupchat=group_chat, llm_config={\"config_list\": config_list, **llm_config})\n", - " agent_list[0].initiate_chat(manager, message=execution_task)" - ] - }, - { - "cell_type": "markdown", - "id": "5fb3db8885dd6ee6", - "metadata": { - "collapsed": false - }, - "source": [ - "## Example for generating an agent library\n", - "Here, we show an example of generating an agent library from a pre-defined list of agents' names by prompting a `gpt-4`. You can also prepare a handcrafted library yourself.\n", - "\n", - "A Library contains each agent's name, description and system_message. The description is a brief introduction about agent's characteristics. As we will feed all agents' names and description to gpt-4 and let it choose the best agents for us, each agent's description should be simple but informative. \n", - "\n", - "First, we define a prompt template for description and system_message generation and a list of agents' name:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "68315f6ec912c58a", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:39:03.317527600Z", - "start_time": "2023-12-23T07:39:03.276859600Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "AGENT_SYS_MSG_PROMPT = \"\"\"Acccording to the following postion name, write a high quality instruction for the position following a given example. You should only return the instruction.\n", - "\n", - "# Position Name\n", - "{position}\n", - "\n", - "# Example instruction for Data Analyst\n", - "\n", - "As Data Analyst, you are tasked with leveraging your extensive knowledge in data analysis to recognize and extract meaningful features from vast datasets. Your expertise in machine learning, specifically with the Random Forest Classifier, allows you to construct robust predictive models adept at handling both classification and regression tasks. You excel in model evaluation and interpretation, ensuring that the performance of your algorithms is not just assessed with precision, but also understood in the context of the data and the problem at hand. With a command over Python and proficiency in using the pandas library, you manipulate and preprocess data with ease.\n", - "\"\"\"\n", - "\n", - "AGENT_DESC_PROMPT = \"\"\"According to position name and the instruction, summarize the position into a high quality one sentence description.\n", - "\n", - "# Position Name\n", - "{position}\n", - "\n", - "# Instruction\n", - "{instruction}\n", - "\"\"\"\n", - "\n", - "position_list = [\n", - " \"Environmental_Scientist\",\n", - " \"Astronomer\",\n", - " \"Software_Developer\",\n", - " \"Data_Analyst\",\n", - " \"Journalist\",\n", - " \"Teacher\",\n", - " \"Lawyer\",\n", - " \"Programmer\",\n", - " \"Accountant\",\n", - " \"Mathematician\",\n", - " \"Physicist\",\n", - " \"Biologist\",\n", - " \"Chemist\",\n", - " \"Statistician\",\n", - " \"IT_Specialist\",\n", - " \"Cybersecurity_Expert\",\n", - " \"Artificial_Intelligence_Engineer\",\n", - " \"Financial_Analyst\",\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "72b8e7d9d334a5c2", - "metadata": { - "collapsed": false - }, - "source": [ - "Then we can prompt a `gpt-4` model to generate each agent's system message as well as the description:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8fbfef9268fc5191", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.703372Z", - "start_time": "2023-12-23T07:39:04.472589200Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "build_manager = autogen.OpenAIWrapper(config_list=config_list)\n", - "sys_msg_list = []\n", - "\n", - "for pos in position_list:\n", - " resp_agent_sys_msg = (\n", - " build_manager.create(\n", - " messages=[\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": AGENT_SYS_MSG_PROMPT.format(\n", - " position=pos,\n", - " ),\n", - " }\n", - " ]\n", - " )\n", - " .choices[0]\n", - " .message.content\n", - " )\n", - " resp_desc_msg = (\n", - " build_manager.create(\n", - " messages=[\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": AGENT_DESC_PROMPT.format(\n", - " position=pos,\n", - " instruction=resp_agent_sys_msg,\n", - " ),\n", - " }\n", - " ]\n", - " )\n", - " .choices[0]\n", - " .message.content\n", - " )\n", - " sys_msg_list.append({\"name\": pos, \"system_message\": resp_agent_sys_msg, \"description\": resp_desc_msg})" - ] - }, - { - "cell_type": "markdown", - "id": "9e26c6db4befacc5", - "metadata": { - "collapsed": false - }, - "source": [ - "The generated profile will have the following format:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8ede1d7088eb183d", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.712399300Z", - "start_time": "2023-12-23T07:40:01.707400200Z" - }, - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'Environmental_Scientist',\n", - " 'system_message': 'As an Environmental Scientist, you are responsible for applying your profound knowledge of environmental science to analyze ecological data and assess the impact of human activities on natural resources and ecosystems. Your proficiency in environmental assessment techniques enables you to design and conduct field studies, collect samples, and monitor environmental parameters effectively. Utilizing Geographic Information Systems (GIS), you spatially analyze and visualize environmental data to better understand patterns and changes in the landscape. You are adept at interpreting the results and communicating your findings clearly to stakeholders, policymakers, and the public, thereby contributing to informed decision-making on environmental issues. Your role is essential in developing sustainable practices and recommending mitigation measures to minimize environmental degradation and promote conservation.',\n", - " 'description': 'As an Environmental Scientist, you are tasked with analyzing and assessing the impact of human activities on ecosystems by conducting field studies, using GIS for spatial analysis, and communicating your findings to inform sustainable practices and conservation efforts.'},\n", - " {'name': 'Astronomer',\n", - " 'system_message': 'As an Astronomer, your duty involves diligent observation and analysis of celestial phenomena across the universe. Utilize cutting-edge telescopes and instruments to gather astronomical data, looking for patterns and irregularities that can lead to groundbreaking discoveries. Your profound knowledge in astrophysics is pivotal in interpreting these findings, which may include identifying new celestial objects, scrutinizing the properties and behaviors of stars, planets, and galaxies, and understanding cosmic events. Mastery of complex astronomical software and advanced mathematics is crucial for modeling astronomical phenomena and processing the vast amounts of data. Your role is essential in advancing our understanding of the cosmos, contributing to the broader scientific community by publishing your findings in reputable journals and engaging in peer collaboration to further space exploration and research.',\n", - " 'description': 'An Astronomer is a professional who meticulously observes, analyzes, and interprets celestial phenomena using advanced telescopes and instruments, requiring a deep knowledge of astrophysics, proficiency in mathematical modeling, and collaboration in scientific communities to enhance our comprehension of the universe.'},\n", - " {'name': 'Software_Developer',\n", - " 'system_message': 'As a Software Developer, your objective is to craft, test, and maintain the software that will meet the needs of our users and clients. Your proficiency in programming languages such as Java, C#, or JavaScript is essential, enabling you to write clean, efficient, and maintainable code. You will design algorithms and flowcharts to create systems that are logical and user-friendly. Collaboration with cross-functional teams, including product managers and designers, is crucial in order to understand software requirements and deliver innovative solutions. With your understanding of the software development life cycle, you will work through the processes of coding, debugging, testing, and deployment. You will employ industry best practices such as version control with Git and conduct code reviews to maintain high standards of software quality. Your role places you at the heart of our development efforts, where your technical prowess advances the functionality, scalability, and reliability of our software products.',\n", - " 'description': 'A Software Developer is responsible for designing, coding, testing, and maintaining software that meets client needs using languages like Java, C#, or JavaScript, collaborating with teams, adhering to best practices like Git for version control, and ensuring quality and innovation throughout the development life cycle.'},\n", - " {'name': 'Data_Analyst',\n", - " 'system_message': 'As a Data Analyst, your role is pivotal in interpreting complex data and providing insights that inform strategic decision-making. Utilize your analytical skills to cleanse and organize large sets of structured and unstructured data, ensuring its accuracy and readiness for in-depth analysis. Apply statistical analysis and predictive modeling to uncover trends, patterns, and correlations that drive operational improvements and innovative solutions. Use your proficiency in SQL for database interactions, and harness visualization tools such as Tableau or Power BI to craft compelling stories from data, aiding stakeholders in visualizing the implications of your findings. Stay abreast with the latest analytics techniques and continuously refine your models for enhanced performance, contributing significantly to the data-driven culture of our organization.',\n", - " 'description': 'The Data Analyst interprets complex datasets to provide strategic insights, cleanses and organizes data, performs statistical analysis and predictive modeling to identify trends and inform improvements, utilizes SQL for database management, and employs visualization tools like Tableau or Power BI to effectively communicate findings to stakeholders.'},\n", - " {'name': 'Journalist',\n", - " 'system_message': 'As a Journalist, you are responsible for identifying and pursuing newsworthy stories with the utmost ethical standards and a commitment to factual reporting. Your innate curiosity and excellent communication skills enable you to conduct thorough research and interviews, uncovering the details that make each story compelling and informative. Skilled in both written and verbal storytelling, you craft articles, reports, and features that engage and inform the public, adhering to strict deadlines without compromising on the integrity and accuracy of your work. Proficient in multimedia journalism, you adeptly use digital tools and social media to reach a wider audience, ensuring that your stories have the maximum impact.',\n", - " 'description': 'A Journalist is tasked with ethically sourcing and meticulously reporting newsworthy events, utilizing strong research and storytelling abilities across multiple platforms to accurately inform and engage a diverse audience.'},\n", - " {'name': 'Teacher',\n", - " 'system_message': 'As a Teacher, you are entrusted with the essential responsibility of fostering knowledge and encouraging academic and personal growth in your students. Your deep understanding of pedagogy, coupled with your expertise in the subject matter, enables you to create and deliver curricula that are both engaging and educational. Your adeptness at differentiated instruction allows you to tailor your teaching methods to suit the varied learning styles and needs within your classroom. By skillfully blending traditional teaching techniques with modern educational technology, you facilitate a dynamic and interactive learning environment. You excel in assessment and feedback, not only to gauge student progress but also to continuously improve your own teaching strategies. With strong interpersonal skills, you maintain open lines of communication with students, parents, and colleagues, fostering a collaborative and supportive school community.',\n", - " 'description': \"A Teacher is responsible for cultivating students' knowledge and growth through expertise in pedagogical practices and subject matter, designing engaging curricula, adapting teaching methods to diverse learning needs, integrating technology, and using assessment for continuous improvement while nurturing a cooperative school community.\"},\n", - " {'name': 'Lawyer',\n", - " 'system_message': 'As a Lawyer, you are required to uphold the highest standards of legal proficiency and ethical practice. Your role involves advising clients on their legal rights and responsibilities, as well as representing them in civil and criminal proceedings. You must possess a strong understanding of the law, paired with the ability to analyze case law and legislate history, to construct compelling arguments in support of your client’s position. Your keen attention to detail and dedication to thorough research are crucial in identifying legal precedents and crafting legal documents that adhere to the strictest of procedural standards. Moreover, you must exhibit exceptional negotiation skills to achieve favorable outcomes, whether in the courtroom or at the settlement table. With your articulate verbal and written communication, you clearly and persuasively present cases, explaining complex legal concepts in understandable terms to clients, judges, and juries. Your commitment to confidentiality and upholding justice is paramount and reflected in all aspects of your professional conduct.',\n", - " 'description': 'A Lawyer is a professionally trained legal advocate responsible for representing clients in legal proceedings, providing expert advice on legal matters, constructing persuasive arguments through meticulous research and analysis of law, and negotiating settlements, all while adhering to the highest ethical standards and maintaining strict confidentiality.'},\n", - " {'name': 'Programmer',\n", - " 'system_message': 'As a Programmer, you are responsible for the design, development, and implementation of software programs. Utilize your comprehensive understanding of programming languages, including but not limited to Java, C++, and Python, to create efficient and innovative software solutions. Your role involves writing clean, maintainable code while adhering to best practices in software development. You are expected to troubleshoot, debug, and upgrade existing software, as well as collaborate with cross-functional teams to define and design new product features. Your ability to think algorithmically and solve problems systematically will be integral in creating software that is not only functional but also scalable and secure.',\n", - " 'description': 'A Programmer designs, develops, and implements innovative and efficient software solutions using languages like Java, C++, and Python, ensuring code maintainability, collaborating on new features, and enhancing existing applications with a strong focus on scalability and security.'},\n", - " {'name': 'Accountant',\n", - " 'system_message': 'As Accountant, you are charged with the meticulous management and analysis of financial records, ensuring accuracy and compliance with relevant laws and regulations. Utilize your comprehensive understanding of accounting principles to prepare, examine, and maintain financial reports and statements, including balance sheets and income statements. Your role involves the reconciliation of accounts, evaluating financial operations to recommend best practices, identifying issues, and strategizing solutions for fiscal efficiency and profitability. Mastery in accounting software such as QuickBooks or Sage, alongside proficiency in Microsoft Excel, enables you to efficiently process and analyze financial data. You must ensure proper financial documentation and control systems are in place, providing comprehensive support to the organization’s financial health and integrity.',\n", - " 'description': 'As an Accountant, you are responsible for the accurate and compliant management, analysis, and reporting of financial data, along with recommending strategies to enhance fiscal efficiency and profitability, supported by proficiency in accounting software and Microsoft Excel.'},\n", - " {'name': 'Mathematician',\n", - " 'system_message': 'As a Mathematician, you are responsible for utilizing your profound understanding of mathematical theories and methodologies to solve complex theoretical and practical problems across various domains. Your proficiency in abstract reasoning enables you to develop new mathematical principles and to recognize and articulate the underlying mathematical relationships within real-world scenarios. You apply your expertise in calculus, algebra, statistics, and other mathematical branches to conduct rigorous analyses and to model systems for prediction and optimization. With a strong foundation in logic and quantitative reasoning, you perform peer reviews and contribute to interdisciplinary research projects, ensuring accuracy and consistency in mathematical arguments and results. Your role is crucial in advancing mathematical knowledge and providing innovative solutions to scientific and engineering challenges.',\n", - " 'description': 'As a Mathematician, you apply advanced mathematical theories and analytical skills to solve theoretical and practical problems in various industries, develop new principles, and provide innovative solutions to complex scientific and engineering challenges.'},\n", - " {'name': 'Physicist',\n", - " 'system_message': 'As a Physicist, you are charged with applying your profound understanding of the physical laws that govern the universe to unravel complex scientific phenomena. Your proficiency in theoretical and experimental physics enables you to develop models and conduct experiments that explore fundamental forces and particles. With exceptional analytical skills, you interpret empirical data to validate existing theories or propose new explanations for unexplained observations. Mastery in the use of mathematical tools such as differential equations and linear algebra is crucial for you to simulate physical processes. You are also adept at using specialized software and equipment for data acquisition and analysis, contributing to advancements in fields ranging from quantum mechanics to cosmology. Your strong critical thinking abilities empower you to solve intricate problems, and your commitment to scientific rigor ensures the integrity and accuracy of your research outcomes.',\n", - " 'description': 'A Physicist applies deep knowledge of physical laws to investigate scientific phenomena through theoretical modeling and experimental research, utilizing advanced mathematical techniques and specialized equipment to advance understanding in areas such as quantum mechanics and cosmology.'},\n", - " {'name': 'Biologist',\n", - " 'system_message': 'As a Biologist, you are entrusted with the study and understanding of living organisms, applying your expertise to investigate their functions, genetics, evolution, and ecosystems. Your skills in experimental design empower you to conduct research and experiments that can unlock new biological insights and improve our comprehension of life processes. Utilizing advanced microscopy techniques and molecular biology methods, you should meticulously analyze cell structures and DNA sequences to uncover the intricacies of life at a microscopic level. Demonstrate proficiency in bioinformatics tools to analyze genetic data and contribute valuable findings to the scientific community. Furthermore, as a communicator of science, ensure that your research findings are effectively documented and presented in scientific journals and at conferences, thereby enhancing the collective knowledge in your field.',\n", - " 'description': 'A Biologist meticulously studies and understands living organisms, conducting advanced research to decode genetics and ecosystems and sharing findings through scientific publications and presentations.'},\n", - " {'name': 'Chemist',\n", - " 'system_message': 'As a Chemist, you are charged with applying your profound understanding of chemical principles to conduct complex experiments, synthesize new compounds, and analyze the molecular and atomic structure of materials. Your proficiency in utilizing sophisticated analytical techniques - such as chromatography, spectroscopy, and mass spectrometry - enables you to decipher the composition and properties of substances. The knowledge you hold in chemical safety and handling procedures ensures a secure laboratory environment. With an adeptness in maintaining accurate records and an insightful approach to interpreting data, you transform raw experimental results into valuable scientific insights. Your ability to communicate complex chemical information clearly makes you essential in collaborative research efforts and in driving innovation within the field.',\n", - " 'description': 'As a Chemist, you are responsible for conducting advanced experiments, synthesizing compounds, deciphering substance compositions with techniques like chromatography and mass spectrometry, and transforming experimental data into scientific insights, while maintaining safety and clear communication in research collaborations.'},\n", - " {'name': 'Statistician',\n", - " 'system_message': 'As a Statistician, your primary duty is to apply mathematical and statistical methods to collect, analyze, and interpret numerical data to make informed decisions. Your strong grounding in probability theory will be essential for designing surveys and experiments to generate data. You are adept at constructing and applying sophisticated statistical models and methods, such as linear regression, ANOVA, or time-series analysis, ensuring that you accurately capture trends and relationships within the data. You possess an in-depth understanding of statistical software such as R or SAS, allowing you to perform complex analyses with efficiency and precision. Your ability to communicate complex statistical concepts to non-experts will be crucial; hence, your role includes presenting findings in a clear, actionable manner, with data visualizations and reports that drive strategic planning and policy development.',\n", - " 'description': 'A Statistician employs and interprets advanced statistical techniques to design data-collection processes, analyze data, and present findings in a comprehensible manner, supporting evidence-based decision-making and policy formation.'},\n", - " {'name': 'IT_Specialist',\n", - " 'system_message': 'As an IT Specialist, your primary responsibility is to maintain the integrity and functionality of all our computer systems and networks. Your comprehensive understanding of hardware and software is crucial for diagnosing and resolving technical issues. You are adept at implementing network security measures to protect data and systems from cyber threats. You also play a significant role in systems and software upgrades, ensuring a seamless transition without disrupting workflow. Utilizing your strong problem-solving skills and proficiency in scripting languages, you automate repetitive tasks, enhancing system efficiency. Your ability to communicate effectively with team members and non-technical staff allows you to provide clear guidance and end-user support.',\n", - " 'description': 'An IT Specialist is responsible for upholding and optimizing our computer systems and networks through maintenance, security, upgrades, issue resolution, automation, and providing support and clear communication to both technical and non-technical personnel.'},\n", - " {'name': 'Cybersecurity_Expert',\n", - " 'system_message': \"As a Cybersecurity Expert, you are charged with the responsibility of safeguarding the organization's computer networks and systems. Your deep understanding of cyber threats and mitigation techniques is critical in identifying vulnerabilities and protecting against malicious attacks. Employing your experience with tools such as firewalls, antivirus software, and intrusion detection systems, you will continuously monitor and defend our digital infrastructure. You are expected to conduct regular security audits and penetration testing to simulate cyber attacks and find potential weaknesses before they can be exploited. Your proficiency in risk management frameworks and incident response protocols ensures that you are prepared to swiftly handle and mitigate any security incidents that occur. With your expertise in encryption technologies and network protocols, you protect sensitive data and ensure compliance with relevant security standards and regulations. Your foresight in staying up-to-date with the latest cybersecurity trends and threats is paramount to maintaining the organization's digital defense at its peak.\",\n", - " 'description': \"As a Cybersecurity Expert, you are responsible for the proactive protection and defense of an organization's computer networks and systems against cyber threats through continuous monitoring, conducting security audits, penetrating testing, and swiftly mitigating security incidents, while ensuring compliance with security regulations.\"},\n", - " {'name': 'Artificial_Intelligence_Engineer',\n", - " 'system_message': 'As an Artificial Intelligence Engineer, you are responsible for conceptualizing, designing, and implementing intelligent systems that simulate human cognitive processes. Your role demands a deep understanding of neural networks, particularly Convolutional Neural Networks (CNNs) for image recognition tasks and Recurrent Neural Networks (RNNs) for natural language processing. With your expertise in TensorFlow or PyTorch, you develop complex models that can learn, adapt, and make decisions. You prioritize the ethical design and deployment of AI systems, conscious of the implications your work may have on society. Mastery of algorithms and a proficiency in a high-level programming language, preferably Python, enable you to transform theoretical AI concepts into practical solutions that drive innovation and efficiency.',\n", - " 'description': 'An Artificial Intelligence Engineer specializes in creating and implementing advanced intelligent systems, with a mastery of neural networks, machine learning frameworks, and ethical AI principles, to develop innovative solutions that emulate human cognition.'},\n", - " {'name': 'Financial_Analyst',\n", - " 'system_message': 'As a Financial Analyst, you are entrusted with utilizing your in-depth understanding of financial principles to assess investment opportunities, analyze financial data, and forecast economic trends. Your proficiency in financial modeling is paramount, enabling you to develop complex models that underpin the valuation of stocks, bonds, and other financial instruments. With a sharp eye for detail, you scrutinize company financial statements to derive actionable insights and recommend strategies to optimize financial performance. Your expertise in Excel, especially with advanced functions and formulas, allows you to efficiently manipulate and analyze large financial datasets. You are a whiz at creating compelling visualizations and delivering presentations to communicate your findings and influence strategic decisions. Your role is crucial in guiding investment decisions and driving the fiscal prudence of the organization.',\n", - " 'description': \"A Financial Analyst performs in-depth financial analysis and modeling to evaluate investments, forecast economic trends, and deliver strategic recommendations, leveraging advanced Excel skills to inform and guide the organization's financial decisions.\"}]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sys_msg_list" - ] - }, - { - "cell_type": "markdown", - "id": "256dd32b03a7a172", - "metadata": { - "collapsed": false - }, - "source": [ - "We can save the generated agents' information into a json file." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "53111125938845cf", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.750855900Z", - "start_time": "2023-12-23T07:40:01.710399600Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "json.dump(sys_msg_list, open(\"./agent_library_example.json\", \"w\"), indent=4)" - ] - }, - { - "cell_type": "markdown", - "id": "cfd883b79a3bd932", - "metadata": { - "collapsed": false - }, - "source": [ - "## Build agents from library (by LLM)\n", - "Here, we introduce how to build agents from the generated library. As in the previous `build`, we also need to specify a `building_task` that lets the build manager know which agents should be selected from the library according to the task. \n", - "\n", - "We also need to specify a `library_path_or_json`, which can be a path of library or a JSON string with agents' configs. Here, we use the previously saved path as the library path." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8963a8709c8e92e2", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.752918500Z", - "start_time": "2023-12-23T07:40:01.735461Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "library_path_or_json = \"./agent_library_example.json\"\n", - "building_task = \"Find a paper on arxiv by programming, and analyze its application in some domain. For example, find a recent paper about gpt-4 on arxiv and find its potential applications in software.\"" - ] - }, - { - "cell_type": "markdown", - "id": "72656a8d0c1a9b12", - "metadata": { - "collapsed": false - }, - "source": [ - "Then, we can call the `build_from_library` from the AgentBuilder to generate a list of agents from the library and let them complete the user's `execution_task` in a group chat." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "5c669b76b2c9b750", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:46:02.075542200Z", - "start_time": "2023-12-23T07:43:55.489042900Z" - }, - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Programmer', 'Mathematician'] are selected.\n", - "\u001b[32m==> Creating agents...\u001b[0m\n", - "Creating agent Programmer...\n", - "Creating agent Mathematician...\n", - "Adding user console proxy...\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "To find a recent paper about explainable AI on arXiv, we can use the arXiv API to search for papers that match the query. However, since I cannot directly access external APIs, I suggest that one of us manually searches for the paper on the arXiv website using relevant search terms such as \"explainable AI\" and \"medical applications\". Once we find a suitable paper, we can discuss its potential applications in the medical field. \n", - "\n", - "Mathematician, would you like to perform the search, or shall I provide a Python script that could be used to perform the search programmatically?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "As a Mathematician, I can certainly appreciate the value of a programmatic approach to searching for academic papers. However, since I do not have direct access to execute code or APIs, I would suggest that you, as the Programmer, provide the Python script that could be used to perform the search on arXiv. Once we have identified a paper, I can then assist in discussing its potential applications in the medical field from a mathematical perspective.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Understood. I will provide a Python script that can be used to search for recent papers about explainable AI on arXiv with potential applications in the medical field. The script will use the `arxiv` Python package, which is a wrapper for the arXiv API. If the package is not installed, we will need to install it first.\n", - "\n", - "Let's start by checking if the `arxiv` package is installed and if not, we will install it. Computer_terminal, please execute the following command to check for the `arxiv` package and install it if necessary.\n", - "\n", - "```sh\n", - "pip show arxiv || pip install arxiv\n", - "```\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is sh)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Name: arxiv\n", - "Version: 2.1.0\n", - "Summary: Python wrapper for the arXiv API: https://arxiv.org/help/api/\n", - "Home-page: https://github.com/lukasschwab/arxiv.py\n", - "Author: Lukas Schwab\n", - "Author-email: lukas.schwab@gmail.com\n", - "License: MIT\n", - "Location: /home/vscode/.local/lib/python3.10/site-packages\n", - "Requires: feedparser, requests\n", - "Required-by: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Great, the `arxiv` package is already installed. Now, I will provide a Python script that uses the `arxiv` package to search for recent papers related to explainable AI with potential applications in the medical field. The script will query the arXiv API for papers with relevant keywords and print out the title and summary of the most recent paper found.\n", - "\n", - "Computer_terminal, please execute the following Python script.\n", - "\n", - "```python\n", - "import arxiv\n", - "\n", - "# Define the search query\n", - "search_query = 'all:explainable AI AND all:medical'\n", - "\n", - "# Search for papers on arXiv\n", - "search = arxiv.Search(\n", - " query = search_query,\n", - " max_results = 1,\n", - " sort_by = arxiv.SortCriterion.SubmittedDate\n", - ")\n", - "\n", - "# Fetch the most recent paper\n", - "for paper in search.results():\n", - " print(\"Title:\", paper.title)\n", - " print(\"Summary:\", paper.summary)\n", - " # Only print the most recent paper\n", - " break\n", - "```\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Title: Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", - "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", - "intelligence (AI) has become a pivotal component in the automation of clinical\n", - "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", - "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", - "comparison to the GPT-4 model, aiming to facilitate automated information\n", - "extraction from thyroid operation narratives. The current research landscape is\n", - "dominated by traditional methods heavily reliant on regular expressions, which\n", - "often face challenges in processing free-style text formats containing critical\n", - "details of operation records, including frozen biopsy reports. Addressing this,\n", - "the study leverages advanced natural language processing (NLP) techniques to\n", - "foster a paradigm shift towards more sophisticated data processing systems.\n", - "Through this comparative study, we aspire to unveil a more streamlined,\n", - "precise, and efficient approach to document processing in the healthcare\n", - "domain, potentially revolutionizing the way medical data is handled and\n", - "analyzed.\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "The paper titled \"Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\" presents a study on the use of artificial intelligence for automating the extraction of information from thyroid operation narratives. This is a clear example of explainable AI being applied in the medical field, specifically in the area of clinical workflows and document processing.\n", - "\n", - "The potential applications of such technology in medicine are vast. By automating the extraction of information from operation narratives, healthcare professionals can save time and reduce the likelihood of human error. This can lead to more accurate patient records, improved patient care, and streamlined administrative processes. Additionally, the ability to quickly and accurately process operation records can facilitate better data analysis, which can be used for medical research, trend analysis, and improving healthcare outcomes.\n", - "\n", - "The use of advanced natural language processing (NLP) techniques, as mentioned in the summary, is particularly important for processing free-style text formats that contain critical medical information. This technology could be further explored to extend its application to other types of medical documents and records, enhancing the overall efficiency of the healthcare system.\n", - "\n", - "The study's focus on comparing the performance of the fine-tuned KoELECTRA model with GPT-4 also highlights the importance of evaluating different AI models to determine the most effective approach for specific medical applications. This comparative analysis can lead to the development of more specialized AI tools tailored to the needs of the healthcare industry.\n", - "\n", - "In conclusion, the research presented in this paper has significant implications for the future of medical document processing and the broader integration of AI in healthcare.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "The insights provided by the Mathematician are indeed valuable. The application of AI for automated information extraction from medical documents like thyroid operation narratives can greatly enhance efficiency and accuracy in healthcare. The use of models like GPT-4 and KoELECTRA for natural language processing tasks shows the potential of AI to handle complex, unstructured data which is common in medical records.\n", - "\n", - "From a programming perspective, the implementation of such AI systems would involve training models on large datasets of medical documents to learn the context and semantics specific to medical terminology. Ensuring the explainability of AI in this context is crucial, as healthcare professionals need to understand and trust the AI's decision-making process, especially when it directly affects patient care.\n", - "\n", - "Moreover, the integration of explainable AI into healthcare systems must adhere to strict privacy and security regulations to protect sensitive patient data. This requires careful design and implementation of data handling procedures within the AI system.\n", - "\n", - "The potential applications extend beyond just document processing to diagnostic assistance, personalized treatment plans, and predictive analytics for patient outcomes. As AI technology continues to evolve, its role in supporting and enhancing the capabilities of healthcare professionals will undoubtedly expand.\n", - "\n", - "Given the importance of the topic and the potential impact on healthcare, it would be beneficial to keep an eye on further developments in this field. If there are no further questions or points to discuss, we can conclude our conversation on this topic.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "I agree with the Programmer's assessment. The implementation of AI in healthcare does indeed require careful consideration of the models used, the training data, and the explainability of the AI's decisions. The ethical implications, particularly concerning patient privacy and data security, are paramount and must be addressed with the utmost rigor.\n", - "\n", - "The potential for AI to assist in diagnostics, treatment planning, and predictive analytics is a promising development for the future of medicine. It is essential that these systems are developed in collaboration with healthcare professionals to ensure they meet the real-world needs of the field.\n", - "\n", - "The interdisciplinary nature of this work, combining expertise in mathematics, computer science, and medicine, is a testament to the collaborative efforts needed to advance healthcare technology. It has been a pleasure discussing the potential applications of explainable AI in medicine with you.\n", - "\n", - "If there are no further points to add, I believe we have reached a natural conclusion to our conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mAll agents have been cleared.\u001b[0m\n" - ] - } - ], - "source": [ - "new_builder = AgentBuilder(\n", - " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", - ")\n", - "agent_list, _ = new_builder.build_from_library(building_task, library_path_or_json, llm_config)\n", - "start_task(\n", - " execution_task=\"Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\",\n", - " agent_list=agent_list,\n", - ")\n", - "new_builder.clear_all_agents()" - ] - }, - { - "cell_type": "markdown", - "id": "c7a10e6fa00a5a0d", - "metadata": { - "collapsed": false - }, - "source": [ - "## Build agents from library (by description-task similarity)\n", - "We also support using embedding similarity to select agents. You can use a [Sentence-Transformers model](https://www.sbert.net/docs/pretrained_models.html) as an embedding extractor, and AgentBuilder will select agents with profiles that are the most similar to the building task from the library by comparing their embedding similarity. This will reduce the use of LLMs but may have less accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "521dc5f961efde59", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-26T17:01:29.333975100Z", - "start_time": "2023-12-26T16:58:11.070813500Z" - }, - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Programmer', 'Mathematician'] are selected.\n", - "\u001b[32m==> Creating agents...\u001b[0m\n", - "Creating agent Programmer...\n", - "Creating agent Mathematician...\n", - "Adding user console proxy...\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "To find a recent paper about GPT-4 on arXiv, we can use the arXiv API to search for papers. However, since I can't directly access external APIs, I can write a Python script that you can run on your local machine to perform this search. Would you like me to provide you with such a script?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "I apologize for the confusion. I will provide a Python script that can be executed by the Computer_terminal to search for recent papers about GPT-4 on arXiv. Let's proceed with that.\n", - "\n", - "```python\n", - "import requests\n", - "from xml.etree import ElementTree\n", - "\n", - "# Define the search parameters and URL for the arXiv API\n", - "search_query = 'all:gpt-4'\n", - "start = 0\n", - "max_results = 5\n", - "sort_by = 'submittedDate'\n", - "sort_order = 'descending'\n", - "url = f'http://export.arxiv.org/api/query?search_query={search_query}&start={start}&max_results={max_results}&sortBy={sort_by}&sortOrder={sort_order}'\n", - "\n", - "# Send a GET request to the arXiv API\n", - "response = requests.get(url)\n", - "\n", - "# Parse the response if it was successful\n", - "if response.status_code == 200:\n", - " root = ElementTree.fromstring(response.content)\n", - " # Find and print the entries (papers)\n", - " for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):\n", - " title = entry.find('{http://www.w3.org/2005/Atom}title').text\n", - " summary = entry.find('{http://www.w3.org/2005/Atom}summary').text\n", - " published = entry.find('{http://www.w3.org/2005/Atom}published').text\n", - " print(f\"Title: {title}\\nSummary: {summary}\\nPublished Date: {published}\\n\")\n", - "else:\n", - " print(f\"Failed to fetch data from arXiv. Status code: {response.status_code}\")\n", - "```\n", - "\n", - "This script will fetch the most recent papers related to GPT-4 from the arXiv API and print out their titles, summaries, and publication dates. Please execute this script to find the information we need.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Title: What If We Recaption Billions of Web Images with LLaMA-3?\n", - "Summary: Web-crawled image-text pairs are inherently noisy. Prior studies demonstrate\n", - "that semantically aligning and enriching textual descriptions of these pairs\n", - "can significantly enhance model training across various vision-language tasks,\n", - "particularly text-to-image generation. However, large-scale investigations in\n", - "this area remain predominantly closed-source. Our paper aims to bridge this\n", - "community effort, leveraging the powerful and \\textit{open-sourced} LLaMA-3, a\n", - "GPT-4 level LLM. Our recaptioning pipeline is simple: first, we fine-tune a\n", - "LLaMA-3-8B powered LLaVA-1.5 and then employ it to recaption 1.3 billion images\n", - "from the DataComp-1B dataset. Our empirical results confirm that this enhanced\n", - "dataset, Recap-DataComp-1B, offers substantial benefits in training advanced\n", - "vision-language models. For discriminative models like CLIP, we observe\n", - "enhanced zero-shot performance in cross-modal retrieval tasks. For generative\n", - "models like text-to-image Diffusion Transformers, the generated images exhibit\n", - "a significant improvement in alignment with users' text instructions,\n", - "especially in following complex queries. Our project page is\n", - "https://www.haqtu.me/Recap-Datacomp-1B/\n", - "\n", - "Published Date: 2024-06-12T17:59:07Z\n", - "\n", - "Title: DafnyBench: A Benchmark for Formal Software Verification\n", - "Summary: We introduce DafnyBench, the largest benchmark of its kind for training and\n", - "evaluating machine learning systems for formal software verification. We test\n", - "the ability of LLMs such as GPT-4 and Claude 3 to auto-generate enough hints\n", - "for the Dafny formal verification engine to successfully verify over 750\n", - "programs with about 53,000 lines of code. The best model and prompting scheme\n", - "achieved 68% success rate, and we quantify how this rate improves when retrying\n", - "with error message feedback and how it deteriorates with the amount of required\n", - "code and hints. We hope that DafnyBench will enable rapid improvements from\n", - "this baseline as LLMs and verification techniques grow in quality.\n", - "\n", - "Published Date: 2024-06-12T17:53:31Z\n", - "\n", - "Title: A Sociotechnical Lens for Evaluating Computer Vision Models: A Case\n", - " Study on Detecting and Reasoning about Gender and Emotion\n", - "Summary: In the evolving landscape of computer vision (CV) technologies, the automatic\n", - "detection and interpretation of gender and emotion in images is a critical area\n", - "of study. This paper investigates social biases in CV models, emphasizing the\n", - "limitations of traditional evaluation metrics such as precision, recall, and\n", - "accuracy. These metrics often fall short in capturing the complexities of\n", - "gender and emotion, which are fluid and culturally nuanced constructs. Our\n", - "study proposes a sociotechnical framework for evaluating CV models,\n", - "incorporating both technical performance measures and considerations of social\n", - "fairness. Using a dataset of 5,570 images related to vaccination and climate\n", - "change, we empirically compared the performance of various CV models, including\n", - "traditional models like DeepFace and FER, and generative models like GPT-4\n", - "Vision. Our analysis involved manually validating the gender and emotional\n", - "expressions in a subset of images to serve as benchmarks. Our findings reveal\n", - "that while GPT-4 Vision outperforms other models in technical accuracy for\n", - "gender classification, it exhibits discriminatory biases, particularly in\n", - "response to transgender and non-binary personas. Furthermore, the model's\n", - "emotion detection skew heavily towards positive emotions, with a notable bias\n", - "towards associating female images with happiness, especially when prompted by\n", - "male personas. These findings underscore the necessity of developing more\n", - "comprehensive evaluation criteria that address both validity and discriminatory\n", - "biases in CV models. Our proposed framework provides guidelines for researchers\n", - "to critically assess CV tools, ensuring their application in communication\n", - "research is both ethical and effective. The significant contribution of this\n", - "study lies in its emphasis on a sociotechnical approach, advocating for CV\n", - "technologies that support social good and mitigate biases rather than\n", - "perpetuate them.\n", - "\n", - "Published Date: 2024-06-12T13:52:30Z\n", - "\n", - "Title: Supportiveness-based Knowledge Rewriting for Retrieval-augmented\n", - " Language Modeling\n", - "Summary: Retrieval-augmented language models (RALMs) have recently shown great\n", - "potential in mitigating the limitations of implicit knowledge in LLMs, such as\n", - "untimely updating of the latest expertise and unreliable retention of long-tail\n", - "knowledge. However, since the external knowledge base, as well as the\n", - "retriever, can not guarantee reliability, potentially leading to the knowledge\n", - "retrieved not being helpful or even misleading for LLM generation. In this\n", - "paper, we introduce Supportiveness-based Knowledge Rewriting (SKR), a robust\n", - "and pluggable knowledge rewriter inherently optimized for LLM generation.\n", - "Specifically, we introduce the novel concept of \"supportiveness\"--which\n", - "represents how effectively a knowledge piece facilitates downstream tasks--by\n", - "considering the perplexity impact of augmented knowledge on the response text\n", - "of a white-box LLM. Based on knowledge supportiveness, we first design a\n", - "training data curation strategy for our rewriter model, effectively identifying\n", - "and filtering out poor or irrelevant rewrites (e.g., with low supportiveness\n", - "scores) to improve data efficacy. We then introduce the direct preference\n", - "optimization (DPO) algorithm to align the generated rewrites to optimal\n", - "supportiveness, guiding the rewriter model to summarize augmented content that\n", - "better improves the final response. Comprehensive evaluations across six\n", - "popular knowledge-intensive tasks and four LLMs have demonstrated the\n", - "effectiveness and superiority of SKR. With only 7B parameters, SKR has shown\n", - "better knowledge rewriting capability over GPT-4, the current state-of-the-art\n", - "general-purpose LLM.\n", - "\n", - "Published Date: 2024-06-12T11:52:35Z\n", - "\n", - "Title: Automated Information Extraction from Thyroid Operation Narrative: A\n", - " Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", - "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", - "intelligence (AI) has become a pivotal component in the automation of clinical\n", - "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", - "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", - "comparison to the GPT-4 model, aiming to facilitate automated information\n", - "extraction from thyroid operation narratives. The current research landscape is\n", - "dominated by traditional methods heavily reliant on regular expressions, which\n", - "often face challenges in processing free-style text formats containing critical\n", - "details of operation records, including frozen biopsy reports. Addressing this,\n", - "the study leverages advanced natural language processing (NLP) techniques to\n", - "foster a paradigm shift towards more sophisticated data processing systems.\n", - "Through this comparative study, we aspire to unveil a more streamlined,\n", - "precise, and efficient approach to document processing in the healthcare\n", - "domain, potentially revolutionizing the way medical data is handled and\n", - "analyzed.\n", - "\n", - "Published Date: 2024-06-12T06:44:05Z\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Based on the search results from the arXiv API, we have found several papers that discuss potential applications of GPT-4 in software:\n", - "\n", - "1. **Recaptioning Web Images with LLaMA-3 and GPT-4**: This paper discusses the use of GPT-4 level LLMs for recaptioning web images, which can enhance model training across various vision-language tasks. This has implications for improving the quality of datasets used in machine learning and could be particularly beneficial for text-to-image generation and cross-modal retrieval tasks.\n", - "\n", - "2. **DafnyBench: A Benchmark for Formal Software Verification**: This paper introduces a benchmark for training and evaluating machine learning systems for formal software verification. It tests the ability of LLMs such as GPT-4 to auto-generate hints for the Dafny formal verification engine to successfully verify programs. This application could significantly impact the field of software verification by automating the generation of verification hints, potentially improving the efficiency and reliability of the verification process.\n", - "\n", - "3. **Automated Information Extraction from Thyroid Operation Narrative**: This study compares the GPT-4 model with the fine-tuned KoELECTRA model for automated information extraction from thyroid operation narratives. The application of GPT-4 in this context could revolutionize document processing in healthcare by providing a more efficient and accurate method for extracting information from medical records.\n", - "\n", - "These papers suggest that GPT-4 has the potential to be applied in various software-related fields, including enhancing datasets for machine learning, formal software verification, and healthcare document processing. The applications in these papers could lead to more efficient, accurate, and reliable software systems across different domains.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "The applications mentioned indeed highlight the versatility of GPT-4 in different domains. To further understand the potential impact of GPT-4 on software, let's delve into the mathematical and algorithmic principles that could be at play in these applications:\n", - "\n", - "1. **Recaptioning Web Images**: The process of recaptioning images with a language model like GPT-4 involves understanding the context of an image and generating descriptive text that accurately reflects its content. This task likely involves a combination of computer vision techniques to interpret the image and natural language processing to generate the caption. From a mathematical perspective, this would involve optimization algorithms to fine-tune the language model on a specific dataset, ensuring that the generated captions are both semantically and syntactically correct.\n", - "\n", - "2. **Formal Software Verification**: The use of GPT-4 to auto-generate hints for formal verification engines like Dafny involves the model understanding the logic and structure of the code. This requires a deep understanding of formal logic, proof theory, and possibly type theory if the language being verified is statically typed. The success rate of auto-generated hints would depend on the model's ability to reason about the correctness of code and the underlying mathematical properties that ensure its validity.\n", - "\n", - "3. **Automated Information Extraction from Medical Records**: For GPT-4 to extract information from medical narratives, it must process unstructured text and identify relevant medical terms and their relationships. This task involves natural language understanding, which from a mathematical standpoint, can be seen as a form of pattern recognition and classification. The model would need to be trained on a large corpus of medical texts, and its performance would be measured by its precision and recall in identifying and extracting the correct information.\n", - "\n", - "In each of these applications, GPT-4's effectiveness would be influenced by the underlying mathematical models, such as neural networks, and the optimization techniques used during training, such as gradient descent. The quality of the training data and the model's architecture (e.g., attention mechanisms, transformer layers) also play a crucial role in its performance.\n", - "\n", - "To verify the potential of GPT-4 in these applications, one could set up experiments to measure the performance of GPT-4 against specific benchmarks or metrics relevant to each domain. For example, in the case of formal software verification, one could measure the percentage of programs that are successfully verified with the hints generated by GPT-4 compared to a baseline or human-generated hints.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "To further verify the potential applications of GPT-4 in software, we can consider the following:\n", - "\n", - "1. **Recaptioning Web Images**: The effectiveness of GPT-4 in this application can be measured by the accuracy of the captions it generates. This can be quantified using metrics such as BLEU (Bilingual Evaluation Understudy) or ROUGE (Recall-Oriented Understudy for Gisting Evaluation), which compare the machine-generated text to a set of reference captions. A high score on these metrics would indicate that GPT-4 is capable of understanding and describing the content of images accurately.\n", - "\n", - "2. **Formal Software Verification**: To verify the application of GPT-4 in software verification, we could measure the success rate of the model in generating verification hints that lead to successful program verification. This could be done by comparing the verification success rate with and without the hints provided by GPT-4. Additionally, the complexity of the programs and the hints required could be analyzed to understand the model's limitations and strengths.\n", - "\n", - "3. **Automated Information Extraction from Medical Records**: The performance of GPT-4 in extracting information from medical narratives can be assessed by comparing the extracted information to a gold standard set of annotations. Precision, recall, and F1-score are common metrics used to evaluate information extraction systems. A high F1-score would suggest that GPT-4 is effective in identifying and extracting relevant information from unstructured medical texts.\n", - "\n", - "For each of these applications, it would be important to conduct a thorough analysis of the results to ensure that the model's performance is not only statistically significant but also practically relevant. Additionally, it would be crucial to assess the model's performance on diverse datasets to ensure that it generalizes well across different contexts and does not exhibit biases that could lead to incorrect or unfair outcomes.\n", - "\n", - "If we want to simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "To simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mAll agents have been cleared.\u001b[0m\n" - ] - } - ], - "source": [ - "new_builder = AgentBuilder(\n", - " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", - ")\n", - "agent_list, _ = new_builder.build_from_library(\n", - " building_task, library_path_or_json, llm_config, embedding_model=\"all-mpnet-base-v2\"\n", - ")\n", - "start_task(\n", - " execution_task=\"Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\",\n", - " agent_list=agent_list,\n", - ")\n", - "new_builder.clear_all_agents()" + "cells": [ + { + "cell_type": "markdown", + "id": "6264276d39875995", + "metadata": { + "collapsed": false + }, + "source": [ + "# Automatically Build Multi-agent System from Agent Library\n", + "\n", + "By: [Linxin Song](https://linxins97.github.io/), [Jieyu Zhang](https://jieyuz2.github.io/)\n", + "\n", + "In this notebook, we introduce a new feature for AutoBuild, `build_from_library`, which help users build an automatic task-solving process powered by a multi-agent system from a pre-defined agent library. \n", + "Specifically, in `build_from_library`, we prompt an LLM to explore useful agents from a pre-defined agent library, generating configurations for those agents for a group chat to solve the user's task." + ] + }, + { + "cell_type": "markdown", + "id": "ec78dda8e3826d8a", + "metadata": { + "collapsed": false + }, + "source": [ + "## Requirement\n", + "\n", + "AutoBuild require `pyautogen[autobuild]`, which can be installed by the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e8e9ae50658be975", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%pip install pyautogen[autobuild]" + ] + }, + { + "cell_type": "markdown", + "id": "176c200804af63f3", + "metadata": { + "collapsed": false + }, + "source": [ + "## Preparation and useful tools\n", + "We need to specify a `config_path`, `default_llm_config` that include backbone LLM configurations." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2505f029423b21ab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-26T16:58:02.762702600Z", + "start_time": "2023-12-26T16:58:02.472073Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import autogen\n", + "from autogen.agentchat.contrib.agent_builder import AgentBuilder\n", + "\n", + "config_file_or_env = \"OAI_CONFIG_LIST\" # modify path\n", + "llm_config = {\"temperature\": 0}\n", + "config_list = autogen.config_list_from_json(config_file_or_env, filter_dict={\"model\": [\"gpt-4-1106-preview\", \"gpt-4\"]})\n", + "\n", + "\n", + "def start_task(execution_task: str, agent_list: list):\n", + " group_chat = autogen.GroupChat(agents=agent_list, messages=[], max_round=12)\n", + " manager = autogen.GroupChatManager(groupchat=group_chat, llm_config={\"config_list\": config_list, **llm_config})\n", + " agent_list[0].initiate_chat(manager, message=execution_task)" + ] + }, + { + "cell_type": "markdown", + "id": "5fb3db8885dd6ee6", + "metadata": { + "collapsed": false + }, + "source": [ + "## Example for generating an agent library\n", + "Here, we show an example of generating an agent library from a pre-defined list of agents' names by prompting a `gpt-4`. You can also prepare a handcrafted library yourself.\n", + "\n", + "A Library contains each agent's name, description and system_message. The description is a brief introduction about agent's characteristics. As we will feed all agents' names and description to gpt-4 and let it choose the best agents for us, each agent's description should be simple but informative. \n", + "\n", + "First, we define a prompt template for description and system_message generation and a list of agents' name:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "68315f6ec912c58a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:39:03.317527600Z", + "start_time": "2023-12-23T07:39:03.276859600Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "AGENT_SYS_MSG_PROMPT = \"\"\"Acccording to the following postion name, write a high quality instruction for the position following a given example. You should only return the instruction.\n", + "\n", + "# Position Name\n", + "{position}\n", + "\n", + "# Example instruction for Data Analyst\n", + "\n", + "As Data Analyst, you are tasked with leveraging your extensive knowledge in data analysis to recognize and extract meaningful features from vast datasets. Your expertise in machine learning, specifically with the Random Forest Classifier, allows you to construct robust predictive models adept at handling both classification and regression tasks. You excel in model evaluation and interpretation, ensuring that the performance of your algorithms is not just assessed with precision, but also understood in the context of the data and the problem at hand. With a command over Python and proficiency in using the pandas library, you manipulate and preprocess data with ease.\n", + "\"\"\"\n", + "\n", + "AGENT_DESC_PROMPT = \"\"\"According to position name and the instruction, summarize the position into a high quality one sentence description.\n", + "\n", + "# Position Name\n", + "{position}\n", + "\n", + "# Instruction\n", + "{instruction}\n", + "\"\"\"\n", + "\n", + "position_list = [\n", + " \"Environmental_Scientist\",\n", + " \"Astronomer\",\n", + " \"Software_Developer\",\n", + " \"Data_Analyst\",\n", + " \"Journalist\",\n", + " \"Teacher\",\n", + " \"Lawyer\",\n", + " \"Programmer\",\n", + " \"Accountant\",\n", + " \"Mathematician\",\n", + " \"Physicist\",\n", + " \"Biologist\",\n", + " \"Chemist\",\n", + " \"Statistician\",\n", + " \"IT_Specialist\",\n", + " \"Cybersecurity_Expert\",\n", + " \"Artificial_Intelligence_Engineer\",\n", + " \"Financial_Analyst\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "72b8e7d9d334a5c2", + "metadata": { + "collapsed": false + }, + "source": [ + "Then we can prompt a `gpt-4` model to generate each agent's system message as well as the description:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8fbfef9268fc5191", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.703372Z", + "start_time": "2023-12-23T07:39:04.472589200Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "build_manager = autogen.OpenAIWrapper(config_list=config_list)\n", + "sys_msg_list = []\n", + "\n", + "for pos in position_list:\n", + " resp_agent_sys_msg = (\n", + " build_manager.create(\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": AGENT_SYS_MSG_PROMPT.format(\n", + " position=pos,\n", + " ),\n", + " }\n", + " ]\n", + " )\n", + " .choices[0]\n", + " .message.content\n", + " )\n", + " resp_desc_msg = (\n", + " build_manager.create(\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": AGENT_DESC_PROMPT.format(\n", + " position=pos,\n", + " instruction=resp_agent_sys_msg,\n", + " ),\n", + " }\n", + " ]\n", + " )\n", + " .choices[0]\n", + " .message.content\n", + " )\n", + " sys_msg_list.append({\"name\": pos, \"system_message\": resp_agent_sys_msg, \"description\": resp_desc_msg})" + ] + }, + { + "cell_type": "markdown", + "id": "9e26c6db4befacc5", + "metadata": { + "collapsed": false + }, + "source": [ + "The generated profile will have the following format:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8ede1d7088eb183d", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.712399300Z", + "start_time": "2023-12-23T07:40:01.707400200Z" + }, + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'name': 'Environmental_Scientist',\n", + " 'system_message': 'As an Environmental Scientist, you are responsible for applying your profound knowledge of environmental science to analyze ecological data and assess the impact of human activities on natural resources and ecosystems. Your proficiency in environmental assessment techniques enables you to design and conduct field studies, collect samples, and monitor environmental parameters effectively. Utilizing Geographic Information Systems (GIS), you spatially analyze and visualize environmental data to better understand patterns and changes in the landscape. You are adept at interpreting the results and communicating your findings clearly to stakeholders, policymakers, and the public, thereby contributing to informed decision-making on environmental issues. Your role is essential in developing sustainable practices and recommending mitigation measures to minimize environmental degradation and promote conservation.',\n", + " 'description': 'As an Environmental Scientist, you are tasked with analyzing and assessing the impact of human activities on ecosystems by conducting field studies, using GIS for spatial analysis, and communicating your findings to inform sustainable practices and conservation efforts.'},\n", + " {'name': 'Astronomer',\n", + " 'system_message': 'As an Astronomer, your duty involves diligent observation and analysis of celestial phenomena across the universe. Utilize cutting-edge telescopes and instruments to gather astronomical data, looking for patterns and irregularities that can lead to groundbreaking discoveries. Your profound knowledge in astrophysics is pivotal in interpreting these findings, which may include identifying new celestial objects, scrutinizing the properties and behaviors of stars, planets, and galaxies, and understanding cosmic events. Mastery of complex astronomical software and advanced mathematics is crucial for modeling astronomical phenomena and processing the vast amounts of data. Your role is essential in advancing our understanding of the cosmos, contributing to the broader scientific community by publishing your findings in reputable journals and engaging in peer collaboration to further space exploration and research.',\n", + " 'description': 'An Astronomer is a professional who meticulously observes, analyzes, and interprets celestial phenomena using advanced telescopes and instruments, requiring a deep knowledge of astrophysics, proficiency in mathematical modeling, and collaboration in scientific communities to enhance our comprehension of the universe.'},\n", + " {'name': 'Software_Developer',\n", + " 'system_message': 'As a Software Developer, your objective is to craft, test, and maintain the software that will meet the needs of our users and clients. Your proficiency in programming languages such as Java, C#, or JavaScript is essential, enabling you to write clean, efficient, and maintainable code. You will design algorithms and flowcharts to create systems that are logical and user-friendly. Collaboration with cross-functional teams, including product managers and designers, is crucial in order to understand software requirements and deliver innovative solutions. With your understanding of the software development life cycle, you will work through the processes of coding, debugging, testing, and deployment. You will employ industry best practices such as version control with Git and conduct code reviews to maintain high standards of software quality. Your role places you at the heart of our development efforts, where your technical prowess advances the functionality, scalability, and reliability of our software products.',\n", + " 'description': 'A Software Developer is responsible for designing, coding, testing, and maintaining software that meets client needs using languages like Java, C#, or JavaScript, collaborating with teams, adhering to best practices like Git for version control, and ensuring quality and innovation throughout the development life cycle.'},\n", + " {'name': 'Data_Analyst',\n", + " 'system_message': 'As a Data Analyst, your role is pivotal in interpreting complex data and providing insights that inform strategic decision-making. Utilize your analytical skills to cleanse and organize large sets of structured and unstructured data, ensuring its accuracy and readiness for in-depth analysis. Apply statistical analysis and predictive modeling to uncover trends, patterns, and correlations that drive operational improvements and innovative solutions. Use your proficiency in SQL for database interactions, and harness visualization tools such as Tableau or Power BI to craft compelling stories from data, aiding stakeholders in visualizing the implications of your findings. Stay abreast with the latest analytics techniques and continuously refine your models for enhanced performance, contributing significantly to the data-driven culture of our organization.',\n", + " 'description': 'The Data Analyst interprets complex datasets to provide strategic insights, cleanses and organizes data, performs statistical analysis and predictive modeling to identify trends and inform improvements, utilizes SQL for database management, and employs visualization tools like Tableau or Power BI to effectively communicate findings to stakeholders.'},\n", + " {'name': 'Journalist',\n", + " 'system_message': 'As a Journalist, you are responsible for identifying and pursuing newsworthy stories with the utmost ethical standards and a commitment to factual reporting. Your innate curiosity and excellent communication skills enable you to conduct thorough research and interviews, uncovering the details that make each story compelling and informative. Skilled in both written and verbal storytelling, you craft articles, reports, and features that engage and inform the public, adhering to strict deadlines without compromising on the integrity and accuracy of your work. Proficient in multimedia journalism, you adeptly use digital tools and social media to reach a wider audience, ensuring that your stories have the maximum impact.',\n", + " 'description': 'A Journalist is tasked with ethically sourcing and meticulously reporting newsworthy events, utilizing strong research and storytelling abilities across multiple platforms to accurately inform and engage a diverse audience.'},\n", + " {'name': 'Teacher',\n", + " 'system_message': 'As a Teacher, you are entrusted with the essential responsibility of fostering knowledge and encouraging academic and personal growth in your students. Your deep understanding of pedagogy, coupled with your expertise in the subject matter, enables you to create and deliver curricula that are both engaging and educational. Your adeptness at differentiated instruction allows you to tailor your teaching methods to suit the varied learning styles and needs within your classroom. By skillfully blending traditional teaching techniques with modern educational technology, you facilitate a dynamic and interactive learning environment. You excel in assessment and feedback, not only to gauge student progress but also to continuously improve your own teaching strategies. With strong interpersonal skills, you maintain open lines of communication with students, parents, and colleagues, fostering a collaborative and supportive school community.',\n", + " 'description': \"A Teacher is responsible for cultivating students' knowledge and growth through expertise in pedagogical practices and subject matter, designing engaging curricula, adapting teaching methods to diverse learning needs, integrating technology, and using assessment for continuous improvement while nurturing a cooperative school community.\"},\n", + " {'name': 'Lawyer',\n", + " 'system_message': 'As a Lawyer, you are required to uphold the highest standards of legal proficiency and ethical practice. Your role involves advising clients on their legal rights and responsibilities, as well as representing them in civil and criminal proceedings. You must possess a strong understanding of the law, paired with the ability to analyze case law and legislate history, to construct compelling arguments in support of your client’s position. Your keen attention to detail and dedication to thorough research are crucial in identifying legal precedents and crafting legal documents that adhere to the strictest of procedural standards. Moreover, you must exhibit exceptional negotiation skills to achieve favorable outcomes, whether in the courtroom or at the settlement table. With your articulate verbal and written communication, you clearly and persuasively present cases, explaining complex legal concepts in understandable terms to clients, judges, and juries. Your commitment to confidentiality and upholding justice is paramount and reflected in all aspects of your professional conduct.',\n", + " 'description': 'A Lawyer is a professionally trained legal advocate responsible for representing clients in legal proceedings, providing expert advice on legal matters, constructing persuasive arguments through meticulous research and analysis of law, and negotiating settlements, all while adhering to the highest ethical standards and maintaining strict confidentiality.'},\n", + " {'name': 'Programmer',\n", + " 'system_message': 'As a Programmer, you are responsible for the design, development, and implementation of software programs. Utilize your comprehensive understanding of programming languages, including but not limited to Java, C++, and Python, to create efficient and innovative software solutions. Your role involves writing clean, maintainable code while adhering to best practices in software development. You are expected to troubleshoot, debug, and upgrade existing software, as well as collaborate with cross-functional teams to define and design new product features. Your ability to think algorithmically and solve problems systematically will be integral in creating software that is not only functional but also scalable and secure.',\n", + " 'description': 'A Programmer designs, develops, and implements innovative and efficient software solutions using languages like Java, C++, and Python, ensuring code maintainability, collaborating on new features, and enhancing existing applications with a strong focus on scalability and security.'},\n", + " {'name': 'Accountant',\n", + " 'system_message': 'As Accountant, you are charged with the meticulous management and analysis of financial records, ensuring accuracy and compliance with relevant laws and regulations. Utilize your comprehensive understanding of accounting principles to prepare, examine, and maintain financial reports and statements, including balance sheets and income statements. Your role involves the reconciliation of accounts, evaluating financial operations to recommend best practices, identifying issues, and strategizing solutions for fiscal efficiency and profitability. Mastery in accounting software such as QuickBooks or Sage, alongside proficiency in Microsoft Excel, enables you to efficiently process and analyze financial data. You must ensure proper financial documentation and control systems are in place, providing comprehensive support to the organization’s financial health and integrity.',\n", + " 'description': 'As an Accountant, you are responsible for the accurate and compliant management, analysis, and reporting of financial data, along with recommending strategies to enhance fiscal efficiency and profitability, supported by proficiency in accounting software and Microsoft Excel.'},\n", + " {'name': 'Mathematician',\n", + " 'system_message': 'As a Mathematician, you are responsible for utilizing your profound understanding of mathematical theories and methodologies to solve complex theoretical and practical problems across various domains. Your proficiency in abstract reasoning enables you to develop new mathematical principles and to recognize and articulate the underlying mathematical relationships within real-world scenarios. You apply your expertise in calculus, algebra, statistics, and other mathematical branches to conduct rigorous analyses and to model systems for prediction and optimization. With a strong foundation in logic and quantitative reasoning, you perform peer reviews and contribute to interdisciplinary research projects, ensuring accuracy and consistency in mathematical arguments and results. Your role is crucial in advancing mathematical knowledge and providing innovative solutions to scientific and engineering challenges.',\n", + " 'description': 'As a Mathematician, you apply advanced mathematical theories and analytical skills to solve theoretical and practical problems in various industries, develop new principles, and provide innovative solutions to complex scientific and engineering challenges.'},\n", + " {'name': 'Physicist',\n", + " 'system_message': 'As a Physicist, you are charged with applying your profound understanding of the physical laws that govern the universe to unravel complex scientific phenomena. Your proficiency in theoretical and experimental physics enables you to develop models and conduct experiments that explore fundamental forces and particles. With exceptional analytical skills, you interpret empirical data to validate existing theories or propose new explanations for unexplained observations. Mastery in the use of mathematical tools such as differential equations and linear algebra is crucial for you to simulate physical processes. You are also adept at using specialized software and equipment for data acquisition and analysis, contributing to advancements in fields ranging from quantum mechanics to cosmology. Your strong critical thinking abilities empower you to solve intricate problems, and your commitment to scientific rigor ensures the integrity and accuracy of your research outcomes.',\n", + " 'description': 'A Physicist applies deep knowledge of physical laws to investigate scientific phenomena through theoretical modeling and experimental research, utilizing advanced mathematical techniques and specialized equipment to advance understanding in areas such as quantum mechanics and cosmology.'},\n", + " {'name': 'Biologist',\n", + " 'system_message': 'As a Biologist, you are entrusted with the study and understanding of living organisms, applying your expertise to investigate their functions, genetics, evolution, and ecosystems. Your skills in experimental design empower you to conduct research and experiments that can unlock new biological insights and improve our comprehension of life processes. Utilizing advanced microscopy techniques and molecular biology methods, you should meticulously analyze cell structures and DNA sequences to uncover the intricacies of life at a microscopic level. Demonstrate proficiency in bioinformatics tools to analyze genetic data and contribute valuable findings to the scientific community. Furthermore, as a communicator of science, ensure that your research findings are effectively documented and presented in scientific journals and at conferences, thereby enhancing the collective knowledge in your field.',\n", + " 'description': 'A Biologist meticulously studies and understands living organisms, conducting advanced research to decode genetics and ecosystems and sharing findings through scientific publications and presentations.'},\n", + " {'name': 'Chemist',\n", + " 'system_message': 'As a Chemist, you are charged with applying your profound understanding of chemical principles to conduct complex experiments, synthesize new compounds, and analyze the molecular and atomic structure of materials. Your proficiency in utilizing sophisticated analytical techniques - such as chromatography, spectroscopy, and mass spectrometry - enables you to decipher the composition and properties of substances. The knowledge you hold in chemical safety and handling procedures ensures a secure laboratory environment. With an adeptness in maintaining accurate records and an insightful approach to interpreting data, you transform raw experimental results into valuable scientific insights. Your ability to communicate complex chemical information clearly makes you essential in collaborative research efforts and in driving innovation within the field.',\n", + " 'description': 'As a Chemist, you are responsible for conducting advanced experiments, synthesizing compounds, deciphering substance compositions with techniques like chromatography and mass spectrometry, and transforming experimental data into scientific insights, while maintaining safety and clear communication in research collaborations.'},\n", + " {'name': 'Statistician',\n", + " 'system_message': 'As a Statistician, your primary duty is to apply mathematical and statistical methods to collect, analyze, and interpret numerical data to make informed decisions. Your strong grounding in probability theory will be essential for designing surveys and experiments to generate data. You are adept at constructing and applying sophisticated statistical models and methods, such as linear regression, ANOVA, or time-series analysis, ensuring that you accurately capture trends and relationships within the data. You possess an in-depth understanding of statistical software such as R or SAS, allowing you to perform complex analyses with efficiency and precision. Your ability to communicate complex statistical concepts to non-experts will be crucial; hence, your role includes presenting findings in a clear, actionable manner, with data visualizations and reports that drive strategic planning and policy development.',\n", + " 'description': 'A Statistician employs and interprets advanced statistical techniques to design data-collection processes, analyze data, and present findings in a comprehensible manner, supporting evidence-based decision-making and policy formation.'},\n", + " {'name': 'IT_Specialist',\n", + " 'system_message': 'As an IT Specialist, your primary responsibility is to maintain the integrity and functionality of all our computer systems and networks. Your comprehensive understanding of hardware and software is crucial for diagnosing and resolving technical issues. You are adept at implementing network security measures to protect data and systems from cyber threats. You also play a significant role in systems and software upgrades, ensuring a seamless transition without disrupting workflow. Utilizing your strong problem-solving skills and proficiency in scripting languages, you automate repetitive tasks, enhancing system efficiency. Your ability to communicate effectively with team members and non-technical staff allows you to provide clear guidance and end-user support.',\n", + " 'description': 'An IT Specialist is responsible for upholding and optimizing our computer systems and networks through maintenance, security, upgrades, issue resolution, automation, and providing support and clear communication to both technical and non-technical personnel.'},\n", + " {'name': 'Cybersecurity_Expert',\n", + " 'system_message': \"As a Cybersecurity Expert, you are charged with the responsibility of safeguarding the organization's computer networks and systems. Your deep understanding of cyber threats and mitigation techniques is critical in identifying vulnerabilities and protecting against malicious attacks. Employing your experience with tools such as firewalls, antivirus software, and intrusion detection systems, you will continuously monitor and defend our digital infrastructure. You are expected to conduct regular security audits and penetration testing to simulate cyber attacks and find potential weaknesses before they can be exploited. Your proficiency in risk management frameworks and incident response protocols ensures that you are prepared to swiftly handle and mitigate any security incidents that occur. With your expertise in encryption technologies and network protocols, you protect sensitive data and ensure compliance with relevant security standards and regulations. Your foresight in staying up-to-date with the latest cybersecurity trends and threats is paramount to maintaining the organization's digital defense at its peak.\",\n", + " 'description': \"As a Cybersecurity Expert, you are responsible for the proactive protection and defense of an organization's computer networks and systems against cyber threats through continuous monitoring, conducting security audits, penetrating testing, and swiftly mitigating security incidents, while ensuring compliance with security regulations.\"},\n", + " {'name': 'Artificial_Intelligence_Engineer',\n", + " 'system_message': 'As an Artificial Intelligence Engineer, you are responsible for conceptualizing, designing, and implementing intelligent systems that simulate human cognitive processes. Your role demands a deep understanding of neural networks, particularly Convolutional Neural Networks (CNNs) for image recognition tasks and Recurrent Neural Networks (RNNs) for natural language processing. With your expertise in TensorFlow or PyTorch, you develop complex models that can learn, adapt, and make decisions. You prioritize the ethical design and deployment of AI systems, conscious of the implications your work may have on society. Mastery of algorithms and a proficiency in a high-level programming language, preferably Python, enable you to transform theoretical AI concepts into practical solutions that drive innovation and efficiency.',\n", + " 'description': 'An Artificial Intelligence Engineer specializes in creating and implementing advanced intelligent systems, with a mastery of neural networks, machine learning frameworks, and ethical AI principles, to develop innovative solutions that emulate human cognition.'},\n", + " {'name': 'Financial_Analyst',\n", + " 'system_message': 'As a Financial Analyst, you are entrusted with utilizing your in-depth understanding of financial principles to assess investment opportunities, analyze financial data, and forecast economic trends. Your proficiency in financial modeling is paramount, enabling you to develop complex models that underpin the valuation of stocks, bonds, and other financial instruments. With a sharp eye for detail, you scrutinize company financial statements to derive actionable insights and recommend strategies to optimize financial performance. Your expertise in Excel, especially with advanced functions and formulas, allows you to efficiently manipulate and analyze large financial datasets. You are a whiz at creating compelling visualizations and delivering presentations to communicate your findings and influence strategic decisions. Your role is crucial in guiding investment decisions and driving the fiscal prudence of the organization.',\n", + " 'description': \"A Financial Analyst performs in-depth financial analysis and modeling to evaluate investments, forecast economic trends, and deliver strategic recommendations, leveraging advanced Excel skills to inform and guide the organization's financial decisions.\"}]" ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_msg_list" + ] + }, + { + "cell_type": "markdown", + "id": "256dd32b03a7a172", + "metadata": { + "collapsed": false + }, + "source": [ + "We can save the generated agents' information into a json file." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "53111125938845cf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.750855900Z", + "start_time": "2023-12-23T07:40:01.710399600Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "json.dump(sys_msg_list, open(\"./agent_library_example.json\", \"w\"), indent=4)" + ] + }, + { + "cell_type": "markdown", + "id": "cfd883b79a3bd932", + "metadata": { + "collapsed": false + }, + "source": [ + "## Build agents from library (by LLM)\n", + "Here, we introduce how to build agents from the generated library. As in the previous `build`, we also need to specify a `building_task` that lets the build manager know which agents should be selected from the library according to the task. \n", + "\n", + "We also need to specify a `library_path_or_json`, which can be a path of library or a JSON string with agents' configs. Here, we use the previously saved path as the library path." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8963a8709c8e92e2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.752918500Z", + "start_time": "2023-12-23T07:40:01.735461Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "library_path_or_json = \"./agent_library_example.json\"\n", + "building_task = \"Find a paper on arxiv by programming, and analyze its application in some domain. For example, find a recent paper about gpt-4 on arxiv and find its potential applications in software.\"" + ] + }, + { + "cell_type": "markdown", + "id": "72656a8d0c1a9b12", + "metadata": { + "collapsed": false + }, + "source": [ + "Then, we can call the `build_from_library` from the AgentBuilder to generate a list of agents from the library and let them complete the user's `execution_task` in a group chat." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5c669b76b2c9b750", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:46:02.075542200Z", + "start_time": "2023-12-23T07:43:55.489042900Z" + }, + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Programmer', 'Mathematician'] are selected.\n", + "\u001b[32m==> Creating agents...\u001b[0m\n", + "Creating agent Programmer...\n", + "Creating agent Mathematician...\n", + "Adding user console proxy...\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "To find a recent paper about explainable AI on arXiv, we can use the arXiv API to search for papers that match the query. However, since I cannot directly access external APIs, I suggest that one of us manually searches for the paper on the arXiv website using relevant search terms such as \"explainable AI\" and \"medical applications\". Once we find a suitable paper, we can discuss its potential applications in the medical field. \n", + "\n", + "Mathematician, would you like to perform the search, or shall I provide a Python script that could be used to perform the search programmatically?\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "As a Mathematician, I can certainly appreciate the value of a programmatic approach to searching for academic papers. However, since I do not have direct access to execute code or APIs, I would suggest that you, as the Programmer, provide the Python script that could be used to perform the search on arXiv. Once we have identified a paper, I can then assist in discussing its potential applications in the medical field from a mathematical perspective.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Understood. I will provide a Python script that can be used to search for recent papers about explainable AI on arXiv with potential applications in the medical field. The script will use the `arxiv` Python package, which is a wrapper for the arXiv API. If the package is not installed, we will need to install it first.\n", + "\n", + "Let's start by checking if the `arxiv` package is installed and if not, we will install it. Computer_terminal, please execute the following command to check for the `arxiv` package and install it if necessary.\n", + "\n", + "```sh\n", + "pip show arxiv || pip install arxiv\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is sh)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Name: arxiv\n", + "Version: 2.1.0\n", + "Summary: Python wrapper for the arXiv API: https://arxiv.org/help/api/\n", + "Home-page: https://github.com/lukasschwab/arxiv.py\n", + "Author: Lukas Schwab\n", + "Author-email: lukas.schwab@gmail.com\n", + "License: MIT\n", + "Location: /home/vscode/.local/lib/python3.10/site-packages\n", + "Requires: feedparser, requests\n", + "Required-by: \n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Great, the `arxiv` package is already installed. Now, I will provide a Python script that uses the `arxiv` package to search for recent papers related to explainable AI with potential applications in the medical field. The script will query the arXiv API for papers with relevant keywords and print out the title and summary of the most recent paper found.\n", + "\n", + "Computer_terminal, please execute the following Python script.\n", + "\n", + "```python\n", + "import arxiv\n", + "\n", + "# Define the search query\n", + "search_query = 'all:explainable AI AND all:medical'\n", + "\n", + "# Search for papers on arXiv\n", + "search = arxiv.Search(\n", + " query = search_query,\n", + " max_results = 1,\n", + " sort_by = arxiv.SortCriterion.SubmittedDate\n", + ")\n", + "\n", + "# Fetch the most recent paper\n", + "for paper in search.results():\n", + " print(\"Title:\", paper.title)\n", + " print(\"Summary:\", paper.summary)\n", + " # Only print the most recent paper\n", + " break\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Title: Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", + "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", + "intelligence (AI) has become a pivotal component in the automation of clinical\n", + "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", + "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", + "comparison to the GPT-4 model, aiming to facilitate automated information\n", + "extraction from thyroid operation narratives. The current research landscape is\n", + "dominated by traditional methods heavily reliant on regular expressions, which\n", + "often face challenges in processing free-style text formats containing critical\n", + "details of operation records, including frozen biopsy reports. Addressing this,\n", + "the study leverages advanced natural language processing (NLP) techniques to\n", + "foster a paradigm shift towards more sophisticated data processing systems.\n", + "Through this comparative study, we aspire to unveil a more streamlined,\n", + "precise, and efficient approach to document processing in the healthcare\n", + "domain, potentially revolutionizing the way medical data is handled and\n", + "analyzed.\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "The paper titled \"Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\" presents a study on the use of artificial intelligence for automating the extraction of information from thyroid operation narratives. This is a clear example of explainable AI being applied in the medical field, specifically in the area of clinical workflows and document processing.\n", + "\n", + "The potential applications of such technology in medicine are vast. By automating the extraction of information from operation narratives, healthcare professionals can save time and reduce the likelihood of human error. This can lead to more accurate patient records, improved patient care, and streamlined administrative processes. Additionally, the ability to quickly and accurately process operation records can facilitate better data analysis, which can be used for medical research, trend analysis, and improving healthcare outcomes.\n", + "\n", + "The use of advanced natural language processing (NLP) techniques, as mentioned in the summary, is particularly important for processing free-style text formats that contain critical medical information. This technology could be further explored to extend its application to other types of medical documents and records, enhancing the overall efficiency of the healthcare system.\n", + "\n", + "The study's focus on comparing the performance of the fine-tuned KoELECTRA model with GPT-4 also highlights the importance of evaluating different AI models to determine the most effective approach for specific medical applications. This comparative analysis can lead to the development of more specialized AI tools tailored to the needs of the healthcare industry.\n", + "\n", + "In conclusion, the research presented in this paper has significant implications for the future of medical document processing and the broader integration of AI in healthcare.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "The insights provided by the Mathematician are indeed valuable. The application of AI for automated information extraction from medical documents like thyroid operation narratives can greatly enhance efficiency and accuracy in healthcare. The use of models like GPT-4 and KoELECTRA for natural language processing tasks shows the potential of AI to handle complex, unstructured data which is common in medical records.\n", + "\n", + "From a programming perspective, the implementation of such AI systems would involve training models on large datasets of medical documents to learn the context and semantics specific to medical terminology. Ensuring the explainability of AI in this context is crucial, as healthcare professionals need to understand and trust the AI's decision-making process, especially when it directly affects patient care.\n", + "\n", + "Moreover, the integration of explainable AI into healthcare systems must adhere to strict privacy and security regulations to protect sensitive patient data. This requires careful design and implementation of data handling procedures within the AI system.\n", + "\n", + "The potential applications extend beyond just document processing to diagnostic assistance, personalized treatment plans, and predictive analytics for patient outcomes. As AI technology continues to evolve, its role in supporting and enhancing the capabilities of healthcare professionals will undoubtedly expand.\n", + "\n", + "Given the importance of the topic and the potential impact on healthcare, it would be beneficial to keep an eye on further developments in this field. If there are no further questions or points to discuss, we can conclude our conversation on this topic.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "I agree with the Programmer's assessment. The implementation of AI in healthcare does indeed require careful consideration of the models used, the training data, and the explainability of the AI's decisions. The ethical implications, particularly concerning patient privacy and data security, are paramount and must be addressed with the utmost rigor.\n", + "\n", + "The potential for AI to assist in diagnostics, treatment planning, and predictive analytics is a promising development for the future of medicine. It is essential that these systems are developed in collaboration with healthcare professionals to ensure they meet the real-world needs of the field.\n", + "\n", + "The interdisciplinary nature of this work, combining expertise in mathematics, computer science, and medicine, is a testament to the collaborative efforts needed to advance healthcare technology. It has been a pleasure discussing the potential applications of explainable AI in medicine with you.\n", + "\n", + "If there are no further points to add, I believe we have reached a natural conclusion to our conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mAll agents have been cleared.\u001b[0m\n" + ] + } + ], + "source": [ + "new_builder = AgentBuilder(\n", + " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", + ")\n", + "agent_list, _ = new_builder.build_from_library(building_task, library_path_or_json, llm_config)\n", + "start_task(\n", + " execution_task=\"Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\",\n", + " agent_list=agent_list,\n", + ")\n", + "new_builder.clear_all_agents()" + ] + }, + { + "cell_type": "markdown", + "id": "c7a10e6fa00a5a0d", + "metadata": { + "collapsed": false + }, + "source": [ + "## Build agents from library (by description-task similarity)\n", + "We also support using embedding similarity to select agents. You can use a [Sentence-Transformers model](https://www.sbert.net/docs/pretrained_models.html) as an embedding extractor, and AgentBuilder will select agents with profiles that are the most similar to the building task from the library by comparing their embedding similarity. This will reduce the use of LLMs but may have less accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "521dc5f961efde59", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-26T17:01:29.333975100Z", + "start_time": "2023-12-26T16:58:11.070813500Z" + }, + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" + ] }, - "nbformat": 4, - "nbformat_minor": 5 - } + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Programmer', 'Mathematician'] are selected.\n", + "\u001b[32m==> Creating agents...\u001b[0m\n", + "Creating agent Programmer...\n", + "Creating agent Mathematician...\n", + "Adding user console proxy...\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "To find a recent paper about GPT-4 on arXiv, we can use the arXiv API to search for papers. However, since I can't directly access external APIs, I can write a Python script that you can run on your local machine to perform this search. Would you like me to provide you with such a script?\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "I apologize for the confusion. I will provide a Python script that can be executed by the Computer_terminal to search for recent papers about GPT-4 on arXiv. Let's proceed with that.\n", + "\n", + "```python\n", + "import requests\n", + "from xml.etree import ElementTree\n", + "\n", + "# Define the search parameters and URL for the arXiv API\n", + "search_query = 'all:gpt-4'\n", + "start = 0\n", + "max_results = 5\n", + "sort_by = 'submittedDate'\n", + "sort_order = 'descending'\n", + "url = f'http://export.arxiv.org/api/query?search_query={search_query}&start={start}&max_results={max_results}&sortBy={sort_by}&sortOrder={sort_order}'\n", + "\n", + "# Send a GET request to the arXiv API\n", + "response = requests.get(url)\n", + "\n", + "# Parse the response if it was successful\n", + "if response.status_code == 200:\n", + " root = ElementTree.fromstring(response.content)\n", + " # Find and print the entries (papers)\n", + " for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):\n", + " title = entry.find('{http://www.w3.org/2005/Atom}title').text\n", + " summary = entry.find('{http://www.w3.org/2005/Atom}summary').text\n", + " published = entry.find('{http://www.w3.org/2005/Atom}published').text\n", + " print(f\"Title: {title}\\nSummary: {summary}\\nPublished Date: {published}\\n\")\n", + "else:\n", + " print(f\"Failed to fetch data from arXiv. Status code: {response.status_code}\")\n", + "```\n", + "\n", + "This script will fetch the most recent papers related to GPT-4 from the arXiv API and print out their titles, summaries, and publication dates. Please execute this script to find the information we need.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Title: What If We Recaption Billions of Web Images with LLaMA-3?\n", + "Summary: Web-crawled image-text pairs are inherently noisy. Prior studies demonstrate\n", + "that semantically aligning and enriching textual descriptions of these pairs\n", + "can significantly enhance model training across various vision-language tasks,\n", + "particularly text-to-image generation. However, large-scale investigations in\n", + "this area remain predominantly closed-source. Our paper aims to bridge this\n", + "community effort, leveraging the powerful and \\textit{open-sourced} LLaMA-3, a\n", + "GPT-4 level LLM. Our recaptioning pipeline is simple: first, we fine-tune a\n", + "LLaMA-3-8B powered LLaVA-1.5 and then employ it to recaption 1.3 billion images\n", + "from the DataComp-1B dataset. Our empirical results confirm that this enhanced\n", + "dataset, Recap-DataComp-1B, offers substantial benefits in training advanced\n", + "vision-language models. For discriminative models like CLIP, we observe\n", + "enhanced zero-shot performance in cross-modal retrieval tasks. For generative\n", + "models like text-to-image Diffusion Transformers, the generated images exhibit\n", + "a significant improvement in alignment with users' text instructions,\n", + "especially in following complex queries. Our project page is\n", + "https://www.haqtu.me/Recap-Datacomp-1B/\n", + "\n", + "Published Date: 2024-06-12T17:59:07Z\n", + "\n", + "Title: DafnyBench: A Benchmark for Formal Software Verification\n", + "Summary: We introduce DafnyBench, the largest benchmark of its kind for training and\n", + "evaluating machine learning systems for formal software verification. We test\n", + "the ability of LLMs such as GPT-4 and Claude 3 to auto-generate enough hints\n", + "for the Dafny formal verification engine to successfully verify over 750\n", + "programs with about 53,000 lines of code. The best model and prompting scheme\n", + "achieved 68% success rate, and we quantify how this rate improves when retrying\n", + "with error message feedback and how it deteriorates with the amount of required\n", + "code and hints. We hope that DafnyBench will enable rapid improvements from\n", + "this baseline as LLMs and verification techniques grow in quality.\n", + "\n", + "Published Date: 2024-06-12T17:53:31Z\n", + "\n", + "Title: A Sociotechnical Lens for Evaluating Computer Vision Models: A Case\n", + " Study on Detecting and Reasoning about Gender and Emotion\n", + "Summary: In the evolving landscape of computer vision (CV) technologies, the automatic\n", + "detection and interpretation of gender and emotion in images is a critical area\n", + "of study. This paper investigates social biases in CV models, emphasizing the\n", + "limitations of traditional evaluation metrics such as precision, recall, and\n", + "accuracy. These metrics often fall short in capturing the complexities of\n", + "gender and emotion, which are fluid and culturally nuanced constructs. Our\n", + "study proposes a sociotechnical framework for evaluating CV models,\n", + "incorporating both technical performance measures and considerations of social\n", + "fairness. Using a dataset of 5,570 images related to vaccination and climate\n", + "change, we empirically compared the performance of various CV models, including\n", + "traditional models like DeepFace and FER, and generative models like GPT-4\n", + "Vision. Our analysis involved manually validating the gender and emotional\n", + "expressions in a subset of images to serve as benchmarks. Our findings reveal\n", + "that while GPT-4 Vision outperforms other models in technical accuracy for\n", + "gender classification, it exhibits discriminatory biases, particularly in\n", + "response to transgender and non-binary personas. Furthermore, the model's\n", + "emotion detection skew heavily towards positive emotions, with a notable bias\n", + "towards associating female images with happiness, especially when prompted by\n", + "male personas. These findings underscore the necessity of developing more\n", + "comprehensive evaluation criteria that address both validity and discriminatory\n", + "biases in CV models. Our proposed framework provides guidelines for researchers\n", + "to critically assess CV tools, ensuring their application in communication\n", + "research is both ethical and effective. The significant contribution of this\n", + "study lies in its emphasis on a sociotechnical approach, advocating for CV\n", + "technologies that support social good and mitigate biases rather than\n", + "perpetuate them.\n", + "\n", + "Published Date: 2024-06-12T13:52:30Z\n", + "\n", + "Title: Supportiveness-based Knowledge Rewriting for Retrieval-augmented\n", + " Language Modeling\n", + "Summary: Retrieval-augmented language models (RALMs) have recently shown great\n", + "potential in mitigating the limitations of implicit knowledge in LLMs, such as\n", + "untimely updating of the latest expertise and unreliable retention of long-tail\n", + "knowledge. However, since the external knowledge base, as well as the\n", + "retriever, can not guarantee reliability, potentially leading to the knowledge\n", + "retrieved not being helpful or even misleading for LLM generation. In this\n", + "paper, we introduce Supportiveness-based Knowledge Rewriting (SKR), a robust\n", + "and pluggable knowledge rewriter inherently optimized for LLM generation.\n", + "Specifically, we introduce the novel concept of \"supportiveness\"--which\n", + "represents how effectively a knowledge piece facilitates downstream tasks--by\n", + "considering the perplexity impact of augmented knowledge on the response text\n", + "of a white-box LLM. Based on knowledge supportiveness, we first design a\n", + "training data curation strategy for our rewriter model, effectively identifying\n", + "and filtering out poor or irrelevant rewrites (e.g., with low supportiveness\n", + "scores) to improve data efficacy. We then introduce the direct preference\n", + "optimization (DPO) algorithm to align the generated rewrites to optimal\n", + "supportiveness, guiding the rewriter model to summarize augmented content that\n", + "better improves the final response. Comprehensive evaluations across six\n", + "popular knowledge-intensive tasks and four LLMs have demonstrated the\n", + "effectiveness and superiority of SKR. With only 7B parameters, SKR has shown\n", + "better knowledge rewriting capability over GPT-4, the current state-of-the-art\n", + "general-purpose LLM.\n", + "\n", + "Published Date: 2024-06-12T11:52:35Z\n", + "\n", + "Title: Automated Information Extraction from Thyroid Operation Narrative: A\n", + " Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", + "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", + "intelligence (AI) has become a pivotal component in the automation of clinical\n", + "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", + "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", + "comparison to the GPT-4 model, aiming to facilitate automated information\n", + "extraction from thyroid operation narratives. The current research landscape is\n", + "dominated by traditional methods heavily reliant on regular expressions, which\n", + "often face challenges in processing free-style text formats containing critical\n", + "details of operation records, including frozen biopsy reports. Addressing this,\n", + "the study leverages advanced natural language processing (NLP) techniques to\n", + "foster a paradigm shift towards more sophisticated data processing systems.\n", + "Through this comparative study, we aspire to unveil a more streamlined,\n", + "precise, and efficient approach to document processing in the healthcare\n", + "domain, potentially revolutionizing the way medical data is handled and\n", + "analyzed.\n", + "\n", + "Published Date: 2024-06-12T06:44:05Z\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Based on the search results from the arXiv API, we have found several papers that discuss potential applications of GPT-4 in software:\n", + "\n", + "1. **Recaptioning Web Images with LLaMA-3 and GPT-4**: This paper discusses the use of GPT-4 level LLMs for recaptioning web images, which can enhance model training across various vision-language tasks. This has implications for improving the quality of datasets used in machine learning and could be particularly beneficial for text-to-image generation and cross-modal retrieval tasks.\n", + "\n", + "2. **DafnyBench: A Benchmark for Formal Software Verification**: This paper introduces a benchmark for training and evaluating machine learning systems for formal software verification. It tests the ability of LLMs such as GPT-4 to auto-generate hints for the Dafny formal verification engine to successfully verify programs. This application could significantly impact the field of software verification by automating the generation of verification hints, potentially improving the efficiency and reliability of the verification process.\n", + "\n", + "3. **Automated Information Extraction from Thyroid Operation Narrative**: This study compares the GPT-4 model with the fine-tuned KoELECTRA model for automated information extraction from thyroid operation narratives. The application of GPT-4 in this context could revolutionize document processing in healthcare by providing a more efficient and accurate method for extracting information from medical records.\n", + "\n", + "These papers suggest that GPT-4 has the potential to be applied in various software-related fields, including enhancing datasets for machine learning, formal software verification, and healthcare document processing. The applications in these papers could lead to more efficient, accurate, and reliable software systems across different domains.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "The applications mentioned indeed highlight the versatility of GPT-4 in different domains. To further understand the potential impact of GPT-4 on software, let's delve into the mathematical and algorithmic principles that could be at play in these applications:\n", + "\n", + "1. **Recaptioning Web Images**: The process of recaptioning images with a language model like GPT-4 involves understanding the context of an image and generating descriptive text that accurately reflects its content. This task likely involves a combination of computer vision techniques to interpret the image and natural language processing to generate the caption. From a mathematical perspective, this would involve optimization algorithms to fine-tune the language model on a specific dataset, ensuring that the generated captions are both semantically and syntactically correct.\n", + "\n", + "2. **Formal Software Verification**: The use of GPT-4 to auto-generate hints for formal verification engines like Dafny involves the model understanding the logic and structure of the code. This requires a deep understanding of formal logic, proof theory, and possibly type theory if the language being verified is statically typed. The success rate of auto-generated hints would depend on the model's ability to reason about the correctness of code and the underlying mathematical properties that ensure its validity.\n", + "\n", + "3. **Automated Information Extraction from Medical Records**: For GPT-4 to extract information from medical narratives, it must process unstructured text and identify relevant medical terms and their relationships. This task involves natural language understanding, which from a mathematical standpoint, can be seen as a form of pattern recognition and classification. The model would need to be trained on a large corpus of medical texts, and its performance would be measured by its precision and recall in identifying and extracting the correct information.\n", + "\n", + "In each of these applications, GPT-4's effectiveness would be influenced by the underlying mathematical models, such as neural networks, and the optimization techniques used during training, such as gradient descent. The quality of the training data and the model's architecture (e.g., attention mechanisms, transformer layers) also play a crucial role in its performance.\n", + "\n", + "To verify the potential of GPT-4 in these applications, one could set up experiments to measure the performance of GPT-4 against specific benchmarks or metrics relevant to each domain. For example, in the case of formal software verification, one could measure the percentage of programs that are successfully verified with the hints generated by GPT-4 compared to a baseline or human-generated hints.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "To further verify the potential applications of GPT-4 in software, we can consider the following:\n", + "\n", + "1. **Recaptioning Web Images**: The effectiveness of GPT-4 in this application can be measured by the accuracy of the captions it generates. This can be quantified using metrics such as BLEU (Bilingual Evaluation Understudy) or ROUGE (Recall-Oriented Understudy for Gisting Evaluation), which compare the machine-generated text to a set of reference captions. A high score on these metrics would indicate that GPT-4 is capable of understanding and describing the content of images accurately.\n", + "\n", + "2. **Formal Software Verification**: To verify the application of GPT-4 in software verification, we could measure the success rate of the model in generating verification hints that lead to successful program verification. This could be done by comparing the verification success rate with and without the hints provided by GPT-4. Additionally, the complexity of the programs and the hints required could be analyzed to understand the model's limitations and strengths.\n", + "\n", + "3. **Automated Information Extraction from Medical Records**: The performance of GPT-4 in extracting information from medical narratives can be assessed by comparing the extracted information to a gold standard set of annotations. Precision, recall, and F1-score are common metrics used to evaluate information extraction systems. A high F1-score would suggest that GPT-4 is effective in identifying and extracting relevant information from unstructured medical texts.\n", + "\n", + "For each of these applications, it would be important to conduct a thorough analysis of the results to ensure that the model's performance is not only statistically significant but also practically relevant. Additionally, it would be crucial to assess the model's performance on diverse datasets to ensure that it generalizes well across different contexts and does not exhibit biases that could lead to incorrect or unfair outcomes.\n", + "\n", + "If we want to simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "To simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mAll agents have been cleared.\u001b[0m\n" + ] + } + ], + "source": [ + "new_builder = AgentBuilder(\n", + " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", + ")\n", + "agent_list, _ = new_builder.build_from_library(\n", + " building_task, library_path_or_json, llm_config, embedding_model=\"all-mpnet-base-v2\"\n", + ")\n", + "start_task(\n", + " execution_task=\"Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\",\n", + " agent_list=agent_list,\n", + ")\n", + "new_builder.clear_all_agents()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/lats_search.ipynb b/notebook/lats_search.ipynb new file mode 100644 index 000000000000..fe7d362aa041 --- /dev/null +++ b/notebook/lats_search.ipynb @@ -0,0 +1,1057 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "211913e6", + "metadata": {}, + "source": [ + "# Language Agent Tree Search\n", + "\n", + "[Language Agent Tree Search](https://arxiv.org/abs/2310.04406) (LATS), by Zhou, et. al, is a general LLM agent search algorithm that combines reflection/evaluation and search (specifically Monte-Carlo tree search) to achieve stronger overall task performance by leveraging inference-time compute.\n", + "\n", + "It has four main phases consisting of six steps:\n", + "\n", + "1. Select: pick the best next state to progress from, based on its aggregate value. \n", + "2. Expand and simulate: sample n potential actions to take and execute them in parallel.\n", + "3. Reflect + Evaluate: observe the outcomes of these actions and score the decisions based on reflection (and possibly external feedback if available)\n", + "4. Backpropagate: update the scores of the root trajectories based on the outcomes.\n", + "\n", + "![lats](https://i.postimg.cc/NjQScLTv/image.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da705b29", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import logging\n", + "import os\n", + "import uuid\n", + "from typing import Any, Dict, List\n", + "\n", + "from autogen import AssistantAgent, ConversableAgent, GroupChat, UserProxyAgent, config_list_from_json" + ] + }, + { + "cell_type": "markdown", + "id": "293fd23b", + "metadata": {}, + "source": [ + "# Configure logging\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a02f8a2c", + "metadata": {}, + "outputs": [], + "source": [ + "logging.basicConfig(level=logging.INFO)" + ] + }, + { + "cell_type": "markdown", + "id": "1d5ca06b", + "metadata": {}, + "source": [ + "# Set environment variables\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1566c7df", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"AUTOGEN_USE_DOCKER\"] = \"0\" # Disable Docker usage globally for Autogen\n", + "os.environ[\"OPENAI_API_KEY\"] = \"YOUR_API_KEY\"" + ] + }, + { + "cell_type": "markdown", + "id": "585654ac", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Install `autogen` (for the LLM framework and agents)\n", + "\n", + "Required packages: autogen\n", + "\n", + "Please ensure these packages are installed before running this script" + ] + }, + { + "cell_type": "markdown", + "id": "586bcf0f", + "metadata": {}, + "source": [ + "# Directly create the config_list with the API key" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9eaf711f", + "metadata": {}, + "outputs": [], + "source": [ + "config_list = [{\"model\": \"gpt-4o-mini\", \"api_key\": \"YOUR_API_KEY\"}]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79701018", + "metadata": {}, + "outputs": [], + "source": [ + "if not config_list:\n", + " raise ValueError(\"Failed to create configuration. Please check the API key.\")" + ] + }, + { + "cell_type": "markdown", + "id": "9041e0a3", + "metadata": {}, + "source": [ + "### Reflection Class\n", + "\n", + "The reflection chain will score agent outputs based on the decision and the tool responses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce0288e9", + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import BaseModel, Field\n", + "\n", + "\n", + "class Reflection(BaseModel):\n", + " reflections: str = Field(\n", + " description=\"The critique and reflections on the sufficiency, superfluency,\"\n", + " \" and general quality of the response\"\n", + " )\n", + " score: int = Field(\n", + " description=\"Score from 0-10 on the quality of the candidate response.\",\n", + " gte=0,\n", + " lte=10,\n", + " )\n", + " found_solution: bool = Field(description=\"Whether the response has fully solved the question or task.\")\n", + "\n", + " def as_message(self):\n", + " return {\"role\": \"human\", \"content\": f\"Reasoning: {self.reflections}\\nScore: {self.score}\"}\n", + "\n", + " @property\n", + " def normalized_score(self) -> float:\n", + " return self.score / 10.0" + ] + }, + { + "cell_type": "markdown", + "id": "1f6d3476", + "metadata": {}, + "source": [ + "## Tree State\n", + "\n", + "LATS is based on a (greedy) Monte-Carlo tree search. For each search steps, it picks the node with the highest \"upper confidence bound\", which is a metric that balances exploitation (highest average reward) and exploration (lowest visits). Starting from that node, it generates N (5 in this case) new candidate actions to take, and adds them to the tree. It stops searching either when it has generated a valid solution OR when it has reached the maximum number of rollouts (search tree depth)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6d0d7a6", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "from collections import deque\n", + "from typing import Optional" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "305a29d6", + "metadata": {}, + "outputs": [], + "source": [ + "class Node:\n", + " def __init__(\n", + " self,\n", + " messages: List[Dict[str, str]],\n", + " reflection: Optional[Reflection] = None,\n", + " parent: Optional[\"Node\"] = None,\n", + " ):\n", + " self.messages = messages\n", + " self.parent = parent\n", + " self.children: List[\"Node\"] = []\n", + " self.value = 0.0\n", + " self.visits = 0\n", + " self.reflection = reflection\n", + " self.depth = parent.depth + 1 if parent is not None else 1\n", + " self._is_solved = reflection.found_solution if reflection else False\n", + " if self._is_solved:\n", + " self._mark_tree_as_solved()\n", + " if reflection:\n", + " self.backpropagate(reflection.normalized_score)\n", + "\n", + " def __repr__(self) -> str:\n", + " return (\n", + " f\"\"\n", + " )\n", + "\n", + " @property\n", + " def is_solved(self) -> bool:\n", + " \"\"\"If any solutions exist, we can end the search.\"\"\"\n", + " return self._is_solved\n", + "\n", + " @property\n", + " def is_terminal(self):\n", + " return not self.children\n", + "\n", + " @property\n", + " def best_child(self):\n", + " \"\"\"Select the child with the highest UCT to search next.\"\"\"\n", + " if not self.children:\n", + " return None\n", + " all_nodes = self._get_all_children()\n", + " return max(all_nodes, key=lambda child: child.upper_confidence_bound())\n", + "\n", + " @property\n", + " def best_child_score(self):\n", + " \"\"\"Return the child with the highest value.\"\"\"\n", + " if not self.children:\n", + " return None\n", + " return max(self.children, key=lambda child: int(child.is_solved) * child.value)\n", + "\n", + " @property\n", + " def height(self) -> int:\n", + " \"\"\"Check for how far we've rolled out the tree.\"\"\"\n", + " if self.children:\n", + " return 1 + max([child.height for child in self.children])\n", + " return 1\n", + "\n", + " def upper_confidence_bound(self, exploration_weight=1.0):\n", + " \"\"\"Return the UCT score. This helps balance exploration vs. exploitation of a branch.\"\"\"\n", + " if self.parent is None:\n", + " raise ValueError(\"Cannot obtain UCT from root node\")\n", + " if self.visits == 0:\n", + " return self.value\n", + " # Encourages exploitation of high-value trajectories\n", + " average_reward = self.value / self.visits\n", + " exploration_term = math.sqrt(math.log(self.parent.visits) / self.visits)\n", + " return average_reward + exploration_weight * exploration_term\n", + "\n", + " def backpropagate(self, reward: float):\n", + " \"\"\"Update the score of this node and its parents.\"\"\"\n", + " node = self\n", + " while node:\n", + " node.visits += 1\n", + " node.value = (node.value * (node.visits - 1) + reward) / node.visits\n", + " node = node.parent\n", + "\n", + " def get_messages(self, include_reflections: bool = True):\n", + " if include_reflections and self.reflection:\n", + " return self.messages + [self.reflection.as_message()]\n", + " return self.messages\n", + "\n", + " def get_trajectory(self, include_reflections: bool = True) -> List[Dict[str, str]]:\n", + " \"\"\"Get messages representing this search branch.\"\"\"\n", + " messages = []\n", + " node = self\n", + " while node:\n", + " messages.extend(node.get_messages(include_reflections=include_reflections)[::-1])\n", + " node = node.parent\n", + " # Reverse the final back-tracked trajectory to return in the correct order\n", + " return messages[::-1] # root solution, reflection, child 1, ...\n", + "\n", + " def _get_all_children(self):\n", + " all_nodes = []\n", + " nodes = deque()\n", + " nodes.append(self)\n", + " while nodes:\n", + " node = nodes.popleft()\n", + " all_nodes.extend(node.children)\n", + " for n in node.children:\n", + " nodes.append(n)\n", + " return all_nodes\n", + "\n", + " def get_best_solution(self):\n", + " \"\"\"Return the best solution from within the current sub-tree.\"\"\"\n", + " all_nodes = [self] + self._get_all_children()\n", + " best_node = max(\n", + " all_nodes,\n", + " # We filter out all non-terminal, non-solution trajectories\n", + " key=lambda node: int(node.is_terminal and node.is_solved) * node.value,\n", + " )\n", + " return best_node\n", + "\n", + " def _mark_tree_as_solved(self):\n", + " parent = self.parent\n", + " while parent:\n", + " parent._is_solved = True\n", + " parent = parent.parent" + ] + }, + { + "cell_type": "markdown", + "id": "98b719d9", + "metadata": {}, + "source": [ + "The main component is the tree, represented by the root node." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "586d953a", + "metadata": {}, + "outputs": [], + "source": [ + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class TreeState(TypedDict):\n", + " # The full tree\n", + " root: Node\n", + " # The original input\n", + " input: str" + ] + }, + { + "cell_type": "markdown", + "id": "3a61a6ee", + "metadata": {}, + "source": [ + "## Define Language Agent\n", + "\n", + "Our agent will have three primary LLM-powered processes:\n", + "\n", + "1. Reflect: score the action based on the tool response.\n", + "2. Initial response: to create the root node and start the search.\n", + "3. Expand: generate 5 candidate \"next steps\" from the best spot in the current tree\n", + "\n", + "For more \"Grounded\" tool applications (such as code synthesis), you could integrate code execution into the reflection/reward step. This type of external feedback is very useful." + ] + }, + { + "cell_type": "markdown", + "id": "a9e6c27f", + "metadata": {}, + "source": [ + "#### Tools\n", + "For our example, we will give the language agent a search engine." + ] + }, + { + "cell_type": "markdown", + "id": "ffb10a00", + "metadata": {}, + "source": [ + "Define the UserProxyAgent with web search / tool-use capability\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e467f73e", + "metadata": {}, + "outputs": [], + "source": [ + "user_proxy = UserProxyAgent(\n", + " name=\"user\",\n", + " human_input_mode=\"NEVER\",\n", + " max_consecutive_auto_reply=10,\n", + " code_execution_config={\n", + " \"work_dir\": \"web\",\n", + " \"use_docker\": False,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5c2b96b2", + "metadata": {}, + "source": [ + "Create a ConversableAgent without tools\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "212daaef", + "metadata": {}, + "outputs": [], + "source": [ + "assistant_agent = ConversableAgent(\n", + " name=\"assistant_agent\",\n", + " system_message=\"You are an AI assistant capable of helping with various tasks.\",\n", + " human_input_mode=\"NEVER\",\n", + " code_execution_config=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "527c1a39", + "metadata": {}, + "source": [ + "### Reflection\n", + "\n", + "Self-reflection allows the agent to boostrap, improving its future responses based on the outcome of previous ones. In agents this is more powerful since it can use external feedback to improve." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bdd8a23", + "metadata": {}, + "outputs": [], + "source": [ + "reflection_prompt = \"\"\"\n", + "Reflect and grade the assistant response to the user question below.\n", + "User question: {input}\n", + "Assistant response: {candidate}\n", + "\n", + "Provide your reflection in the following format:\n", + "Reflections: [Your detailed critique and reflections]\n", + "Score: [A score from 0-10]\n", + "Found Solution: [true/false]\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7750d32f", + "metadata": {}, + "outputs": [], + "source": [ + "reflection_agent = AssistantAgent(\n", + " name=\"reflection_agent\",\n", + " system_message=\"You are an AI assistant that reflects on and grades responses.\",\n", + " llm_config={\n", + " \"config_list\": config_list,\n", + " \"temperature\": 0.2,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23f26bf0", + "metadata": {}, + "outputs": [], + "source": [ + "def reflection_chain(inputs: Dict[str, Any]) -> Reflection:\n", + " try:\n", + " candidate_content = \"\"\n", + " if \"candidate\" in inputs:\n", + " candidate = inputs[\"candidate\"]\n", + " if isinstance(candidate, list):\n", + " candidate_content = (\n", + " candidate[-1][\"content\"]\n", + " if isinstance(candidate[-1], dict) and \"content\" in candidate[-1]\n", + " else str(candidate[-1])\n", + " )\n", + " elif isinstance(candidate, dict):\n", + " candidate_content = candidate.get(\"content\", str(candidate))\n", + " elif isinstance(candidate, str):\n", + " candidate_content = candidate\n", + " else:\n", + " candidate_content = str(candidate)\n", + "\n", + " formatted_prompt = [\n", + " {\"role\": \"system\", \"content\": \"You are an AI assistant that reflects on and grades responses.\"},\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": reflection_prompt.format(input=inputs.get(\"input\", \"\"), candidate=candidate_content),\n", + " },\n", + " ]\n", + " response = reflection_agent.generate_reply(formatted_prompt)\n", + "\n", + " # Parse the response\n", + " response_str = str(response)\n", + " lines = response_str.split(\"\\n\")\n", + " reflections = next((line.split(\": \", 1)[1] for line in lines if line.startswith(\"Reflections:\")), \"\")\n", + " score_str = next((line.split(\": \", 1)[1] for line in lines if line.startswith(\"Score:\")), \"0\")\n", + " try:\n", + " if \"/\" in score_str:\n", + " numerator, denominator = map(int, score_str.split(\"/\"))\n", + " score = int((numerator / denominator) * 10)\n", + " else:\n", + " score = int(score_str)\n", + " except ValueError:\n", + " logging.warning(f\"Invalid score value: {score_str}. Defaulting to 0.\")\n", + " score = 0\n", + "\n", + " found_solution = next(\n", + " (line.split(\": \", 1)[1].lower() == \"true\" for line in lines if line.startswith(\"Found Solution:\")), False\n", + " )\n", + "\n", + " if not reflections:\n", + " logging.warning(\"No reflections found in the response. Using default values.\")\n", + " reflections = \"No reflections provided.\"\n", + "\n", + " return Reflection(reflections=reflections, score=score, found_solution=found_solution)\n", + " except Exception as e:\n", + " logging.error(f\"Error in reflection_chain: {str(e)}\", exc_info=True)\n", + " return Reflection(reflections=f\"Error in reflection: {str(e)}\", score=0, found_solution=False)" + ] + }, + { + "cell_type": "markdown", + "id": "fc4b9911", + "metadata": {}, + "source": [ + "### Initial Response\n", + "\n", + "We start with a single root node, generated by this first step. It responds to the user input either with a tool invocation or a response." + ] + }, + { + "cell_type": "markdown", + "id": "60675131", + "metadata": {}, + "source": [ + "# Create Autogen agents\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd743ab5", + "metadata": {}, + "outputs": [], + "source": [ + "assistant = AssistantAgent(name=\"assistant\", llm_config={\"config_list\": config_list}, code_execution_config=False)\n", + "user = UserProxyAgent(\n", + " name=\"user\",\n", + " human_input_mode=\"NEVER\",\n", + " max_consecutive_auto_reply=10,\n", + " code_execution_config={\"work_dir\": \"web\", \"use_docker\": False},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1f93b734", + "metadata": {}, + "source": [ + "# Define a function to create the initial prompt\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7e00575", + "metadata": {}, + "outputs": [], + "source": [ + "def create_initial_prompt(input_text):\n", + " return [\n", + " {\"role\": \"system\", \"content\": \"You are an AI assistant.\"},\n", + " {\"role\": \"user\", \"content\": input_text},\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "b8442317", + "metadata": {}, + "source": [ + "# Function to generate initial response\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7afcd1b", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_initial_response(state: TreeState) -> TreeState:\n", + " chat_messages = create_initial_prompt(state[\"input\"])\n", + " try:\n", + " # Ensure chat_messages is a list of dictionaries\n", + " if not isinstance(chat_messages, list):\n", + " chat_messages = [{\"role\": \"user\", \"content\": chat_messages}]\n", + "\n", + " logging.info(f\"Generating initial response for input: {state['input']}\")\n", + " logging.debug(f\"Chat messages: {chat_messages}\")\n", + "\n", + " response = assistant.generate_reply(chat_messages)\n", + " logging.debug(f\"Raw response from assistant: {response}\")\n", + "\n", + " # Ensure response is properly formatted as a string\n", + " if isinstance(response, str):\n", + " content = response\n", + " elif isinstance(response, dict) and \"content\" in response:\n", + " content = response[\"content\"]\n", + " elif isinstance(response, list) and len(response) > 0:\n", + " content = response[-1].get(\"content\", str(response[-1]))\n", + " else:\n", + " content = str(response)\n", + "\n", + " content = content.strip()\n", + " if not content:\n", + " raise ValueError(\"Generated content is empty after processing\")\n", + "\n", + " logging.debug(f\"Processed content: {content[:100]}...\") # Log first 100 chars\n", + "\n", + " # Generate reflection\n", + " reflection_input = {\"input\": state[\"input\"], \"candidate\": content}\n", + " logging.info(\"Generating reflection on the initial response\")\n", + " reflection = reflection_chain(reflection_input)\n", + " logging.debug(f\"Reflection generated: {reflection}\")\n", + "\n", + " # Create Node with messages as a list containing a single dict\n", + " messages = [{\"role\": \"assistant\", \"content\": content}]\n", + " root = Node(messages=messages, reflection=reflection)\n", + "\n", + " logging.info(\"Initial response and reflection generated successfully\")\n", + " return TreeState(root=root, input=state[\"input\"])\n", + "\n", + " except Exception as e:\n", + " logging.error(f\"Error in generate_initial_response: {str(e)}\", exc_info=True)\n", + " return TreeState(root=None, input=state[\"input\"])" + ] + }, + { + "cell_type": "markdown", + "id": "87ef17ca", + "metadata": {}, + "source": [ + "# Example usage of the generate_initial_response function\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ab75669", + "metadata": {}, + "outputs": [], + "source": [ + "initial_prompt = \"Why is the sky blue?\"\n", + "initial_state = TreeState(input=initial_prompt, root=None)\n", + "result_state = generate_initial_response(initial_state)\n", + "if result_state[\"root\"] is not None:\n", + " print(result_state[\"root\"].messages[0][\"content\"])\n", + "else:\n", + " print(\"Failed to generate initial response.\")" + ] + }, + { + "cell_type": "markdown", + "id": "e619223f", + "metadata": {}, + "source": [ + "#### Starting Node\n", + "\n", + "We will package up the candidate generation and reflection in a single node of our graph. This is represented by the following function:" + ] + }, + { + "cell_type": "markdown", + "id": "24c052e0", + "metadata": {}, + "source": [ + "\n", + "# Define the function to generate the initial response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94c92498", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function to generate the initial response\n", + "\n", + "\n", + "def generate_initial_response(state: TreeState) -> TreeState:\n", + " \"\"\"Generate the initial candidate response using Autogen components.\"\"\"\n", + " assistant = AssistantAgent(name=\"assistant\", llm_config={\"config_list\": config_list}, code_execution_config=False)\n", + "\n", + " # Generate initial response\n", + " initial_message = [\n", + " {\"role\": \"system\", \"content\": \"You are an AI assistant.\"},\n", + " {\"role\": \"user\", \"content\": state[\"input\"]},\n", + " ]\n", + "\n", + " try:\n", + " logging.info(f\"Generating initial response for input: {state['input']}\")\n", + " response = assistant.generate_reply(initial_message)\n", + " logging.debug(f\"Raw response from assistant: {response}\")\n", + "\n", + " # Ensure response is properly formatted as a string\n", + " if isinstance(response, str):\n", + " content = response\n", + " elif isinstance(response, dict):\n", + " content = response.get(\"content\", \"\")\n", + " if not content:\n", + " content = json.dumps(response)\n", + " elif isinstance(response, list):\n", + " content = \" \".join(str(item) for item in response)\n", + " else:\n", + " content = str(response)\n", + "\n", + " # Ensure content is always a string and not empty\n", + " content = content.strip()\n", + " if not content:\n", + " raise ValueError(\"Generated content is empty after processing\")\n", + "\n", + " logging.debug(f\"Final processed content (first 100 chars): {content[:100]}...\")\n", + "\n", + " # Generate reflection\n", + " logging.info(\"Generating reflection on the initial response\")\n", + " reflection_input = {\"input\": state[\"input\"], \"candidate\": content}\n", + " reflection = reflection_chain(reflection_input)\n", + " logging.debug(f\"Reflection generated: {reflection}\")\n", + "\n", + " if not isinstance(reflection, Reflection):\n", + " raise TypeError(f\"Invalid reflection type: {type(reflection)}. Expected Reflection, got {type(reflection)}\")\n", + "\n", + " # Create Node with messages as a list containing a single dict\n", + " messages = [{\"role\": \"assistant\", \"content\": content}]\n", + " logging.debug(f\"Creating Node with messages: {messages}\")\n", + " root = Node(messages=messages, reflection=reflection)\n", + " logging.info(\"Initial response and reflection generated successfully\")\n", + " logging.debug(f\"Created root node: {root}\")\n", + " return TreeState(root=root, input=state[\"input\"])\n", + "\n", + " except Exception as e:\n", + " logging.error(f\"Error in generate_initial_response: {str(e)}\", exc_info=True)\n", + " return TreeState(root=None, input=state[\"input\"])" + ] + }, + { + "cell_type": "markdown", + "id": "c58a4074", + "metadata": {}, + "source": [ + "### Candidate Generation\n", + "The following code prompts the same LLM to generate N additional candidates to check.\n", + "\n", + "This generates N candidate values for a single input to sample actions from the environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27a3a1db", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_candidates(messages: list, config: dict):\n", + " n = config.get(\"N\", 5)\n", + " assistant = AssistantAgent(name=\"assistant\", llm_config={\"config_list\": config_list}, code_execution_config=False)\n", + "\n", + " candidates = []\n", + " for _ in range(n):\n", + " try:\n", + " # Use the assistant to generate a response\n", + " last_message = messages[-1][\"content\"] if messages and isinstance(messages[-1], dict) else str(messages[-1])\n", + " response = assistant.generate_reply([{\"role\": \"user\", \"content\": last_message}])\n", + " if isinstance(response, str):\n", + " candidates.append(response)\n", + " elif isinstance(response, dict) and \"content\" in response:\n", + " candidates.append(response[\"content\"])\n", + " elif (\n", + " isinstance(response, list) and response and isinstance(response[-1], dict) and \"content\" in response[-1]\n", + " ):\n", + " candidates.append(response[-1][\"content\"])\n", + " else:\n", + " candidates.append(str(response))\n", + " except Exception as e:\n", + " logging.error(f\"Error generating candidate: {str(e)}\")\n", + " candidates.append(\"Failed to generate candidate.\")\n", + "\n", + " if not candidates:\n", + " logging.warning(\"No candidates were generated.\")\n", + "\n", + " return candidates\n", + "\n", + "\n", + "expansion_chain = generate_candidates" + ] + }, + { + "cell_type": "markdown", + "id": "a47c8161", + "metadata": {}, + "source": [ + "#### Candidate generation node\n", + "\n", + "We will package the candidate generation and reflection steps in the following \"expand\" node.\n", + "We do all the operations as a batch process to speed up execution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "175afca7", + "metadata": {}, + "outputs": [], + "source": [ + "def expand(state: TreeState, config: Dict[str, Any]) -> dict:\n", + " root = state[\"root\"]\n", + " best_candidate: Node = root.best_child if root.children else root\n", + " messages = best_candidate.get_trajectory()\n", + "\n", + " # Generate N candidates using Autogen's generate_candidates function\n", + " new_candidates = generate_candidates(messages, config)\n", + "\n", + " # Reflect on each candidate using Autogen's AssistantAgent\n", + " reflections = []\n", + " for candidate in new_candidates:\n", + " reflection = reflection_chain({\"input\": state[\"input\"], \"candidate\": candidate})\n", + " reflections.append(reflection)\n", + "\n", + " # Grow tree\n", + " child_nodes = [\n", + " Node([{\"role\": \"assistant\", \"content\": candidate}], parent=best_candidate, reflection=reflection)\n", + " for candidate, reflection in zip(new_candidates, reflections)\n", + " ]\n", + " best_candidate.children.extend(child_nodes)\n", + "\n", + " # We have already extended the tree directly, so we just return the state\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "id": "717b7b93", + "metadata": {}, + "source": [ + "## Create Tree\n", + "\n", + "With those two nodes defined, we are ready to define the tree. After each agent step, we have the option of finishing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e309ea9f", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any, Dict, Literal\n", + "\n", + "\n", + "def should_loop(state: Dict[str, Any]) -> Literal[\"expand\", \"end\"]:\n", + " \"\"\"Determine whether to continue the tree search.\"\"\"\n", + " root = state[\"root\"]\n", + " if root.is_solved:\n", + " return \"end\"\n", + " if root.height > 5:\n", + " return \"end\"\n", + " return \"expand\"\n", + "\n", + "\n", + "def run_lats(input_query: str, max_iterations: int = 10):\n", + " import logging\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " try:\n", + "\n", + " state = {\"input\": input_query, \"root\": None}\n", + " try:\n", + " state = generate_initial_response(state)\n", + " if not isinstance(state, dict) or \"root\" not in state or state[\"root\"] is None:\n", + " logger.error(\"Initial response generation failed or returned invalid state\")\n", + " return \"Failed to generate initial response.\"\n", + " logger.info(\"Initial response generated successfully\")\n", + " except Exception as e:\n", + " logger.error(f\"Error generating initial response: {str(e)}\", exc_info=True)\n", + " return \"Failed to generate initial response due to an unexpected error.\"\n", + "\n", + " for iteration in range(max_iterations):\n", + " action = should_loop(state)\n", + " if action == \"end\":\n", + " logger.info(f\"Search ended after {iteration + 1} iterations\")\n", + " break\n", + " try:\n", + " state = expand(\n", + " state,\n", + " {\n", + " \"N\": 5,\n", + " \"input_query\": input_query,\n", + " },\n", + " )\n", + " logger.info(f\"Completed iteration {iteration + 1}\")\n", + " except Exception as e:\n", + " logger.error(f\"Error during iteration {iteration + 1}: {str(e)}\", exc_info=True)\n", + " continue\n", + "\n", + " if not isinstance(state, dict) or \"root\" not in state or state[\"root\"] is None:\n", + " return \"No valid solution found due to an error in the search process.\"\n", + "\n", + " solution_node = state[\"root\"].get_best_solution()\n", + " best_trajectory = solution_node.get_trajectory(include_reflections=False)\n", + " if not best_trajectory:\n", + " return \"No solution found in the search process.\"\n", + "\n", + " result = (\n", + " best_trajectory[-1].get(\"content\") if isinstance(best_trajectory[-1], dict) else str(best_trajectory[-1])\n", + " )\n", + " logger.info(\"LATS search completed successfully\")\n", + " return result\n", + " except Exception as e:\n", + " logger.error(f\"An unexpected error occurred during LATS execution: {str(e)}\", exc_info=True)\n", + " return f\"An unexpected error occurred: {str(e)}\"" + ] + }, + { + "cell_type": "markdown", + "id": "e274e373", + "metadata": {}, + "source": [ + "Example usage:\n", + "\n", + "result = run_lats(\"Write a research report on deep learning.\")\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "aa719ff2", + "metadata": {}, + "source": [ + "\n", + "# Example usage of the LATS algorithm with Autogen" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "683c0f2c", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\")\n", + "logger = logging.getLogger(__name__)\n", + "\n", + "\n", + "def run_lats_example(question):\n", + " try:\n", + " logger.info(f\"Processing question: {question}\")\n", + " result = run_lats(question)\n", + " logger.info(f\"LATS algorithm completed. Result: {result[:100]}...\") # Log first 100 chars of result\n", + " print(f\"Question: {question}\")\n", + " print(f\"Answer: {result}\")\n", + " except Exception as e:\n", + " logger.error(f\"An error occurred while processing the question: {str(e)}\", exc_info=True)\n", + " print(f\"An error occurred: {str(e)}\")\n", + " finally:\n", + " print(\"---\")" + ] + }, + { + "cell_type": "markdown", + "id": "a4ce778e", + "metadata": {}, + "source": [ + "# List of example questions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60fa1f07", + "metadata": {}, + "outputs": [], + "source": [ + "questions = [\n", + " \"Explain how epigenetic modifications can influence gene expression across generations and the implications for evolution.\",\n", + " \"Discuss the challenges of grounding ethical theories in moral realism, especially in light of the is-ought problem introduced by Hume.\",\n", + " \"How does the Riemann Hypothesis relate to the distribution of prime numbers, and why is it significant in number theory?\",\n", + " \"Describe the challenges and theoretical underpinnings of unifying general relativity with quantum mechanics, particularly focusing on string theory and loop quantum gravity.\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "a0fed5fe", + "metadata": {}, + "source": [ + "# Run LATS algorithm for each question\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d1e5754", + "metadata": {}, + "outputs": [], + "source": [ + "for i, question in enumerate(questions, 1):\n", + " print(f\"\\nExample {i}:\")\n", + " run_lats_example(question)\n", + "\n", + "logger.info(\"All examples processed.\")" + ] + }, + { + "cell_type": "markdown", + "id": "af7254a5", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "Congrats on implementing LATS! This is a technique that can be reasonably fast and effective at solving complex agent tasks. A few notes that you probably observed above:\n", + "\n", + "1. While LATS is effective, the tree rollout process can require additional inference compute time. If you plan to integrate this into a production application, consider streaming intermediate steps to allow users to see the thought process and access intermediate results. Alternatively, you could use it to generate fine-tuning data to enhance single-shot accuracy and avoid lengthy rollouts. The cost of using LATS has significantly decreased since its initial proposal and is expected to continue decreasing.\n", + "\n", + "2. The effectiveness of the candidate selection process depends on the quality of the rewards generated. In this example, we exclusively use self-reflection as feedback, but if you have access to external feedback sources (such as code test execution), those should be incorporated as suggested above." + ] + }, + { + "cell_type": "markdown", + "id": "be01ff1e", + "metadata": {}, + "source": [ + "# \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 7981ef4b43d5..107c438a7f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ exclude = [ "math_utils\\.py$", "**/cap/py/autogencap/proto/*", ] -ignore-init-module-imports = true unfixable = ["F401"] [tool.ruff.lint.mccabe] diff --git a/samples/apps/autogen-studio/.gitignore b/samples/apps/autogen-studio/.gitignore index e1e3c9942ec1..549ce16b6db9 100644 --- a/samples/apps/autogen-studio/.gitignore +++ b/samples/apps/autogen-studio/.gitignore @@ -9,6 +9,9 @@ autogenstudio/web/workdir/* autogenstudio/web/ui/* autogenstudio/web/skills/user/* .release.sh +.nightly.sh + +notebooks/work_dir/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/samples/apps/autogen-studio/autogenstudio/chatmanager.py b/samples/apps/autogen-studio/autogenstudio/chatmanager.py index 84b85673f07c..e8ed3abfd627 100644 --- a/samples/apps/autogen-studio/autogenstudio/chatmanager.py +++ b/samples/apps/autogen-studio/autogenstudio/chatmanager.py @@ -1,20 +1,12 @@ -import asyncio -import json import os -import time from datetime import datetime from queue import Queue from typing import Any, Dict, List, Optional, Tuple, Union -import websockets -from fastapi import WebSocket, WebSocketDisconnect +from loguru import logger -from .datamodel import Message, SocketMessage, Workflow -from .utils import ( - extract_successful_code_blocks, - get_modified_files, - summarize_chat_history, -) +from .datamodel import Message +from .websocket_connection_manager import WebSocketConnectionManager from .workflowmanager import WorkflowManager @@ -24,15 +16,19 @@ class AutoGenChatManager: using an automated workflow configuration and message queue. """ - def __init__(self, message_queue: Queue) -> None: + def __init__( + self, message_queue: Queue, websocket_manager: WebSocketConnectionManager = None, human_input_timeout: int = 180 + ) -> None: """ Initializes the AutoGenChatManager with a message queue. :param message_queue: A queue to use for sending messages asynchronously. """ self.message_queue = message_queue + self.websocket_manager = websocket_manager + self.a_human_input_timeout = human_input_timeout - def send(self, message: str) -> None: + def send(self, message: dict) -> None: """ Sends a message by putting it into the message queue. @@ -41,6 +37,45 @@ def send(self, message: str) -> None: if self.message_queue is not None: self.message_queue.put_nowait(message) + async def a_send(self, message: dict) -> None: + """ + Asynchronously sends a message via the WebSocketManager class + + :param message: The message string to be sent. + """ + for connection, socket_client_id in self.websocket_manager.active_connections: + if message["connection_id"] == socket_client_id: + logger.info( + f"Sending message to connection_id: {message['connection_id']}. Connection ID: {socket_client_id}" + ) + await self.websocket_manager.send_message(message, connection) + else: + logger.info( + f"Skipping message for connection_id: {message['connection_id']}. Connection ID: {socket_client_id}" + ) + + async def a_prompt_for_input(self, prompt: dict, timeout: int = 60) -> str: + """ + Sends the user a prompt and waits for a response asynchronously via the WebSocketManager class + + :param message: The message string to be sent. + """ + + for connection, socket_client_id in self.websocket_manager.active_connections: + if prompt["connection_id"] == socket_client_id: + logger.info( + f"Sending message to connection_id: {prompt['connection_id']}. Connection ID: {socket_client_id}" + ) + try: + result = await self.websocket_manager.get_input(prompt, connection, timeout) + return result + except Exception as e: + return f"Error: {e}\nTERMINATE" + else: + logger.info( + f"Skipping message for connection_id: {prompt['connection_id']}. Connection ID: {socket_client_id}" + ) + def chat( self, message: Message, @@ -79,170 +114,66 @@ def chat( history=history, work_dir=work_dir, send_message_function=self.send, + a_send_message_function=self.a_send, connection_id=connection_id, ) - workflow = Workflow.model_validate(workflow) - message_text = message.content.strip() + result_message: Message = workflow_manager.run(message=f"{message_text}", clear_history=False, history=history) - start_time = time.time() - workflow_manager.run(message=f"{message_text}", clear_history=False) - end_time = time.time() - - metadata = { - "messages": workflow_manager.agent_history, - "summary_method": workflow.summary_method, - "time": end_time - start_time, - "files": get_modified_files(start_time, end_time, source_dir=work_dir), - } - - output = self._generate_output(message_text, workflow_manager, workflow) - - output_message = Message( - user_id=message.user_id, - role="assistant", - content=output, - meta=json.dumps(metadata), - session_id=message.session_id, - ) - - return output_message - - def _generate_output( - self, - message_text: str, - workflow_manager: WorkflowManager, - workflow: Workflow, - ) -> str: - """ - Generates the output response based on the workflow configuration and agent history. - - :param message_text: The text of the incoming message. - :param flow: An instance of `WorkflowManager`. - :param flow_config: An instance of `AgentWorkFlowConfig`. - :return: The output response as a string. - """ + result_message.user_id = message.user_id + result_message.session_id = message.session_id + return result_message - output = "" - if workflow.summary_method == "last": - successful_code_blocks = extract_successful_code_blocks(workflow_manager.agent_history) - last_message = ( - workflow_manager.agent_history[-1]["message"]["content"] if workflow_manager.agent_history else "" - ) - successful_code_blocks = "\n\n".join(successful_code_blocks) - output = (last_message + "\n" + successful_code_blocks) if successful_code_blocks else last_message - elif workflow.summary_method == "llm": - client = workflow_manager.receiver.client - status_message = SocketMessage( - type="agent_status", - data={ - "status": "summarizing", - "message": "Summarizing agent dialogue", - }, - connection_id=workflow_manager.connection_id, - ) - self.send(status_message.dict()) - output = summarize_chat_history( - task=message_text, - messages=workflow_manager.agent_history, - client=client, - ) - - elif workflow.summary_method == "none": - output = "" - return output - - -class WebSocketConnectionManager: - """ - Manages WebSocket connections including sending, broadcasting, and managing the lifecycle of connections. - """ - - def __init__( + async def a_chat( self, - active_connections: List[Tuple[WebSocket, str]] = None, - active_connections_lock: asyncio.Lock = None, - ) -> None: - """ - Initializes WebSocketConnectionManager with an optional list of active WebSocket connections. - - :param active_connections: A list of tuples, each containing a WebSocket object and its corresponding client_id. - """ - if active_connections is None: - active_connections = [] - self.active_connections_lock = active_connections_lock - self.active_connections: List[Tuple[WebSocket, str]] = active_connections - - async def connect(self, websocket: WebSocket, client_id: str) -> None: + message: Message, + history: List[Dict[str, Any]], + workflow: Any = None, + connection_id: Optional[str] = None, + user_dir: Optional[str] = None, + **kwargs, + ) -> Message: """ - Accepts a new WebSocket connection and appends it to the active connections list. + Processes an incoming message according to the agent's workflow configuration + and generates a response. - :param websocket: The WebSocket instance representing a client connection. - :param client_id: A string representing the unique identifier of the client. + :param message: An instance of `Message` representing an incoming message. + :param history: A list of dictionaries, each representing a past interaction. + :param flow_config: An instance of `AgentWorkFlowConfig`. If None, defaults to a standard configuration. + :param connection_id: An optional connection identifier. + :param kwargs: Additional keyword arguments. + :return: An instance of `Message` representing a response. """ - await websocket.accept() - async with self.active_connections_lock: - self.active_connections.append((websocket, client_id)) - print(f"New Connection: {client_id}, Total: {len(self.active_connections)}") - async def disconnect(self, websocket: WebSocket) -> None: - """ - Disconnects and removes a WebSocket connection from the active connections list. + # create a working director for workflow based on user_dir/session_id/time_hash + work_dir = os.path.join( + user_dir, + str(message.session_id), + datetime.now().strftime("%Y%m%d_%H-%M-%S"), + ) + os.makedirs(work_dir, exist_ok=True) - :param websocket: The WebSocket instance to remove. - """ - async with self.active_connections_lock: - try: - self.active_connections = [conn for conn in self.active_connections if conn[0] != websocket] - print(f"Connection Closed. Total: {len(self.active_connections)}") - except ValueError: - print("Error: WebSocket connection not found") - - async def disconnect_all(self) -> None: - """ - Disconnects all active WebSocket connections. - """ - for connection, _ in self.active_connections[:]: - await self.disconnect(connection) + # if no flow config is provided, use the default + if workflow is None: + raise ValueError("Workflow must be specified") - async def send_message(self, message: Union[Dict, str], websocket: WebSocket) -> None: - """ - Sends a JSON message to a single WebSocket connection. + workflow_manager = WorkflowManager( + workflow=workflow, + history=history, + work_dir=work_dir, + send_message_function=self.send, + a_send_message_function=self.a_send, + a_human_input_function=self.a_prompt_for_input, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=connection_id, + ) - :param message: A JSON serializable dictionary containing the message to send. - :param websocket: The WebSocket instance through which to send the message. - """ - try: - async with self.active_connections_lock: - await websocket.send_json(message) - except WebSocketDisconnect: - print("Error: Tried to send a message to a closed WebSocket") - await self.disconnect(websocket) - except websockets.exceptions.ConnectionClosedOK: - print("Error: WebSocket connection closed normally") - await self.disconnect(websocket) - except Exception as e: - print(f"Error in sending message: {str(e)}", message) - await self.disconnect(websocket) - - async def broadcast(self, message: Dict) -> None: - """ - Broadcasts a JSON message to all active WebSocket connections. + message_text = message.content.strip() + result_message: Message = await workflow_manager.a_run( + message=f"{message_text}", clear_history=False, history=history + ) - :param message: A JSON serializable dictionary containing the message to broadcast. - """ - # Create a message dictionary with the desired format - message_dict = {"message": message} - - for connection, _ in self.active_connections[:]: - try: - if connection.client_state == websockets.protocol.State.OPEN: - # Call send_message method with the message dictionary and current WebSocket connection - await self.send_message(message_dict, connection) - else: - print("Error: WebSocket connection is closed") - await self.disconnect(connection) - except (WebSocketDisconnect, websockets.exceptions.ConnectionClosedOK) as e: - print(f"Error: WebSocket disconnected or closed({str(e)})") - await self.disconnect(connection) + result_message.user_id = message.user_id + result_message.session_id = message.session_id + return result_message diff --git a/samples/apps/autogen-studio/autogenstudio/cli.py b/samples/apps/autogen-studio/autogenstudio/cli.py index 42642bcd68af..81fee7991455 100644 --- a/samples/apps/autogen-studio/autogenstudio/cli.py +++ b/samples/apps/autogen-studio/autogenstudio/cli.py @@ -16,7 +16,7 @@ def ui( port: int = 8081, workers: int = 1, reload: Annotated[bool, typer.Option("--reload")] = False, - docs: bool = False, + docs: bool = True, appdir: str = None, database_uri: Optional[str] = None, ): @@ -48,6 +48,39 @@ def ui( ) +@app.command() +def serve( + workflow: str = "", + host: str = "127.0.0.1", + port: int = 8084, + workers: int = 1, + docs: bool = False, +): + """ + Serve an API Endpoint based on an AutoGen Studio workflow json file. + + Args: + workflow (str): Path to the workflow json file. + host (str, optional): Host to run the UI on. Defaults to 127.0.0.1 (localhost). + port (int, optional): Port to run the UI on. Defaults to 8081. + workers (int, optional): Number of workers to run the UI with. Defaults to 1. + reload (bool, optional): Whether to reload the UI on code changes. Defaults to False. + docs (bool, optional): Whether to generate API docs. Defaults to False. + + """ + + os.environ["AUTOGENSTUDIO_API_DOCS"] = str(docs) + os.environ["AUTOGENSTUDIO_WORKFLOW_FILE"] = workflow + + uvicorn.run( + "autogenstudio.web.serve:app", + host=host, + port=port, + workers=workers, + reload=False, + ) + + @app.command() def version(): """ diff --git a/samples/apps/autogen-studio/autogenstudio/database/dbmanager.py b/samples/apps/autogen-studio/autogenstudio/database/dbmanager.py index 00d3714b63fa..6a02a0a7038c 100644 --- a/samples/apps/autogen-studio/autogenstudio/database/dbmanager.py +++ b/samples/apps/autogen-studio/autogenstudio/database/dbmanager.py @@ -1,3 +1,4 @@ +import threading from datetime import datetime from typing import Optional @@ -15,15 +16,23 @@ Skill, Workflow, WorkflowAgentLink, + WorkflowAgentType, ) from .utils import init_db_samples valid_link_types = ["agent_model", "agent_skill", "agent_agent", "workflow_agent"] +class WorkflowAgentMap(SQLModel): + agent: Agent + link: WorkflowAgentLink + + class DBManager: """A class to manage database operations""" + _init_lock = threading.Lock() # Class-level lock + def __init__(self, engine_uri: str): connection_args = {"check_same_thread": True} if "sqlite" in engine_uri else {} self.engine = create_engine(engine_uri, connect_args=connection_args) @@ -31,14 +40,15 @@ def __init__(self, engine_uri: str): def create_db_and_tables(self): """Create a new database and tables""" - try: - SQLModel.metadata.create_all(self.engine) + with self._init_lock: # Use the lock try: - init_db_samples(self) + SQLModel.metadata.create_all(self.engine) + try: + init_db_samples(self) + except Exception as e: + logger.info("Error while initializing database samples: " + str(e)) except Exception as e: - logger.info("Error while initializing database samples: " + str(e)) - except Exception as e: - logger.info("Error while creating database tables:" + str(e)) + logger.info("Error while creating database tables:" + str(e)) def upsert(self, model: SQLModel): """Create a new entity""" @@ -62,7 +72,7 @@ def upsert(self, model: SQLModel): session.refresh(model) except Exception as e: session.rollback() - logger.error("Error while upserting %s", e) + logger.error("Error while updating " + str(model_class.__name__) + ": " + str(e)) status = False response = Response( @@ -115,7 +125,7 @@ def get_items( session.rollback() status = False status_message = f"Error while fetching {model_class.__name__}" - logger.error("Error while getting %s: %s", model_class.__name__, e) + logger.error("Error while getting items: " + str(model_class.__name__) + " " + str(e)) response: Response = Response( message=status_message, @@ -157,16 +167,16 @@ def delete(self, model_class: SQLModel, filters: dict = None): status_message = f"{model_class.__name__} Deleted Successfully" else: print(f"Row with filters {filters} not found") - logger.info("Row with filters %s not found", filters) + logger.info("Row with filters + filters + not found") status_message = "Row not found" except exc.IntegrityError as e: session.rollback() - logger.error("Integrity ... Error while deleting: %s", e) + logger.error("Integrity ... Error while deleting: " + str(e)) status_message = f"The {model_class.__name__} is linked to another entity and cannot be deleted." status = False except Exception as e: session.rollback() - logger.error("Error while deleting: %s", e) + logger.error("Error while deleting: " + str(e)) status_message = f"Error while deleting: {e}" status = False response = Response( @@ -182,6 +192,7 @@ def get_linked_entities( primary_id: int, return_json: bool = False, agent_type: Optional[str] = None, + sequence_id: Optional[int] = None, ): """ Get all entities linked to the primary entity. @@ -217,19 +228,21 @@ def get_linked_entities( linked_entities = agent.agents elif link_type == "workflow_agent": linked_entities = session.exec( - select(Agent) - .join(WorkflowAgentLink) + select(WorkflowAgentLink, Agent) + .join(Agent, WorkflowAgentLink.agent_id == Agent.id) .where( WorkflowAgentLink.workflow_id == primary_id, - WorkflowAgentLink.agent_type == agent_type, ) ).all() + + linked_entities = [WorkflowAgentMap(agent=agent, link=link) for link, agent in linked_entities] + linked_entities = sorted(linked_entities, key=lambda x: x.link.sequence_id) # type: ignore except Exception as e: - logger.error("Error while getting linked entities: %s", e) + logger.error("Error while getting linked entities: " + str(e)) status_message = f"Error while getting linked entities: {e}" status = False if return_json: - linked_entities = [self._model_to_dict(row) for row in linked_entities] + linked_entities = [row.model_dump() for row in linked_entities] response = Response( message=status_message, @@ -245,6 +258,7 @@ def link( primary_id: int, secondary_id: int, agent_type: Optional[str] = None, + sequence_id: Optional[int] = None, ) -> Response: """ Link two entities together. @@ -357,6 +371,7 @@ def link( WorkflowAgentLink.workflow_id == primary_id, WorkflowAgentLink.agent_id == secondary_id, WorkflowAgentLink.agent_type == agent_type, + WorkflowAgentLink.sequence_id == sequence_id, ) ).first() if existing_link: @@ -373,6 +388,7 @@ def link( workflow_id=primary_id, agent_id=secondary_id, agent_type=agent_type, + sequence_id=sequence_id, ) session.add(workflow_agent_link) # add and commit the link @@ -385,7 +401,7 @@ def link( except Exception as e: session.rollback() - logger.error("Error while linking: %s", e) + logger.error("Error while linking: " + str(e)) status = False status_message = f"Error while linking due to an exception: {e}" @@ -402,6 +418,7 @@ def unlink( primary_id: int, secondary_id: int, agent_type: Optional[str] = None, + sequence_id: Optional[int] = 0, ) -> Response: """ Unlink two entities. @@ -417,6 +434,7 @@ def unlink( """ status = True status_message = "" + print("primary", primary_id, "secondary", secondary_id, "sequence", sequence_id, "agent_type", agent_type) if link_type not in valid_link_types: status = False @@ -452,6 +470,7 @@ def unlink( WorkflowAgentLink.workflow_id == primary_id, WorkflowAgentLink.agent_id == secondary_id, WorkflowAgentLink.agent_type == agent_type, + WorkflowAgentLink.sequence_id == sequence_id, ) ).first() @@ -465,7 +484,7 @@ def unlink( except Exception as e: session.rollback() - logger.error("Error while unlinking: %s", e) + logger.error("Error while unlinking: " + str(e)) status = False status_message = f"Error while unlinking due to an exception: {e}" diff --git a/samples/apps/autogen-studio/autogenstudio/database/utils.py b/samples/apps/autogen-studio/autogenstudio/database/utils.py index c14003b414c3..ac77a9161498 100644 --- a/samples/apps/autogen-studio/autogenstudio/database/utils.py +++ b/samples/apps/autogen-studio/autogenstudio/database/utils.py @@ -23,6 +23,7 @@ Skill, Workflow, WorkflowAgentLink, + WorkFlowType, ) @@ -71,9 +72,15 @@ def get_agent(agent_id): agent_dict["agents"] = [get_agent(agent.id) for agent in agent.agents] return agent_dict + agents = [] for link in workflow_agent_links: agent_dict = get_agent(link.agent_id) - workflow[str(link.agent_type.value)] = agent_dict + agents.append({"agent": agent_dict, "link": link.model_dump(mode="json")}) + # workflow[str(link.agent_type.value)] = agent_dict + if workflow["type"] == WorkFlowType.sequential.value: + # sort agents by sequence_id in link + agents = sorted(agents, key=lambda x: x["link"]["sequence_id"]) + workflow["agents"] = agents return workflow @@ -141,9 +148,13 @@ def init_db_samples(dbmanager: Any): logger.info("Database already initialized with Default and Travel Planning Workflows") return logger.info("Initializing database with Default and Travel Planning Workflows") + # models - gpt_4_model = Model( - model="gpt-4-1106-preview", description="OpenAI GPT-4 model", user_id="guestuser@gmail.com", api_type="open_ai" + google_gemini_model = Model( + model="gemini-1.5-pro-latest", + description="Google's Gemini model", + user_id="guestuser@gmail.com", + api_type="google", ) azure_model = Model( model="gpt4-turbo", @@ -160,61 +171,49 @@ def init_db_samples(dbmanager: Any): api_type="open_ai", ) - google_gemini_model = Model( - model="gemini-1.5-pro-latest", - description="Google's Gemini model", + gpt_4_model = Model( + model="gpt-4-1106-preview", description="OpenAI GPT-4 model", user_id="guestuser@gmail.com", api_type="open_ai" + ) + + anthropic_sonnet_model = Model( + model="claude-3-5-sonnet-20240620", + description="Anthropic's Claude 3.5 Sonnet model", + api_type="anthropic", user_id="guestuser@gmail.com", - api_type="google", ) # skills - + generate_pdf_skill = Skill( + name="generate_and_save_pdf", + description="Generate and save a pdf file based on the provided input sections.", + user_id="guestuser@gmail.com", + libraries=["requests", "fpdf", "PIL"], + content='import uuid\nimport requests\nfrom fpdf import FPDF\nfrom typing import List, Dict, Optional\nfrom pathlib import Path\nfrom PIL import Image, ImageDraw, ImageOps\nfrom io import BytesIO\n\ndef generate_and_save_pdf(\n sections: List[Dict[str, Optional[str]]], \n output_file: str = "report.pdf", \n report_title: str = "PDF Report"\n) -> None:\n """\n Function to generate a beautiful PDF report in A4 paper format. \n\n :param sections: A list of sections where each section is represented by a dictionary containing:\n - title: The title of the section.\n - level: The heading level (e.g., "title", "h1", "h2").\n - content: The content or body text of the section.\n - image: (Optional) The URL or local path to the image.\n :param output_file: The name of the output PDF file. (default is "report.pdf")\n :param report_title: The title of the report. (default is "PDF Report")\n :return: None\n """\n\n def get_image(image_url_or_path):\n if image_url_or_path.startswith("http://") or image_url_or_path.startswith("https://"):\n response = requests.get(image_url_or_path)\n if response.status_code == 200:\n return BytesIO(response.content)\n elif Path(image_url_or_path).is_file():\n return open(image_url_or_path, \'rb\')\n return None\n\n def add_rounded_corners(img, radius=6):\n mask = Image.new(\'L\', img.size, 0)\n draw = ImageDraw.Draw(mask)\n draw.rounded_rectangle([(0, 0), img.size], radius, fill=255)\n img = ImageOps.fit(img, mask.size, centering=(0.5, 0.5))\n img.putalpha(mask)\n return img\n\n class PDF(FPDF):\n def header(self):\n self.set_font("Arial", "B", 12)\n self.cell(0, 10, report_title, 0, 1, "C")\n \n def chapter_title(self, txt): \n self.set_font("Arial", "B", 12)\n self.cell(0, 10, txt, 0, 1, "L")\n self.ln(2)\n \n def chapter_body(self, body):\n self.set_font("Arial", "", 12)\n self.multi_cell(0, 10, body)\n self.ln()\n\n def add_image(self, img_data):\n img = Image.open(img_data)\n img = add_rounded_corners(img)\n img_path = Path(f"temp_{uuid.uuid4().hex}.png")\n img.save(img_path, format="PNG")\n self.image(str(img_path), x=None, y=None, w=190 if img.width > 190 else img.width)\n self.ln(10)\n img_path.unlink()\n\n pdf = PDF()\n pdf.add_page()\n font_size = {"title": 16, "h1": 14, "h2": 12, "body": 12}\n\n for section in sections:\n title, level, content, image = section.get("title", ""), section.get("level", "h1"), section.get("content", ""), section.get("image")\n pdf.set_font("Arial", "B" if level in font_size else "", font_size.get(level, font_size["body"]))\n pdf.chapter_title(title)\n\n if content: pdf.chapter_body(content)\n if image:\n img_data = get_image(image)\n if img_data:\n pdf.add_image(img_data)\n if isinstance(img_data, BytesIO):\n img_data.close()\n\n pdf.output(output_file)\n print(f"PDF report saved as {output_file}")\n\n# # Example usage\n# sections = [\n# {\n# "title": "Introduction - Early Life",\n# "level": "h1",\n# "image": "https://picsum.photos/536/354",\n# "content": ("Marie Curie was born on 7 November 1867 in Warsaw, Poland. "\n# "She was the youngest of five children. Both of her parents were teachers. "\n# "Her father was a math and physics instructor, and her mother was the head of a private school. "\n# "Marie\'s curiosity and brilliance were evident from an early age."),\n# },\n# {\n# "title": "Academic Accomplishments",\n# "level": "h2",\n# "content": ("Despite many obstacles, Marie Curie earned degrees in physics and mathematics from the University of Paris. "\n# "She conducted groundbreaking research on radioactivity, becoming the first woman to win a Nobel Prize. "\n# "Her achievements paved the way for future generations of scientists, particularly women in STEM fields."),\n# },\n# {\n# "title": "Major Discoveries",\n# "level": "h2",\n# "image": "https://picsum.photos/536/354",\n# "content": ("One of Marie Curie\'s most notable discoveries was that of radium and polonium, two radioactive elements. "\n# "Her meticulous work not only advanced scientific understanding but also had practical applications in medicine and industry."),\n# },\n# {\n# "title": "Conclusion - Legacy",\n# "level": "h1",\n# "content": ("Marie Curie\'s legacy lives on through her contributions to science, her role as a trailblazer for women in STEM, "\n# "and the ongoing impact of her discoveries on modern medicine and technology. "\n# "Her life and work remain an inspiration to many, demonstrating the power of perseverance and intellectual curiosity."),\n# },\n# ]\n\n# generate_and_save_pdf_report(sections, "my_report.pdf", "The Life of Marie Curie")', + ) generate_image_skill = Skill( - name="generate_images", + name="generate_and_save_images", + secrets=[{"secret": "OPENAI_API_KEY", "value": None}], + libraries=["openai"], description="Generate and save images based on a user's query.", content='\nfrom typing import List\nimport uuid\nimport requests # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = "1024x1024") -> List[str]:\n """\n Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI\'s DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.\n\n :param query: A natural language description of the image to be generated.\n :param image_size: The size of the image to be generated. (default is "1024x1024")\n :return: A list of filenames for the saved images.\n """\n\n client = OpenAI() # Initialize the OpenAI client\n response = client.images.generate(model="dall-e-3", prompt=query, n=1, size=image_size) # Generate images\n\n # List to store the file names of saved images\n saved_files = []\n\n # Check if the response is successful\n if response.data:\n for image_data in response.data:\n # Generate a random UUID as the file name\n file_name = str(uuid.uuid4()) + ".png" # Assuming the image is a PNG\n file_path = Path(file_name)\n\n img_url = image_data.url\n img_response = requests.get(img_url)\n if img_response.status_code == 200:\n # Write the binary content to a file\n with open(file_path, "wb") as img_file:\n img_file.write(img_response.content)\n print(f"Image saved to {file_path}")\n saved_files.append(str(file_path))\n else:\n print(f"Failed to download the image from {img_url}")\n else:\n print("No image data found in the response!")\n\n # Return the list of saved files\n return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images("A cute baby sea otter")\n', user_id="guestuser@gmail.com", ) # agents - user_proxy_config = AgentConfig( - name="user_proxy", - description="User Proxy Agent Configuration", - human_input_mode="NEVER", - max_consecutive_auto_reply=25, - system_message="You are a helpful assistant", - code_execution_config=CodeExecutionConfigTypes.local, - default_auto_reply="TERMINATE", - llm_config=False, - ) - user_proxy = Agent( - user_id="guestuser@gmail.com", type=AgentType.userproxy, config=user_proxy_config.model_dump(mode="json") - ) - - painter_assistant_config = AgentConfig( - name="default_assistant", - description="Assistant Agent", - human_input_mode="NEVER", - max_consecutive_auto_reply=25, - system_message=AssistantAgent.DEFAULT_SYSTEM_MESSAGE, - code_execution_config=CodeExecutionConfigTypes.none, - llm_config={}, - ) - painter_assistant = Agent( - user_id="guestuser@gmail.com", type=AgentType.assistant, config=painter_assistant_config.model_dump(mode="json") - ) planner_assistant_config = AgentConfig( name="planner_assistant", description="Assistant Agent", human_input_mode="NEVER", max_consecutive_auto_reply=25, - system_message="You are a helpful assistant that can suggest a travel plan for a user. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.", + system_message="You are a helpful assistant that can suggest a travel plan for a user and utilize any context information provided. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.", code_execution_config=CodeExecutionConfigTypes.none, llm_config={}, ) planner_assistant = Agent( - user_id="guestuser@gmail.com", type=AgentType.assistant, config=planner_assistant_config.model_dump(mode="json") + user_id="guestuser@gmail.com", + type=AgentType.assistant, + config=planner_assistant_config.model_dump(mode="json"), ) local_assistant_config = AgentConfig( @@ -222,7 +221,7 @@ def init_db_samples(dbmanager: Any): description="Local Assistant Agent", human_input_mode="NEVER", max_consecutive_auto_reply=25, - system_message="You are a local assistant that can suggest local activities or places to visit for a user. You can suggest local activities, places to visit, restaurants to eat at, etc. You can also provide information about the weather, local events, etc. You can provide information about the local area, but you cannot suggest a complete travel plan. You can only provide information about the local area.", + system_message="You are a local assistant that can suggest local activities or places to visit for a user and can utilize any context information provided. You can suggest local activities, places to visit, restaurants to eat at, etc. You can also provide information about the weather, local events, etc. You can provide information about the local area, but you cannot suggest a complete travel plan. You can only provide information about the local area.", code_execution_config=CodeExecutionConfigTypes.none, llm_config={}, ) @@ -245,7 +244,7 @@ def init_db_samples(dbmanager: Any): config=language_assistant_config.model_dump(mode="json"), ) - # group chat + # group chat agent travel_groupchat_config = AgentConfig( name="travel_groupchat", admin_name="groupchat", @@ -262,11 +261,48 @@ def init_db_samples(dbmanager: Any): user_id="guestuser@gmail.com", type=AgentType.groupchat, config=travel_groupchat_config.model_dump(mode="json") ) - # workflows - default_workflow = Workflow(name="Default Workflow", description="Default workflow", user_id="guestuser@gmail.com") + user_proxy_config = AgentConfig( + name="user_proxy", + description="User Proxy Agent Configuration", + human_input_mode="NEVER", + max_consecutive_auto_reply=25, + system_message="You are a helpful assistant", + code_execution_config=CodeExecutionConfigTypes.local, + default_auto_reply="TERMINATE", + llm_config=False, + ) + user_proxy = Agent( + user_id="guestuser@gmail.com", type=AgentType.userproxy, config=user_proxy_config.model_dump(mode="json") + ) + + default_assistant_config = AgentConfig( + name="default_assistant", + description="Assistant Agent", + human_input_mode="NEVER", + max_consecutive_auto_reply=25, + system_message=AssistantAgent.DEFAULT_SYSTEM_MESSAGE, + code_execution_config=CodeExecutionConfigTypes.none, + llm_config={}, + ) + default_assistant = Agent( + user_id="guestuser@gmail.com", type=AgentType.assistant, config=default_assistant_config.model_dump(mode="json") + ) + # workflows travel_workflow = Workflow( - name="Travel Planning Workflow", description="Travel workflow", user_id="guestuser@gmail.com" + name="Travel Planning Workflow", + description="Travel workflow", + user_id="guestuser@gmail.com", + sample_tasks=["Plan a 3 day trip to Hawaii Islands.", "Plan an eventful and exciting trip to Uzbeksitan."], + ) + default_workflow = Workflow( + name="Default Workflow", + description="Default workflow", + user_id="guestuser@gmail.com", + sample_tasks=[ + "paint a picture of a glass of ethiopian coffee, freshly brewed in a tall glass cup, on a table right in front of a lush green forest scenery", + "Plot the stock price of NVIDIA YTD.", + ], ) with Session(dbmanager.engine) as session: @@ -274,27 +310,29 @@ def init_db_samples(dbmanager: Any): session.add(google_gemini_model) session.add(azure_model) session.add(gpt_4_model) + session.add(anthropic_sonnet_model) session.add(generate_image_skill) + session.add(generate_pdf_skill) session.add(user_proxy) - session.add(painter_assistant) + session.add(default_assistant) session.add(travel_groupchat_agent) session.add(planner_assistant) session.add(local_assistant) session.add(language_assistant) - session.add(default_workflow) session.add(travel_workflow) + session.add(default_workflow) session.commit() - dbmanager.link(link_type="agent_model", primary_id=painter_assistant.id, secondary_id=gpt_4_model.id) - dbmanager.link(link_type="agent_skill", primary_id=painter_assistant.id, secondary_id=generate_image_skill.id) + dbmanager.link(link_type="agent_model", primary_id=default_assistant.id, secondary_id=gpt_4_model.id) + dbmanager.link(link_type="agent_skill", primary_id=default_assistant.id, secondary_id=generate_image_skill.id) dbmanager.link( link_type="workflow_agent", primary_id=default_workflow.id, secondary_id=user_proxy.id, agent_type="sender" ) dbmanager.link( link_type="workflow_agent", primary_id=default_workflow.id, - secondary_id=painter_assistant.id, + secondary_id=default_assistant.id, agent_type="receiver", ) diff --git a/samples/apps/autogen-studio/autogenstudio/datamodel.py b/samples/apps/autogen-studio/autogenstudio/datamodel.py index 3dbd46c357ee..92d60cf5c525 100644 --- a/samples/apps/autogen-studio/autogenstudio/datamodel.py +++ b/samples/apps/autogen-studio/autogenstudio/datamodel.py @@ -16,10 +16,32 @@ Enum as SqlEnum, ) -SQLModel.model_config["protected_namespaces"] = () +# added for python3.11 and sqlmodel 0.0.22 incompatibility +if hasattr(SQLModel, "model_config"): + SQLModel.model_config["protected_namespaces"] = () +elif hasattr(SQLModel, "Config"): + + class CustomSQLModel(SQLModel): + class Config: + protected_namespaces = () + + SQLModel = CustomSQLModel +else: + print("Warning: Unable to set protected_namespaces.") + # pylint: disable=protected-access +class MessageMeta(SQLModel, table=False): + task: Optional[str] = None + messages: Optional[List[Dict[str, Any]]] = None + summary_method: Optional[str] = "last" + files: Optional[List[dict]] = None + time: Optional[datetime] = None + log: Optional[List[dict]] = None + usage: Optional[List[dict]] = None + + class Message(SQLModel, table=True): __table_args__ = {"sqlite_autoincrement": True} id: Optional[int] = Field(default=None, primary_key=True) @@ -38,7 +60,7 @@ class Message(SQLModel, table=True): default=None, sa_column=Column(Integer, ForeignKey("session.id", ondelete="CASCADE")) ) connection_id: Optional[str] = None - meta: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) + meta: Optional[Union[MessageMeta, dict]] = Field(default={}, sa_column=Column(JSON)) class Session(SQLModel, table=True): @@ -82,11 +104,12 @@ class Skill(SQLModel, table=True): sa_column=Column(DateTime(timezone=True), onupdate=func.now()), ) # pylint: disable=not-callable user_id: Optional[str] = None + version: Optional[str] = "0.0.1" name: str content: str description: Optional[str] = None - secrets: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) - libraries: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) + secrets: Optional[List[dict]] = Field(default_factory=list, sa_column=Column(JSON)) + libraries: Optional[List[str]] = Field(default_factory=list, sa_column=Column(JSON)) agents: List["Agent"] = Relationship(back_populates="skills", link_model=AgentSkillLink) @@ -97,7 +120,7 @@ class LLMConfig(SQLModel, table=False): temperature: float = 0 cache_seed: Optional[Union[int, None]] = None timeout: Optional[int] = None - max_tokens: Optional[int] = 1000 + max_tokens: Optional[int] = 2048 extra_body: Optional[dict] = None @@ -105,6 +128,10 @@ class ModelTypes(str, Enum): openai = "open_ai" google = "google" azure = "azure" + anthropic = "anthropic" + mistral = "mistral" + together = "together" + groq = "groq" class Model(SQLModel, table=True): @@ -119,6 +146,7 @@ class Model(SQLModel, table=True): sa_column=Column(DateTime(timezone=True), onupdate=func.now()), ) # pylint: disable=not-callable user_id: Optional[str] = None + version: Optional[str] = "0.0.1" model: str api_key: Optional[str] = None base_url: Optional[str] = None @@ -164,6 +192,7 @@ class WorkflowAgentType(str, Enum): sender = "sender" receiver = "receiver" planner = "planner" + sequential = "sequential" class WorkflowAgentLink(SQLModel, table=True): @@ -174,6 +203,7 @@ class WorkflowAgentLink(SQLModel, table=True): default=WorkflowAgentType.sender, sa_column=Column(SqlEnum(WorkflowAgentType), primary_key=True), ) + sequence_id: Optional[int] = Field(default=0, primary_key=True) class AgentLink(SQLModel, table=True): @@ -194,8 +224,9 @@ class Agent(SQLModel, table=True): sa_column=Column(DateTime(timezone=True), onupdate=func.now()), ) # pylint: disable=not-callable user_id: Optional[str] = None + version: Optional[str] = "0.0.1" type: AgentType = Field(default=AgentType.assistant, sa_column=Column(SqlEnum(AgentType))) - config: AgentConfig = Field(default_factory=AgentConfig, sa_column=Column(JSON)) + config: Union[AgentConfig, dict] = Field(default_factory=AgentConfig, sa_column=Column(JSON)) skills: List[Skill] = Relationship(back_populates="agents", link_model=AgentSkillLink) models: List[Model] = Relationship(back_populates="agents", link_model=AgentModelLink) workflows: List["Workflow"] = Relationship(link_model=WorkflowAgentLink, back_populates="agents") @@ -215,11 +246,12 @@ class Agent(SQLModel, table=True): secondaryjoin="Agent.id==AgentLink.agent_id", ), ) + task_instruction: Optional[str] = None class WorkFlowType(str, Enum): - twoagents = "twoagents" - groupchat = "groupchat" + autonomous = "autonomous" + sequential = "sequential" class WorkFlowSummaryMethod(str, Enum): @@ -240,14 +272,16 @@ class Workflow(SQLModel, table=True): sa_column=Column(DateTime(timezone=True), onupdate=func.now()), ) # pylint: disable=not-callable user_id: Optional[str] = None + version: Optional[str] = "0.0.1" name: str description: str agents: List[Agent] = Relationship(back_populates="workflows", link_model=WorkflowAgentLink) - type: WorkFlowType = Field(default=WorkFlowType.twoagents, sa_column=Column(SqlEnum(WorkFlowType))) + type: WorkFlowType = Field(default=WorkFlowType.autonomous, sa_column=Column(SqlEnum(WorkFlowType))) summary_method: Optional[WorkFlowSummaryMethod] = Field( default=WorkFlowSummaryMethod.last, sa_column=Column(SqlEnum(WorkFlowSummaryMethod)), ) + sample_tasks: Optional[List[str]] = Field(default_factory=list, sa_column=Column(JSON)) class Response(SQLModel): diff --git a/samples/apps/autogen-studio/autogenstudio/profiler.py b/samples/apps/autogen-studio/autogenstudio/profiler.py new file mode 100644 index 000000000000..679a56917e20 --- /dev/null +++ b/samples/apps/autogen-studio/autogenstudio/profiler.py @@ -0,0 +1,108 @@ +# metrics - agent_frequency, execution_count, tool_count, + +from typing import Dict, List, Optional + +from .datamodel import Message, MessageMeta + + +class Profiler: + """ + Profiler class to profile agent task runs and compute metrics + for performance evaluation. + """ + + def __init__(self): + self.metrics: List[Dict] = [] + + def _is_code(self, message: Message) -> bool: + """ + Check if the message contains code. + + :param message: The message instance to check. + :return: True if the message contains code, False otherwise. + """ + content = message.get("message").get("content").lower() + return "```" in content + + def _is_tool(self, message: Message) -> bool: + """ + Check if the message uses a tool. + + :param message: The message instance to check. + :return: True if the message uses a tool, False otherwise. + """ + content = message.get("message").get("content").lower() + return "from skills import" in content + + def _is_code_execution(self, message: Message) -> bool: + """ + Check if the message indicates code execution. + + :param message: The message instance to check. + :return: dict with is_code and status keys. + """ + content = message.get("message").get("content").lower() + if "exitcode:" in content: + status = "exitcode: 0" in content + return {"is_code": True, "status": status} + else: + return {"is_code": False, "status": False} + + def _is_terminate(self, message: Message) -> bool: + """ + Check if the message indicates termination. + + :param message: The message instance to check. + :return: True if the message indicates termination, False otherwise. + """ + content = message.get("message").get("content").lower() + return "terminate" in content + + def profile(self, agent_message: Message): + """ + Profile the agent task run and compute metrics. + + :param agent: The agent instance that ran the task. + :param task: The task instance that was run. + """ + meta = MessageMeta(**agent_message.meta) + print(meta.log) + usage = meta.usage + messages = meta.messages + profile = [] + bar = [] + stats = {} + total_code_executed = 0 + success_code_executed = 0 + agents = [] + for message in messages: + agent = message.get("sender") + is_code = self._is_code(message) + is_tool = self._is_tool(message) + is_code_execution = self._is_code_execution(message) + total_code_executed += is_code_execution["is_code"] + success_code_executed += 1 if is_code_execution["status"] else 0 + + row = { + "agent": agent, + "tool_call": is_code, + "code_execution": is_code_execution, + "terminate": self._is_terminate(message), + } + bar_row = { + "agent": agent, + "tool_call": "tool call" if is_tool else "no tool call", + "code_execution": ( + "success" + if is_code_execution["status"] + else "failure" if is_code_execution["is_code"] else "no code" + ), + "message": 1, + } + profile.append(row) + bar.append(bar_row) + agents.append(agent) + code_success_rate = (success_code_executed / total_code_executed if total_code_executed > 0 else 0) * 100 + stats["code_success_rate"] = code_success_rate + stats["total_code_executed"] = total_code_executed + return {"profile": profile, "bar": bar, "stats": stats, "agents": set(agents), "usage": usage} diff --git a/samples/apps/autogen-studio/autogenstudio/utils/utils.py b/samples/apps/autogen-studio/autogenstudio/utils/utils.py index ed533ec3883c..40cd549cb06b 100644 --- a/samples/apps/autogen-studio/autogenstudio/utils/utils.py +++ b/samples/apps/autogen-studio/autogenstudio/utils/utils.py @@ -289,7 +289,7 @@ def init_app_folders(app_file_path: str) -> Dict[str, str]: return folders -def get_skills_from_prompt(skills: List[Skill], work_dir: str) -> str: +def get_skills_prompt(skills: List[Skill], work_dir: str) -> str: """ Create a prompt with the content of all skills and write the skills to a file named skills.py in the work_dir. @@ -306,10 +306,18 @@ def get_skills_from_prompt(skills: List[Skill], work_dir: str) -> str: """ prompt = "" # filename: skills.py + for skill in skills: + if not isinstance(skill, Skill): + skill = Skill(**skill) + if skill.secrets: + for secret in skill.secrets: + if secret.get("value") is not None: + os.environ[secret["secret"]] = secret["value"] prompt += f""" ##### Begin of {skill.name} ##### +from skills import {skill.name} # Import the function from skills.py {skill.content} @@ -317,15 +325,40 @@ def get_skills_from_prompt(skills: List[Skill], work_dir: str) -> str: """ + return instruction + prompt + + +def save_skills_to_file(skills: List[Skill], work_dir: str) -> None: + """ + Write the skills to a file named skills.py in the work_dir. + + :param skills: A dictionary skills + """ + + # TBD: Double check for duplicate skills? + # check if work_dir exists if not os.path.exists(work_dir): os.makedirs(work_dir) + skills_content = "" + for skill in skills: + if not isinstance(skill, Skill): + skill = Skill(**skill) + + skills_content += f""" + +##### Begin of {skill.name} ##### + +{skill.content} + +#### End of {skill.name} #### + + """ + # overwrite skills.py in work_dir with open(os.path.join(work_dir, "skills.py"), "w", encoding="utf-8") as f: - f.write(prompt) - - return instruction + prompt + f.write(skills_content) def delete_files_in_folder(folders: Union[str, List[str]]) -> None: @@ -405,9 +438,23 @@ def test_model(model: Model): Test the model endpoint by sending a simple message to the model and returning the response. """ + print("Testing model", model) + sanitized_model = sanitize_model(model) client = OpenAIWrapper(config_list=[sanitized_model]) - response = client.create(messages=[{"role": "user", "content": "2+2="}], cache_seed=None) + response = client.create( + messages=[ + { + "role": "system", + "content": "You are a helpful assistant that can add numbers. ONLY RETURN THE RESULT.", + }, + { + "role": "user", + "content": "2+2=", + }, + ], + cache_seed=None, + ) return response.choices[0].message.content @@ -426,7 +473,11 @@ def load_code_execution_config(code_execution_type: CodeExecutionConfigTypes, wo if code_execution_type == CodeExecutionConfigTypes.local: executor = LocalCommandLineCodeExecutor(work_dir=work_dir) elif code_execution_type == CodeExecutionConfigTypes.docker: - executor = DockerCommandLineCodeExecutor(work_dir=work_dir) + try: + executor = DockerCommandLineCodeExecutor(work_dir=work_dir) + except Exception as e: + logger.error(f"Error initializing Docker executor: {e}") + return False elif code_execution_type == CodeExecutionConfigTypes.none: return False else: @@ -462,3 +513,61 @@ def summarize_chat_history(task: str, messages: List[Dict[str, str]], client: Mo ] response = client.create(messages=summarization_prompt, cache_seed=None) return response.choices[0].message.content + + +def get_autogen_log(db_path="logs.db"): + """ + Fetches data the autogen logs database. + Args: + dbname (str): Name of the database file. Defaults to "logs.db". + table (str): Name of the table to query. Defaults to "chat_completions". + + Returns: + list: A list of dictionaries, where each dictionary represents a row from the table. + """ + import json + import sqlite3 + + con = sqlite3.connect(db_path) + query = """ + SELECT + chat_completions.*, + agents.name AS agent_name + FROM + chat_completions + JOIN + agents ON chat_completions.wrapper_id = agents.wrapper_id + """ + cursor = con.execute(query) + rows = cursor.fetchall() + column_names = [description[0] for description in cursor.description] + data = [dict(zip(column_names, row)) for row in rows] + for row in data: + response = json.loads(row["response"]) + print(response) + total_tokens = response.get("usage", {}).get("total_tokens", 0) + row["total_tokens"] = total_tokens + con.close() + return data + + +def find_key_value(d, target_key): + """ + Recursively search for a key in a nested dictionary and return its value. + """ + if d is None: + return None + + if isinstance(d, dict): + if target_key in d: + return d[target_key] + for k in d: + item = find_key_value(d[k], target_key) + if item is not None: + return item + elif isinstance(d, list): + for i in d: + item = find_key_value(i, target_key) + if item is not None: + return item + return None diff --git a/samples/apps/autogen-studio/autogenstudio/version.py b/samples/apps/autogen-studio/autogenstudio/version.py index bafe37f75b14..bf51c1b62b6e 100644 --- a/samples/apps/autogen-studio/autogenstudio/version.py +++ b/samples/apps/autogen-studio/autogenstudio/version.py @@ -1,3 +1,3 @@ -VERSION = "0.0.56rc9" +VERSION = "0.1.6" __version__ = VERSION APP_NAME = "autogenstudio" diff --git a/samples/apps/autogen-studio/autogenstudio/web/app.py b/samples/apps/autogen-studio/autogenstudio/web/app.py index 76ab8139ebc3..bbd087f52ea2 100644 --- a/samples/apps/autogen-studio/autogenstudio/web/app.py +++ b/samples/apps/autogen-studio/autogenstudio/web/app.py @@ -4,7 +4,7 @@ import threading import traceback from contextlib import asynccontextmanager -from typing import Any +from typing import Any, Union from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware @@ -12,13 +12,16 @@ from loguru import logger from openai import OpenAIError -from ..chatmanager import AutoGenChatManager, WebSocketConnectionManager +from ..chatmanager import AutoGenChatManager from ..database import workflow_from_id from ..database.dbmanager import DBManager from ..datamodel import Agent, Message, Model, Response, Session, Skill, Workflow +from ..profiler import Profiler from ..utils import check_and_cast_datetime_fields, init_app_folders, md5_hash, test_model from ..version import VERSION +from ..websocket_connection_manager import WebSocketConnectionManager +profiler = Profiler() managers = {"chat": None} # manage calls to autogen # Create thread-safe queue for messages between api thread and autogen threads message_queue = queue.Queue() @@ -62,11 +65,17 @@ def message_handler(): database_engine_uri = folders["database_engine_uri"] dbmanager = DBManager(engine_uri=database_engine_uri) +HUMAN_INPUT_TIMEOUT_SECONDS = 180 + @asynccontextmanager async def lifespan(app: FastAPI): print("***** App started *****") - managers["chat"] = AutoGenChatManager(message_queue=message_queue) + managers["chat"] = AutoGenChatManager( + message_queue=message_queue, + websocket_manager=websocket_manager, + human_input_timeout=HUMAN_INPUT_TIMEOUT_SECONDS, + ) dbmanager.create_db_and_tables() yield @@ -92,8 +101,15 @@ async def lifespan(app: FastAPI): allow_headers=["*"], ) - -api = FastAPI(root_path="/api") +show_docs = os.environ.get("AUTOGENSTUDIO_API_DOCS", "False").lower() == "true" +docs_url = "/docs" if show_docs else None +api = FastAPI( + root_path="/api", + title="AutoGen Studio API", + version=VERSION, + docs_url=docs_url, + description="AutoGen Studio is a low-code tool for building and testing multi-agent workflows using AutoGen.", +) # mount an api route such that the main route serves the ui and the /api app.mount("/api", api) @@ -293,6 +309,19 @@ async def get_workflow(workflow_id: int, user_id: str): return list_entity(Workflow, filters=filters) +@api.get("/workflows/export/{workflow_id}") +async def export_workflow(workflow_id: int, user_id: str): + """Export a user workflow""" + response = Response(message="Workflow exported successfully", status=True, data=None) + try: + workflow_details = workflow_from_id(workflow_id, dbmanager=dbmanager) + response.data = workflow_details + except Exception as ex_error: + response.message = "Error occurred while exporting workflow: " + str(ex_error) + response.status = False + return response.model_dump(mode="json") + + @api.post("/workflows") async def create_workflow(workflow: Workflow): """Create a new workflow""" @@ -317,6 +346,19 @@ async def link_workflow_agent(workflow_id: int, agent_id: int, agent_type: str): ) +@api.post("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}/{sequence_id}") +async def link_workflow_agent_sequence(workflow_id: int, agent_id: int, agent_type: str, sequence_id: int): + """Link an agent to a workflow""" + print("Sequence ID: ", sequence_id) + return dbmanager.link( + link_type="workflow_agent", + primary_id=workflow_id, + secondary_id=agent_id, + agent_type=agent_type, + sequence_id=sequence_id, + ) + + @api.delete("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}") async def unlink_workflow_agent(workflow_id: int, agent_id: int, agent_type: str): """Unlink an agent from a workflow""" @@ -328,17 +370,47 @@ async def unlink_workflow_agent(workflow_id: int, agent_id: int, agent_type: str ) -@api.get("/workflows/link/agent/{workflow_id}/{agent_type}") -async def get_linked_workflow_agents(workflow_id: int, agent_type: str): +@api.delete("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}/{sequence_id}") +async def unlink_workflow_agent_sequence(workflow_id: int, agent_id: int, agent_type: str, sequence_id: int): + """Unlink an agent from a workflow sequence""" + return dbmanager.unlink( + link_type="workflow_agent", + primary_id=workflow_id, + secondary_id=agent_id, + agent_type=agent_type, + sequence_id=sequence_id, + ) + + +@api.get("/workflows/link/agent/{workflow_id}") +async def get_linked_workflow_agents(workflow_id: int): """Get all agents linked to a workflow""" return dbmanager.get_linked_entities( link_type="workflow_agent", primary_id=workflow_id, - agent_type=agent_type, return_json=True, ) +@api.get("/profiler/{message_id}") +async def profile_agent_task_run(message_id: int): + """Profile an agent task run""" + try: + agent_message = dbmanager.get(Message, filters={"id": message_id}).data[0] + + profile = profiler.profile(agent_message) + return { + "status": True, + "message": "Agent task run profiled successfully", + "data": profile, + } + except Exception as ex_error: + return { + "status": False, + "message": "Error occurred while profiling agent task run: " + str(ex_error), + } + + @api.get("/sessions") async def list_sessions(user_id: str): """List all sessions for a user""" @@ -384,7 +456,7 @@ async def run_session_workflow(message: Message, session_id: int, workflow_id: i user_dir = os.path.join(folders["files_static_root"], "user", md5_hash(message.user_id)) os.makedirs(user_dir, exist_ok=True) workflow = workflow_from_id(workflow_id, dbmanager=dbmanager) - agent_response: Message = managers["chat"].chat( + agent_response: Message = await managers["chat"].a_chat( message=message, history=user_message_history, user_dir=user_dir, @@ -395,7 +467,6 @@ async def run_session_workflow(message: Message, session_id: int, workflow_id: i response: Response = dbmanager.upsert(agent_response) return response.model_dump(mode="json") except Exception as ex_error: - print(traceback.format_exc()) return { "status": False, "message": "Error occurred while processing message: " + str(ex_error), diff --git a/samples/apps/autogen-studio/autogenstudio/web/serve.py b/samples/apps/autogen-studio/autogenstudio/web/serve.py new file mode 100644 index 000000000000..462615378b8a --- /dev/null +++ b/samples/apps/autogen-studio/autogenstudio/web/serve.py @@ -0,0 +1,30 @@ +# loads a fast api api endpoint with a single endpoint that takes text query and return a response + +import json +import os + +from fastapi import FastAPI + +from ..datamodel import Response +from ..workflowmanager import WorkflowManager + +app = FastAPI() +workflow_file_path = os.environ.get("AUTOGENSTUDIO_WORKFLOW_FILE", None) + + +if workflow_file_path: + workflow_manager = WorkflowManager(workflow=workflow_file_path) +else: + raise ValueError("Workflow file must be specified") + + +@app.get("/predict/{task}") +async def predict(task: str): + response = Response(message="Task successfully completed", status=True, data=None) + try: + result_message = workflow_manager.run(message=task, clear_history=False) + response.data = result_message + except Exception as e: + response.message = str(e) + response.status = False + return response diff --git a/samples/apps/autogen-studio/autogenstudio/websocket_connection_manager.py b/samples/apps/autogen-studio/autogenstudio/websocket_connection_manager.py new file mode 100644 index 000000000000..73f7ef896811 --- /dev/null +++ b/samples/apps/autogen-studio/autogenstudio/websocket_connection_manager.py @@ -0,0 +1,135 @@ +import asyncio +from typing import Any, Dict, List, Optional, Tuple, Union + +import websockets +from fastapi import WebSocket, WebSocketDisconnect + + +class WebSocketConnectionManager: + """ + Manages WebSocket connections including sending, broadcasting, and managing the lifecycle of connections. + """ + + def __init__( + self, + active_connections: List[Tuple[WebSocket, str]] = None, + active_connections_lock: asyncio.Lock = None, + ) -> None: + """ + Initializes WebSocketConnectionManager with an optional list of active WebSocket connections. + + :param active_connections: A list of tuples, each containing a WebSocket object and its corresponding client_id. + """ + if active_connections is None: + active_connections = [] + self.active_connections_lock = active_connections_lock + self.active_connections: List[Tuple[WebSocket, str]] = active_connections + + async def connect(self, websocket: WebSocket, client_id: str) -> None: + """ + Accepts a new WebSocket connection and appends it to the active connections list. + + :param websocket: The WebSocket instance representing a client connection. + :param client_id: A string representing the unique identifier of the client. + """ + await websocket.accept() + async with self.active_connections_lock: + self.active_connections.append((websocket, client_id)) + print(f"New Connection: {client_id}, Total: {len(self.active_connections)}") + + async def disconnect(self, websocket: WebSocket) -> None: + """ + Disconnects and removes a WebSocket connection from the active connections list. + + :param websocket: The WebSocket instance to remove. + """ + async with self.active_connections_lock: + try: + self.active_connections = [conn for conn in self.active_connections if conn[0] != websocket] + print(f"Connection Closed. Total: {len(self.active_connections)}") + except ValueError: + print("Error: WebSocket connection not found") + + async def disconnect_all(self) -> None: + """ + Disconnects all active WebSocket connections. + """ + for connection, _ in self.active_connections[:]: + await self.disconnect(connection) + + async def send_message(self, message: Union[Dict, str], websocket: WebSocket) -> None: + """ + Sends a JSON message to a single WebSocket connection. + + :param message: A JSON serializable dictionary containing the message to send. + :param websocket: The WebSocket instance through which to send the message. + """ + try: + async with self.active_connections_lock: + await websocket.send_json(message) + except WebSocketDisconnect: + print("Error: Tried to send a message to a closed WebSocket") + await self.disconnect(websocket) + except websockets.exceptions.ConnectionClosedOK: + print("Error: WebSocket connection closed normally") + await self.disconnect(websocket) + except Exception as e: + print(f"Error in sending message: {str(e)}", message) + await self.disconnect(websocket) + + async def get_input(self, prompt: Union[Dict, str], websocket: WebSocket, timeout: int = 60) -> str: + """ + Sends a JSON message to a single WebSocket connection as a prompt for user input. + Waits on a user response or until the given timeout elapses. + + :param prompt: A JSON serializable dictionary containing the message to send. + :param websocket: The WebSocket instance through which to send the message. + """ + response = "Error: Unexpected response.\nTERMINATE" + try: + async with self.active_connections_lock: + await websocket.send_json(prompt) + result = await asyncio.wait_for(websocket.receive_json(), timeout=timeout) + data = result.get("data") + if data: + response = data.get("content", "Error: Unexpected response format\nTERMINATE") + else: + response = "Error: Unexpected response format\nTERMINATE" + + except asyncio.TimeoutError: + response = f"The user was timed out after {timeout} seconds of inactivity.\nTERMINATE" + except WebSocketDisconnect: + print("Error: Tried to send a message to a closed WebSocket") + await self.disconnect(websocket) + response = "The user was disconnected\nTERMINATE" + except websockets.exceptions.ConnectionClosedOK: + print("Error: WebSocket connection closed normally") + await self.disconnect(websocket) + response = "The user was disconnected\nTERMINATE" + except Exception as e: + print(f"Error in sending message: {str(e)}", prompt) + await self.disconnect(websocket) + response = f"Error: {e}\nTERMINATE" + + return response + + async def broadcast(self, message: Dict) -> None: + """ + Broadcasts a JSON message to all active WebSocket connections. + + :param message: A JSON serializable dictionary containing the message to broadcast. + """ + # Create a message dictionary with the desired format + message_dict = {"message": message} + + for connection, _ in self.active_connections[:]: + try: + if connection.client_state == websockets.protocol.State.OPEN: + # Call send_message method with the message dictionary and current WebSocket connection + await self.send_message(message_dict, connection) + else: + print("Error: WebSocket connection is closed") + await self.disconnect(connection) + except (WebSocketDisconnect, websockets.exceptions.ConnectionClosedOK) as e: + print(f"Error: WebSocket disconnected or closed({str(e)})") + await self.disconnect(connection) diff --git a/samples/apps/autogen-studio/autogenstudio/workflowmanager.py b/samples/apps/autogen-studio/autogenstudio/workflowmanager.py index 8b41caab4285..2da3b58b7cec 100644 --- a/samples/apps/autogen-studio/autogenstudio/workflowmanager.py +++ b/samples/apps/autogen-studio/autogenstudio/workflowmanager.py @@ -1,54 +1,155 @@ +import json import os +import time from datetime import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Coroutine, Dict, List, Optional, Union import autogen from .datamodel import ( Agent, AgentType, + CodeExecutionConfigTypes, Message, SocketMessage, + Workflow, + WorkFlowSummaryMethod, + WorkFlowType, +) +from .utils import ( + clear_folder, + find_key_value, + get_modified_files, + get_skills_prompt, + load_code_execution_config, + sanitize_model, + save_skills_to_file, + summarize_chat_history, ) -from .utils import clear_folder, get_skills_from_prompt, load_code_execution_config, sanitize_model -class WorkflowManager: +class AutoWorkflowManager: """ - AutoGenWorkFlowManager class to load agents from a provided configuration and run a chat between them + WorkflowManager class to load agents from a provided configuration and run a chat between them. """ def __init__( self, - workflow: Dict, + workflow: Union[Dict, str], history: Optional[List[Message]] = None, work_dir: str = None, clear_work_dir: bool = True, send_message_function: Optional[callable] = None, + a_send_message_function: Optional[Coroutine] = None, + a_human_input_function: Optional[callable] = None, + a_human_input_timeout: Optional[int] = 60, connection_id: Optional[str] = None, ) -> None: """ - Initializes the AutoGenFlow with agents specified in the config and optional - message history. + Initializes the WorkflowManager with agents specified in the config and optional message history. Args: - config: The configuration settings for the sender and receiver agents. - history: An optional list of previous messages to populate the agents' history. - + workflow (Union[Dict, str]): The workflow configuration. This can be a dictionary or a string which is a path to a JSON file. + history (Optional[List[Message]]): The message history. + work_dir (str): The working directory. + clear_work_dir (bool): If set to True, clears the working directory. + send_message_function (Optional[callable]): The function to send messages. + a_send_message_function (Optional[Coroutine]): Async coroutine to send messages. + a_human_input_function (Optional[callable]): Async coroutine to prompt the user for input. + a_human_input_timeout (Optional[int]): A time (in seconds) to wait for user input. After this time, the a_human_input_function will timeout and end the conversation. + connection_id (Optional[str]): The connection identifier. """ + if isinstance(workflow, str): + if os.path.isfile(workflow): + with open(workflow, "r") as file: + self.workflow = json.load(file) + else: + raise FileNotFoundError(f"The file {workflow} does not exist.") + elif isinstance(workflow, dict): + self.workflow = workflow + else: + raise ValueError("The 'workflow' parameter should be either a dictionary or a valid JSON file path") + # TODO - improved typing for workflow + self.workflow_skills = [] self.send_message_function = send_message_function + self.a_send_message_function = a_send_message_function + self.a_human_input_function = a_human_input_function + self.a_human_input_timeout = a_human_input_timeout self.connection_id = connection_id self.work_dir = work_dir or "work_dir" + self.code_executor_pool = { + CodeExecutionConfigTypes.local: load_code_execution_config( + CodeExecutionConfigTypes.local, work_dir=self.work_dir + ), + CodeExecutionConfigTypes.docker: load_code_execution_config( + CodeExecutionConfigTypes.docker, work_dir=self.work_dir + ), + } if clear_work_dir: clear_folder(self.work_dir) - self.workflow = workflow - self.sender = self.load(workflow.get("sender")) - self.receiver = self.load(workflow.get("receiver")) self.agent_history = [] + self.history = history or [] + self.sender = None + self.receiver = None + + def _run_workflow(self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False) -> None: + """ + Runs the workflow based on the provided configuration. + + Args: + message: The initial message to start the chat. + history: A list of messages to populate the agents' history. + clear_history: If set to True, clears the chat history before initiating. + + """ + for agent in self.workflow.get("agents", []): + if agent.get("link").get("agent_type") == "sender": + self.sender = self.load(agent.get("agent")) + elif agent.get("link").get("agent_type") == "receiver": + self.receiver = self.load(agent.get("agent")) + if self.sender and self.receiver: + # save all agent skills to skills.py + save_skills_to_file(self.workflow_skills, self.work_dir) + if history: + self._populate_history(history) + self.sender.initiate_chat( + self.receiver, + message=message, + clear_history=clear_history, + ) + else: + raise ValueError("Sender and receiver agents are not defined in the workflow configuration.") + + async def _a_run_workflow( + self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False + ) -> None: + """ + Asynchronously runs the workflow based on the provided configuration. + + Args: + message: The initial message to start the chat. + history: A list of messages to populate the agents' history. + clear_history: If set to True, clears the chat history before initiating. - if history: - self._populate_history(history) + """ + for agent in self.workflow.get("agents", []): + if agent.get("link").get("agent_type") == "sender": + self.sender = self.load(agent.get("agent")) + elif agent.get("link").get("agent_type") == "receiver": + self.receiver = self.load(agent.get("agent")) + if self.sender and self.receiver: + # save all agent skills to skills.py + save_skills_to_file(self.workflow_skills, self.work_dir) + if history: + self._populate_history(history) + await self.sender.a_initiate_chat( + self.receiver, + message=message, + clear_history=clear_history, + ) + else: + raise ValueError("Sender and receiver agents are not defined in the workflow configuration.") def _serialize_agent( self, @@ -120,7 +221,9 @@ def process_message( "connection_id": self.connection_id, "message_type": "agent_message", } - # if the agent will respond to the message, or the message is sent by a groupchat agent. This avoids adding groupchat broadcast messages to the history (which are sent with request_reply=False), or when agent populated from history + # if the agent will respond to the message, or the message is sent by a groupchat agent. + # This avoids adding groupchat broadcast messages to the history (which are sent with request_reply=False), + # or when agent populated from history if request_reply is not False or sender_type == "groupchat": self.agent_history.append(message_payload) # add to history if self.send_message_function: # send over the message queue @@ -131,6 +234,53 @@ def process_message( ) self.send_message_function(socket_msg.dict()) + async def a_process_message( + self, + sender: autogen.Agent, + receiver: autogen.Agent, + message: Dict, + request_reply: bool = False, + silent: bool = False, + sender_type: str = "agent", + ) -> None: + """ + Asynchronously processes the message and adds it to the agent history. + + Args: + + sender: The sender of the message. + receiver: The receiver of the message. + message: The message content. + request_reply: If set to True, the message will be added to agent history. + silent: determining verbosity. + sender_type: The type of the sender of the message. + """ + + message = message if isinstance(message, dict) else {"content": message, "role": "user"} + message_payload = { + "recipient": receiver.name, + "sender": sender.name, + "message": message, + "timestamp": datetime.now().isoformat(), + "sender_type": sender_type, + "connection_id": self.connection_id, + "message_type": "agent_message", + } + # if the agent will respond to the message, or the message is sent by a groupchat agent. + # This avoids adding groupchat broadcast messages to the history (which are sent with request_reply=False), + # or when agent populated from history + if request_reply is not False or sender_type == "groupchat": + self.agent_history.append(message_payload) # add to history + socket_msg = SocketMessage( + type="agent_message", + data=message_payload, + connection_id=self.connection_id, + ) + if self.a_send_message_function: # send over the message queue + await self.a_send_message_function(socket_msg.dict()) + elif self.send_message_function: # send over the message queue + self.send_message_function(socket_msg.dict()) + def _populate_history(self, history: List[Message]) -> None: """ Populates the agent message history from the provided list of messages. @@ -160,6 +310,12 @@ def sanitize_agent(self, agent: Dict) -> Agent: """ """ skills = agent.get("skills", []) + + # When human input mode is not NEVER and no model is attached, the ui is passing bogus llm_config. + configured_models = agent.get("models") + if not configured_models or len(configured_models) == 0: + agent["config"]["llm_config"] = False + agent = Agent.model_validate(agent) agent.config.is_termination_msg = agent.config.is_termination_msg or ( lambda x: "TERMINATE" in x.get("content", "").rstrip()[-20:] @@ -184,13 +340,13 @@ def get_default_system_message(agent_type: str) -> str: config_list.append(sanitized_llm) agent.config.llm_config.config_list = config_list - agent.config.code_execution_config = load_code_execution_config( - agent.config.code_execution_config, work_dir=self.work_dir - ) + agent.config.code_execution_config = self.code_executor_pool.get(agent.config.code_execution_config, False) if skills: + for skill in skills: + self.workflow_skills.append(skill) skills_prompt = "" - skills_prompt = get_skills_from_prompt(skills, self.work_dir) + skills_prompt = get_skills_prompt(skills, self.work_dir) if agent.config.system_message: agent.config.system_message = agent.config.system_message + "\n\n" + skills_prompt else: @@ -222,6 +378,10 @@ def load(self, agent: Any) -> autogen.Agent: agent = ExtendedGroupChatManager( groupchat=groupchat, message_processor=self.process_message, + a_message_processor=self.a_process_message, + a_human_input_function=self.a_human_input_function, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=self.connection_id, llm_config=agent.config.llm_config.model_dump(), ) return agent @@ -231,17 +391,392 @@ def load(self, agent: Any) -> autogen.Agent: agent = ExtendedConversableAgent( **self._serialize_agent(agent), message_processor=self.process_message, + a_message_processor=self.a_process_message, + a_human_input_function=self.a_human_input_function, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=self.connection_id, ) elif agent.type == "userproxy": agent = ExtendedConversableAgent( **self._serialize_agent(agent), message_processor=self.process_message, + a_message_processor=self.a_process_message, + a_human_input_function=self.a_human_input_function, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=self.connection_id, ) else: raise ValueError(f"Unknown agent type: {agent.type}") return agent - def run(self, message: str, clear_history: bool = False) -> None: + def _generate_output( + self, + message_text: str, + summary_method: str, + ) -> str: + """ + Generates the output response based on the workflow configuration and agent history. + + :param message_text: The text of the incoming message. + :param flow: An instance of `WorkflowManager`. + :param flow_config: An instance of `AgentWorkFlowConfig`. + :return: The output response as a string. + """ + + output = "" + if summary_method == WorkFlowSummaryMethod.last: + (self.agent_history) + last_message = self.agent_history[-1]["message"]["content"] if self.agent_history else "" + output = last_message + elif summary_method == WorkFlowSummaryMethod.llm: + client = self.receiver.client + if self.connection_id: + status_message = SocketMessage( + type="agent_status", + data={ + "status": "summarizing", + "message": "Summarizing agent dialogue", + }, + connection_id=self.connection_id, + ) + self.send_message_function(status_message.model_dump(mode="json")) + output = summarize_chat_history( + task=message_text, + messages=self.agent_history, + client=client, + ) + + elif summary_method == "none": + output = "" + return output + + def _get_agent_usage(self, agent: autogen.Agent): + final_usage = [] + default_usage = {"total_cost": 0, "total_tokens": 0} + agent_usage = agent.client.total_usage_summary if agent.client else default_usage + agent_usage = { + "agent": agent.name, + "total_cost": find_key_value(agent_usage, "total_cost") or 0, + "total_tokens": find_key_value(agent_usage, "total_tokens") or 0, + } + final_usage.append(agent_usage) + + if type(agent) == ExtendedGroupChatManager: + print("groupchat found, processing", len(agent.groupchat.agents)) + for agent in agent.groupchat.agents: + agent_usage = agent.client.total_usage_summary if agent.client else default_usage or default_usage + agent_usage = { + "agent": agent.name, + "total_cost": find_key_value(agent_usage, "total_cost") or 0, + "total_tokens": find_key_value(agent_usage, "total_tokens") or 0, + } + final_usage.append(agent_usage) + return final_usage + + def _get_usage_summary(self): + sender_usage = self._get_agent_usage(self.sender) + receiver_usage = self._get_agent_usage(self.receiver) + + all_usage = [] + all_usage.extend(sender_usage) + all_usage.extend(receiver_usage) + # all_usage = [sender_usage, receiver_usage] + return all_usage + + def run(self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False) -> Message: + """ + Initiates a chat between the sender and receiver agents with an initial message + and an option to clear the history. + + Args: + message: The initial message to start the chat. + clear_history: If set to True, clears the chat history before initiating. + """ + + start_time = time.time() + self._run_workflow(message=message, history=history, clear_history=clear_history) + end_time = time.time() + + output = self._generate_output(message, self.workflow.get("summary_method", "last")) + + usage = self._get_usage_summary() + # print("usage", usage) + + result_message = Message( + content=output, + role="assistant", + meta={ + "messages": self.agent_history, + "summary_method": self.workflow.get("summary_method", "last"), + "time": end_time - start_time, + "files": get_modified_files(start_time, end_time, source_dir=self.work_dir), + "usage": usage, + }, + ) + return result_message + + async def a_run( + self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False + ) -> Message: + """ + Asynchronously initiates a chat between the sender and receiver agents with an initial message + and an option to clear the history. + + Args: + message: The initial message to start the chat. + clear_history: If set to True, clears the chat history before initiating. + """ + + start_time = time.time() + await self._a_run_workflow(message=message, history=history, clear_history=clear_history) + end_time = time.time() + + output = self._generate_output(message, self.workflow.get("summary_method", "last")) + + usage = self._get_usage_summary() + # print("usage", usage) + + result_message = Message( + content=output, + role="assistant", + meta={ + "messages": self.agent_history, + "summary_method": self.workflow.get("summary_method", "last"), + "time": end_time - start_time, + "files": get_modified_files(start_time, end_time, source_dir=self.work_dir), + "usage": usage, + }, + ) + return result_message + + +class SequentialWorkflowManager: + """ + WorkflowManager class to load agents from a provided configuration and run a chat between them sequentially. + """ + + def __init__( + self, + workflow: Union[Dict, str], + history: Optional[List[Message]] = None, + work_dir: str = None, + clear_work_dir: bool = True, + send_message_function: Optional[callable] = None, + a_send_message_function: Optional[Coroutine] = None, + a_human_input_function: Optional[callable] = None, + a_human_input_timeout: Optional[int] = 60, + connection_id: Optional[str] = None, + ) -> None: + """ + Initializes the WorkflowManager with agents specified in the config and optional message history. + + Args: + workflow (Union[Dict, str]): The workflow configuration. This can be a dictionary or a string which is a path to a JSON file. + history (Optional[List[Message]]): The message history. + work_dir (str): The working directory. + clear_work_dir (bool): If set to True, clears the working directory. + send_message_function (Optional[callable]): The function to send messages. + a_send_message_function (Optional[Coroutine]): Async coroutine to send messages. + a_human_input_function (Optional[callable]): Async coroutine to prompt for human input. + a_human_input_timeout (Optional[int]): A time (in seconds) to wait for user input. After this time, the a_human_input_function will timeout and end the conversation. + connection_id (Optional[str]): The connection identifier. + """ + if isinstance(workflow, str): + if os.path.isfile(workflow): + with open(workflow, "r") as file: + self.workflow = json.load(file) + else: + raise FileNotFoundError(f"The file {workflow} does not exist.") + elif isinstance(workflow, dict): + self.workflow = workflow + else: + raise ValueError("The 'workflow' parameter should be either a dictionary or a valid JSON file path") + + # TODO - improved typing for workflow + self.send_message_function = send_message_function + self.a_send_message_function = a_send_message_function + self.a_human_input_function = a_human_input_function + self.a_human_input_timeout = a_human_input_timeout + self.connection_id = connection_id + self.work_dir = work_dir or "work_dir" + if clear_work_dir: + clear_folder(self.work_dir) + self.agent_history = [] + self.history = history or [] + self.sender = None + self.receiver = None + self.model_client = None + + def _run_workflow(self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False) -> None: + """ + Runs the workflow based on the provided configuration. + + Args: + message: The initial message to start the chat. + history: A list of messages to populate the agents' history. + clear_history: If set to True, clears the chat history before initiating. + + """ + user_proxy = { + "config": { + "name": "user_proxy", + "human_input_mode": "NEVER", + "max_consecutive_auto_reply": 25, + "code_execution_config": "local", + "default_auto_reply": "TERMINATE", + "description": "User Proxy Agent Configuration", + "llm_config": False, + "type": "userproxy", + } + } + sequential_history = [] + for i, agent in enumerate(self.workflow.get("agents", [])): + workflow = Workflow( + name="agent workflow", type=WorkFlowType.autonomous, summary_method=WorkFlowSummaryMethod.llm + ) + workflow = workflow.model_dump(mode="json") + agent = agent.get("agent") + workflow["agents"] = [ + {"agent": user_proxy, "link": {"agent_type": "sender"}}, + {"agent": agent, "link": {"agent_type": "receiver"}}, + ] + + auto_workflow = AutoWorkflowManager( + workflow=workflow, + history=history, + work_dir=self.work_dir, + clear_work_dir=True, + send_message_function=self.send_message_function, + a_send_message_function=self.a_send_message_function, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=self.connection_id, + ) + task_prompt = ( + f""" + Your primary instructions are as follows: + {agent.get("task_instruction")} + Context for addressing your task is below: + ======= + {str(sequential_history)} + ======= + Now address your task: + """ + if i > 0 + else message + ) + result = auto_workflow.run(message=task_prompt, clear_history=clear_history) + sequential_history.append(result.content) + self.model_client = auto_workflow.receiver.client + print(f"======== end of sequence === {i}============") + self.agent_history.extend(result.meta.get("messages", [])) + + async def _a_run_workflow( + self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False + ) -> None: + """ + Asynchronously runs the workflow based on the provided configuration. + + Args: + message: The initial message to start the chat. + history: A list of messages to populate the agents' history. + clear_history: If set to True, clears the chat history before initiating. + + """ + user_proxy = { + "config": { + "name": "user_proxy", + "human_input_mode": "NEVER", + "max_consecutive_auto_reply": 25, + "code_execution_config": "local", + "default_auto_reply": "TERMINATE", + "description": "User Proxy Agent Configuration", + "llm_config": False, + "type": "userproxy", + } + } + sequential_history = [] + for i, agent in enumerate(self.workflow.get("agents", [])): + workflow = Workflow( + name="agent workflow", type=WorkFlowType.autonomous, summary_method=WorkFlowSummaryMethod.llm + ) + workflow = workflow.model_dump(mode="json") + agent = agent.get("agent") + workflow["agents"] = [ + {"agent": user_proxy, "link": {"agent_type": "sender"}}, + {"agent": agent, "link": {"agent_type": "receiver"}}, + ] + + auto_workflow = AutoWorkflowManager( + workflow=workflow, + history=history, + work_dir=self.work_dir, + clear_work_dir=True, + send_message_function=self.send_message_function, + a_send_message_function=self.a_send_message_function, + a_human_input_function=self.a_human_input_function, + a_human_input_timeout=self.a_human_input_timeout, + connection_id=self.connection_id, + ) + task_prompt = ( + f""" + Your primary instructions are as follows: + {agent.get("task_instruction")} + Context for addressing your task is below: + ======= + {str(sequential_history)} + ======= + Now address your task: + """ + if i > 0 + else message + ) + result = await auto_workflow.a_run(message=task_prompt, clear_history=clear_history) + sequential_history.append(result.content) + self.model_client = auto_workflow.receiver.client + print(f"======== end of sequence === {i}============") + self.agent_history.extend(result.meta.get("messages", [])) + + def _generate_output( + self, + message_text: str, + summary_method: str, + ) -> str: + """ + Generates the output response based on the workflow configuration and agent history. + + :param message_text: The text of the incoming message. + :param flow: An instance of `WorkflowManager`. + :param flow_config: An instance of `AgentWorkFlowConfig`. + :return: The output response as a string. + """ + + output = "" + if summary_method == WorkFlowSummaryMethod.last: + (self.agent_history) + last_message = self.agent_history[-1]["message"]["content"] if self.agent_history else "" + output = last_message + elif summary_method == WorkFlowSummaryMethod.llm: + if self.connection_id: + status_message = SocketMessage( + type="agent_status", + data={ + "status": "summarizing", + "message": "Summarizing agent dialogue", + }, + connection_id=self.connection_id, + ) + self.send_message_function(status_message.model_dump(mode="json")) + output = summarize_chat_history( + task=message_text, + messages=self.agent_history, + client=self.model_client, + ) + + elif summary_method == "none": + output = "" + return output + + def run(self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False) -> Message: """ Initiates a chat between the sender and receiver agents with an initial message and an option to clear the history. @@ -250,17 +785,143 @@ def run(self, message: str, clear_history: bool = False) -> None: message: The initial message to start the chat. clear_history: If set to True, clears the chat history before initiating. """ - self.sender.initiate_chat( - self.receiver, - message=message, - clear_history=clear_history, + + start_time = time.time() + self._run_workflow(message=message, history=history, clear_history=clear_history) + end_time = time.time() + output = self._generate_output(message, self.workflow.get("summary_method", "last")) + + result_message = Message( + content=output, + role="assistant", + meta={ + "messages": self.agent_history, + "summary_method": self.workflow.get("summary_method", "last"), + "time": end_time - start_time, + "files": get_modified_files(start_time, end_time, source_dir=self.work_dir), + "task": message, + }, + ) + return result_message + + async def a_run( + self, message: str, history: Optional[List[Message]] = None, clear_history: bool = False + ) -> Message: + """ + Asynchronously initiates a chat between the sender and receiver agents with an initial message + and an option to clear the history. + + Args: + message: The initial message to start the chat. + clear_history: If set to True, clears the chat history before initiating. + """ + + start_time = time.time() + await self._a_run_workflow(message=message, history=history, clear_history=clear_history) + end_time = time.time() + output = self._generate_output(message, self.workflow.get("summary_method", "last")) + + result_message = Message( + content=output, + role="assistant", + meta={ + "messages": self.agent_history, + "summary_method": self.workflow.get("summary_method", "last"), + "time": end_time - start_time, + "files": get_modified_files(start_time, end_time, source_dir=self.work_dir), + "task": message, + }, ) + return result_message + + +class WorkflowManager: + """ + WorkflowManager class to load agents from a provided configuration and run a chat between them. + """ + + def __new__( + self, + workflow: Union[Dict, str], + history: Optional[List[Message]] = None, + work_dir: str = None, + clear_work_dir: bool = True, + send_message_function: Optional[callable] = None, + a_send_message_function: Optional[Coroutine] = None, + a_human_input_function: Optional[callable] = None, + a_human_input_timeout: Optional[int] = 60, + connection_id: Optional[str] = None, + ) -> None: + """ + Initializes the WorkflowManager with agents specified in the config and optional message history. + + Args: + workflow (Union[Dict, str]): The workflow configuration. This can be a dictionary or a string which is a path to a JSON file. + history (Optional[List[Message]]): The message history. + work_dir (str): The working directory. + clear_work_dir (bool): If set to True, clears the working directory. + send_message_function (Optional[callable]): The function to send messages. + a_send_message_function (Optional[Coroutine]): Async coroutine to send messages. + a_human_input_function (Optional[callable]): Async coroutine to prompt for user input. + a_human_input_timeout (Optional[int]): A time (in seconds) to wait for user input. After this time, the a_human_input_function will timeout and end the conversation. + connection_id (Optional[str]): The connection identifier. + """ + if isinstance(workflow, str): + if os.path.isfile(workflow): + with open(workflow, "r") as file: + self.workflow = json.load(file) + else: + raise FileNotFoundError(f"The file {workflow} does not exist.") + elif isinstance(workflow, dict): + self.workflow = workflow + else: + raise ValueError("The 'workflow' parameter should be either a dictionary or a valid JSON file path") + + if self.workflow.get("type") == WorkFlowType.autonomous.value: + return AutoWorkflowManager( + workflow=workflow, + history=history, + work_dir=work_dir, + clear_work_dir=clear_work_dir, + send_message_function=send_message_function, + a_send_message_function=a_send_message_function, + a_human_input_function=a_human_input_function, + a_human_input_timeout=a_human_input_timeout, + connection_id=connection_id, + ) + elif self.workflow.get("type") == WorkFlowType.sequential.value: + return SequentialWorkflowManager( + workflow=workflow, + history=history, + work_dir=work_dir, + clear_work_dir=clear_work_dir, + send_message_function=send_message_function, + a_send_message_function=a_send_message_function, + a_human_input_function=a_human_input_function, + a_human_input_timeout=a_human_input_timeout, + connection_id=connection_id, + ) class ExtendedConversableAgent(autogen.ConversableAgent): - def __init__(self, message_processor=None, *args, **kwargs): + def __init__( + self, + message_processor=None, + a_message_processor=None, + a_human_input_function=None, + a_human_input_timeout: Optional[int] = 60, + connection_id=None, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) self.message_processor = message_processor + self.a_message_processor = a_message_processor + self.a_human_input_function = a_human_input_function + self.a_human_input_response = None + self.a_human_input_timeout = a_human_input_timeout + self.connection_id = connection_id def receive( self, @@ -273,14 +934,79 @@ def receive( self.message_processor(sender, self, message, request_reply, silent, sender_type="agent") super().receive(message, sender, request_reply, silent) + async def a_receive( + self, + message: Union[Dict, str], + sender: autogen.Agent, + request_reply: Optional[bool] = None, + silent: Optional[bool] = False, + ) -> None: + if self.a_message_processor: + await self.a_message_processor(sender, self, message, request_reply, silent, sender_type="agent") + elif self.message_processor: + self.message_processor(sender, self, message, request_reply, silent, sender_type="agent") + await super().a_receive(message, sender, request_reply, silent) + + # Strangely, when the response from a_get_human_input == "" (empty string) the libs call into the + # sync version. I guess that's "just in case", but it's odd because replying with an empty string + # is the intended way for the user to signal the underlying libs that they want to system to go forward + # with whatever function call, tool call or AI generated response the request calls for. Oh well, + # Que Sera Sera. + def get_human_input(self, prompt: str) -> str: + if self.a_human_input_response is None: + return super().get_human_input(prompt) + else: + response = self.a_human_input_response + self.a_human_input_response = None + return response + + async def a_get_human_input(self, prompt: str) -> str: + if self.message_processor and self.a_human_input_function: + message_dict = {"content": prompt, "role": "system", "type": "user-input-request"} + + message_payload = { + "recipient": self.name, + "sender": "system", + "message": message_dict, + "timestamp": datetime.now().isoformat(), + "sender_type": "system", + "connection_id": self.connection_id, + "message_type": "agent_message", + } + + socket_msg = SocketMessage( + type="user_input_request", + data=message_payload, + connection_id=self.connection_id, + ) + self.a_human_input_response = await self.a_human_input_function( + socket_msg.dict(), self.a_human_input_timeout + ) + return self.a_human_input_response -"" + else: + result = await super().a_get_human_input(prompt) + return result class ExtendedGroupChatManager(autogen.GroupChatManager): - def __init__(self, message_processor=None, *args, **kwargs): + def __init__( + self, + message_processor=None, + a_message_processor=None, + a_human_input_function=None, + a_human_input_timeout: Optional[int] = 60, + connection_id=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.message_processor = message_processor + self.a_message_processor = a_message_processor + self.a_human_input_function = a_human_input_function + self.a_human_input_response = None + self.a_human_input_timeout = a_human_input_timeout + self.connection_id = connection_id def receive( self, @@ -292,3 +1018,49 @@ def receive( if self.message_processor: self.message_processor(sender, self, message, request_reply, silent, sender_type="groupchat") super().receive(message, sender, request_reply, silent) + + async def a_receive( + self, + message: Union[Dict, str], + sender: autogen.Agent, + request_reply: Optional[bool] = None, + silent: Optional[bool] = False, + ) -> None: + if self.a_message_processor: + await self.a_message_processor(sender, self, message, request_reply, silent, sender_type="agent") + elif self.message_processor: + self.message_processor(sender, self, message, request_reply, silent, sender_type="agent") + await super().a_receive(message, sender, request_reply, silent) + + def get_human_input(self, prompt: str) -> str: + if self.a_human_input_response is None: + return super().get_human_input(prompt) + else: + response = self.a_human_input_response + self.a_human_input_response = None + return response + + async def a_get_human_input(self, prompt: str) -> str: + if self.message_processor and self.a_human_input_function: + message_dict = {"content": prompt, "role": "system", "type": "user-input-request"} + + message_payload = { + "recipient": self.name, + "sender": "system", + "message": message_dict, + "timestamp": datetime.now().isoformat(), + "sender_type": "system", + "connection_id": self.connection_id, + "message_type": "agent_message", + } + socket_msg = SocketMessage( + type="user_input_request", + data=message_payload, + connection_id=self.connection_id, + ) + result = await self.a_human_input_function(socket_msg.dict(), self.a_human_input_timeout) + return result + + else: + result = await super().a_get_human_input(prompt) + return result diff --git a/samples/apps/autogen-studio/frontend/gatsby-config.ts b/samples/apps/autogen-studio/frontend/gatsby-config.ts index 9644cfc03898..f66761c24be8 100644 --- a/samples/apps/autogen-studio/frontend/gatsby-config.ts +++ b/samples/apps/autogen-studio/frontend/gatsby-config.ts @@ -1,5 +1,5 @@ import type { GatsbyConfig } from "gatsby"; -import fs from 'fs'; +import fs from "fs"; const envFile = `.env.${process.env.NODE_ENV}`; @@ -14,7 +14,7 @@ require("dotenv").config({ }); const config: GatsbyConfig = { - pathPrefix: `${process.env.PREFIX_PATH_VALUE}`, + pathPrefix: process.env.PREFIX_PATH_VALUE || '', siteMetadata: { title: `AutoGen Studio [Beta]`, description: `Build Multi-Agent Apps`, diff --git a/samples/apps/autogen-studio/frontend/package.json b/samples/apps/autogen-studio/frontend/package.json index da33db85014c..7a06f09dac03 100644 --- a/samples/apps/autogen-studio/frontend/package.json +++ b/samples/apps/autogen-studio/frontend/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@ant-design/charts": "^1.3.6", + "@ant-design/plots": "^2.2.2", "@headlessui/react": "^1.7.16", "@heroicons/react": "^2.0.18", "@mdx-js/mdx": "^1.6.22", @@ -65,7 +66,6 @@ "@types/react-inner-image-zoom": "^3.0.0", "@types/react-resizable": "^3.0.2", "@types/uuid": "^9.0.8", - "gh-pages": "^4.0.0", "typescript": "^4.6.4" } } diff --git a/samples/apps/autogen-studio/frontend/src/components/atoms.tsx b/samples/apps/autogen-studio/frontend/src/components/atoms.tsx index c4c1368a1232..8f52e60281b7 100644 --- a/samples/apps/autogen-studio/frontend/src/components/atoms.tsx +++ b/samples/apps/autogen-studio/frontend/src/components/atoms.tsx @@ -49,7 +49,7 @@ export const SectionHeader = ({ icon, }: IProps) => { return ( -
+

{/* {count !== null && {count}} */} {icon && <>{icon}} @@ -72,6 +72,7 @@ export const IconButton = ({ }: IProps) => { return ( { return (