From 99718f778ffa14311a582eeaa94319319aac3d70 Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Tue, 4 Jun 2024 20:56:52 +0530 Subject: [PATCH 1/7] Polish deploy instructions (#285) * Update deployment information * Do not create duplicate applications * Update deployment instructions --- .../client/components/DynamicFormBuilder.tsx | 23 +++++++++++++------ .../components/TosAndMarketingEmailsModal.tsx | 2 +- .../tests/TosAndMarketingEmailsModal.test.tsx | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/client/components/DynamicFormBuilder.tsx b/app/src/client/components/DynamicFormBuilder.tsx index 25327c49..f43b00f3 100644 --- a/app/src/client/components/DynamicFormBuilder.tsx +++ b/app/src/client/components/DynamicFormBuilder.tsx @@ -62,6 +62,10 @@ const DynamicFormBuilder: React.FC = ({ const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + // Avoid creating duplicate applications + if (instructionForApplication && !updateExistingModel) { + return; + } setIsLoading(true); const isSecretUpdate = type_name === 'secret' && !!updateExistingModel; let formDataToSubmit: any = {}; @@ -153,13 +157,18 @@ The application is based on Prerequisites: Before you begin, ensure you have the following: 1. Fly.io account: -- Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans -require you to add your credit card information before you can proceed. If you don't, the deployment will fail. +- If you don't have a Fly.io account, you can create one here. +- Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans +require you to add your credit card information before you can proceed. If you don't, the deployment will fail. + +Important: If you already have a Fly.io account and created more than one organization, make sure you choose "Personal" as the organization + while creating the Fly.io API Token in the deployment steps below. + Deployment Steps: Step 1: Fork the GitHub Repository: 1.1 Fork this GitHub Repository to your account. Ensure the checkbox "Copy the main branch only" is checked. -Step 2: Generate Fly.io API Token:: -2.1 Go to your Fly.io dashboard and click on the Tokens tab. +Step 2: Generate Fly.io API Token: +2.1 Go to your Fly.io dashboard and click on the Tokens tab (the one on the left sidebar). 2.2 Enter a name and set the Optional Expiration to 999999h, then click on Create Organization Token to generate a token. 2.3 Copy the token, including the "FlyV1 " prefix and space at the beginning. Step 3: Set necessary GitHub action secrets: @@ -174,7 +183,7 @@ Before you begin, ensure you have the following: I understand my workflows, go ahead and enable them button. 4.2 On the left-hand side, you will see options like: All workflows, Fly Deployment Pipeline, Pipeline. 4.3 Click on the Fly Deployment Pipeline option and and then click the Run workflow button against the main branch. -4.4 Wait for the workflow to complete. Once completed, you will see the Client, Server, and Database apps +4.4 Wait for the workflow to complete (approx. 2 mins). Once completed, you will see the Client, Server, and Database apps created on Fly.io dashboard. 4.5 The workflow will only set up the applications in Fly.io and not deploy the actual application code which will be done in the next step. @@ -182,7 +191,7 @@ Before you begin, ensure you have the following: 5.1 The above workflow might have also created a pull request in your GitHub repository to update the fly.toml files. 5.2 Go to the Pull requests tab in your forked repository on GitHub and merge the PR named "Add Fly.io configuration files". 5.3 It will trigger the below workflows in sequence: -- Pipeline to run tests and verify the build +- Pipeline to run tests and verify the build (approx. 2 mins). - Pipeline to deploy the tested application to Fly.io (approx. 5 - 10 mins). 5.4 Once the workflow is completed, you can access your application using the hostname provided in the Fly.io dashboard. 5.5 Go to fly dashboard and click on the client application (similar to: fastagency-app-******-client). @@ -193,7 +202,7 @@ Before you begin, ensure you have the following: - Make sure you have added a payment method to your Fly.io account. Else, the deployment will fail. - Review the deployment logs on Fly.io for any error messages. You can access the logs by clicking on the server application on the Fly.io dashboard and then clicking on the Live Logs tab. -- If you need any help, please reach out to us on discord. +- If you need any help, please reach out to us on discord. ` : ''; diff --git a/app/src/client/components/TosAndMarketingEmailsModal.tsx b/app/src/client/components/TosAndMarketingEmailsModal.tsx index f4bf42ec..55f753aa 100644 --- a/app/src/client/components/TosAndMarketingEmailsModal.tsx +++ b/app/src/client/components/TosAndMarketingEmailsModal.tsx @@ -48,7 +48,7 @@ const TosAndMarketingEmailsModal = () => { hasSubscribedToMarketingEmails: marketingEmailsChecked, }), }); - history.push('/playground'); + history.push('/build'); } else { setErrorMessage(checkBoxErrMsg); } diff --git a/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx b/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx index 4f87eee1..9092fd7d 100644 --- a/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx +++ b/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx @@ -69,6 +69,6 @@ describe('TosAndMarketingEmailsModal', () => { const saveButton = screen.getByRole('button', { name: /Save/i }); fireEvent.click(saveButton); - expect(mockHistoryPush).toHaveBeenCalledWith('/playground'); + expect(mockHistoryPush).toHaveBeenCalledWith('/build'); }); }); From debe91f597c520b44ac9c8af7ba3881463563070 Mon Sep 17 00:00:00 2001 From: Kumaran Rajendhiran Date: Wed, 5 Jun 2024 09:54:43 +0530 Subject: [PATCH 2/7] Implement toolbox (#259) * Add OpenAPIAuth * Fix failing test case * Fix imports * Add toolbox and add a fixture to run fastapi for tests * Add uvicorn as testing dependency * Use same uvicorn version everywhere * Update test * Update tests * Reorder fastapi conftest * Add validation for toolbox openapi spec and update tests * Use multiprocess.Process instead of multiprocessing.Process * Rearrange conftest * Add db pytest mark * Remove useless parameters * Increase fastapi startup wait time for windows tests * Create client in create_autogen * Add toolboxes to agents * Fix bug in client generation code * Update test for toolbox * Use uvicorn.Server class to run FastAPI app in testing (#265) * Update error message * Debug test in windows * Increase timeout to 10 * Add debug step in workflow * Add dill based process for tests * Run as daemon * Use uvicorn server run_in_thread * Fix mypy error * Remove multiprocess as requirement * Remove all the debug stuff * Implement toolbox fix docs (#266) * fix: docs * fix: docs * Implement toolbox refactoring (#281) * refacotring * added from_db to base Model class * toolbox integrated into flow * Use cls.from_db method --------- Co-authored-by: Kumaran Rajendhiran * Integrate Toolboxes * Add fastapi weather app * Update model description * Do not use field name similar to type hint * Include servers in weather app * Fix problem in function calling in toolbox * Update banner image and polish deployment instructions --------- Co-authored-by: Davor Runje Co-authored-by: Harish Mohan Raj --- .secrets.baseline | 12 +- app/src/client/components/CustomSidebar.tsx | 86 ++++---- .../client/components/DynamicFormBuilder.tsx | 15 +- .../components/buildPage/Applications.tsx | 2 +- .../client/components/buildPage/ToolBoxes.tsx | 15 +- app/src/client/static/open-saas-banner.png | 4 +- app/src/client/utils/constants.ts | 1 + docker-compose.yaml | 1 + docs/docs/SUMMARY.md | 14 ++ .../en/api/fastagency/app/application_chat.md | 11 + .../en/api/fastagency/app/validate_toolbox.md | 11 + .../io/ionats/ErrorResoponseModel.md | 11 + .../applications/application/Application.md | 11 + .../teams/base/register_toolbox_functions.md | 11 + .../multi_agent_team/AutogenMultiAgentTeam.md | 11 + .../two_agent_teams/AutogenTwoAgentTeam.md | 11 + .../models/toolboxes/toolbox/FunctionInfo.md | 11 + .../models/toolboxes/toolbox/OpenAPIAuth.md | 11 + .../models/toolboxes/toolbox/Toolbox.md | 11 + fastagency/app.py | 40 +++- fastagency/io/ionats.py | 10 +- fastagency/models/__init__.py | 2 +- fastagency/models/agents/assistant.py | 19 +- fastagency/models/agents/base.py | 52 ++++- fastagency/models/agents/user_proxy.py | 20 +- fastagency/models/agents/web_surfer.py | 35 ++-- fastagency/models/applications/application.py | 2 +- fastagency/models/base.py | 16 +- fastagency/models/llms/azure.py | 19 +- fastagency/models/llms/openai.py | 20 +- fastagency/models/registry.py | 2 +- fastagency/models/secrets/__init__.py | 0 fastagency/models/teams/base.py | 19 +- fastagency/models/teams/multi_agent_team.py | 104 ++++------ fastagency/models/teams/two_agent_teams.py | 93 +++++---- fastagency/models/toolboxes/__init__.py | 1 + fastagency/models/toolboxes/toolbox.py | 96 +++++++++ fastagency/openapi/client.py | 28 ++- fastagency/weather_app.py | 69 +++++++ pyproject.toml | 4 +- scripts/build-docs.sh | 1 + scripts/run_server.sh | 2 + templates/main.jinja2 | 4 +- tests/app/test_get_schemas.py | 14 +- tests/app/test_openai_extensively.py | 165 ++++++++++++++- tests/conftest.py | 103 +++++++++- tests/models/agents/test_assistant.py | 99 ++++++++- tests/models/agents/test_user_proxy.py | 50 +---- tests/models/agents/test_web_surfer.py | 63 +++++- tests/models/applications/test_application.py | 2 +- tests/models/llms/test_azure.py | 17 +- tests/models/llms/test_openai.py | 17 +- tests/models/teams/test_base.py | 115 +++++++++++ tests/models/teams/test_multi_agents_team.py | 41 ++-- tests/models/teams/test_two_agents_team.py | 42 ++-- tests/models/test_registry.py | 4 +- tests/models/toolboxes/__init__.py | 0 tests/models/toolboxes/test_toolbox.py | 144 +++++++++++++ tests/openapi/templates/openapi2.json | 189 ++++++++++++++++++ .../test_fastapi_codegen_template.py | 9 +- tests/openapi/test_client.py | 21 +- tests/test_nats.py | 10 +- tests/test_weather_app.py | 32 +++ 63 files changed, 1673 insertions(+), 382 deletions(-) create mode 100644 docs/docs/en/api/fastagency/app/application_chat.md create mode 100644 docs/docs/en/api/fastagency/app/validate_toolbox.md create mode 100644 docs/docs/en/api/fastagency/io/ionats/ErrorResoponseModel.md create mode 100644 docs/docs/en/api/fastagency/models/applications/application/Application.md create mode 100644 docs/docs/en/api/fastagency/models/teams/base/register_toolbox_functions.md create mode 100644 docs/docs/en/api/fastagency/models/teams/multi_agent_team/AutogenMultiAgentTeam.md create mode 100644 docs/docs/en/api/fastagency/models/teams/two_agent_teams/AutogenTwoAgentTeam.md create mode 100644 docs/docs/en/api/fastagency/models/toolboxes/toolbox/FunctionInfo.md create mode 100644 docs/docs/en/api/fastagency/models/toolboxes/toolbox/OpenAPIAuth.md create mode 100644 docs/docs/en/api/fastagency/models/toolboxes/toolbox/Toolbox.md create mode 100644 fastagency/models/secrets/__init__.py create mode 100644 fastagency/models/toolboxes/__init__.py create mode 100644 fastagency/models/toolboxes/toolbox.py create mode 100644 fastagency/weather_app.py create mode 100644 tests/models/teams/test_base.py create mode 100644 tests/models/toolboxes/__init__.py create mode 100644 tests/models/toolboxes/test_toolbox.py create mode 100644 tests/openapi/templates/openapi2.json create mode 100644 tests/test_weather_app.py diff --git a/.secrets.baseline b/.secrets.baseline index 327f40cb..fa553df5 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -129,7 +129,17 @@ "line_number": 73, "is_secret": false } + ], + "tests/secrets/test_openapi_auth.py": [ + { + "type": "Secret Keyword", + "filename": "tests/secrets/test_openapi_auth.py", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 21, + "is_secret": false + } ] }, - "generated_at": "2024-05-16T16:39:55Z" + "generated_at": "2024-05-29T09:18:25Z" } diff --git a/app/src/client/components/CustomSidebar.tsx b/app/src/client/components/CustomSidebar.tsx index 6c996ce7..b1dbf638 100644 --- a/app/src/client/components/CustomSidebar.tsx +++ b/app/src/client/components/CustomSidebar.tsx @@ -54,6 +54,49 @@ export const navLinkItems: NavLinkItem[] = [ ), componentName: 'llm', }, + { + label: 'ToolBoxes', + svgIcon: ( + + + + + + + + + + + + ), + componentName: 'toolbox', + }, { label: 'Agents', svgIcon: ( @@ -142,49 +185,6 @@ export const navLinkItems: NavLinkItem[] = [ ), componentName: 'team', }, - { - label: 'ToolBoxes', - svgIcon: ( - - - - - - - - - - - - ), - componentName: 'toolbox', - }, { label: 'Applications', svgIcon: ( diff --git a/app/src/client/components/DynamicFormBuilder.tsx b/app/src/client/components/DynamicFormBuilder.tsx index f43b00f3..bb6fbee5 100644 --- a/app/src/client/components/DynamicFormBuilder.tsx +++ b/app/src/client/components/DynamicFormBuilder.tsx @@ -174,10 +174,10 @@ Before you begin, ensure you have the following: Step 3: Set necessary GitHub action secrets: 3.1 Create the below two "repository secrets" in your forked GitHub repository. Note: If you don't know how to create a secret, follow this guide. -====================================================================== -FLY_API_TOKEN: paste the value you copied from 2.3 +================================================================================== +FLY_API_TOKEN: <--- paste the value you copied from 2.3 ---> FASTAGENCY_APPLICATION_UUID: ${instructionForApplication} -====================================================================== +================================================================================== Step 4: Set up applications Fly.io: 4.1 Go to the Actions tab in your forked repository on GitHub and click the I understand my workflows, go ahead and enable them button. @@ -196,6 +196,15 @@ Before you begin, ensure you have the following: 5.4 Once the workflow is completed, you can access your application using the hostname provided in the Fly.io dashboard. 5.5 Go to fly dashboard and click on the client application (similar to: fastagency-app-******-client). 5.6 The hostname is the URL of your application. Open the URL in your browser to launch your application. +Application customization (Optional): +- You can perform basic customization such as changing the app name and adding a support email address in the generated application by + setting the below optional environment variables. +================================================================================== +REACT_APP_NAME: <--- Your App Name ---> +REACT_APP_SUPPORT_EMAIL: <--- Your Support Email Address ---> +================================================================================== +- After setting the environment variables, you can manualy trigger the Fly Deployment Pipeline workflow (refer 4.2 and 4.3) to deploy the changes. +- For further customization, you can refer to the Wasp documentation. Troubleshooting: If you encounter any issues during the deployment, check the following common problems: Deployment Failures: diff --git a/app/src/client/components/buildPage/Applications.tsx b/app/src/client/components/buildPage/Applications.tsx index 1aea07c3..6f9b3b5d 100644 --- a/app/src/client/components/buildPage/Applications.tsx +++ b/app/src/client/components/buildPage/Applications.tsx @@ -6,7 +6,7 @@ import { SecretsProps } from '../../interfaces/BuildPageInterfaces'; const Applications = ({ data }: SecretsProps) => { return ( <> - +
diff --git a/app/src/client/components/buildPage/ToolBoxes.tsx b/app/src/client/components/buildPage/ToolBoxes.tsx index 282b090c..bc3b0dda 100644 --- a/app/src/client/components/buildPage/ToolBoxes.tsx +++ b/app/src/client/components/buildPage/ToolBoxes.tsx @@ -1,17 +1,16 @@ +import React from 'react'; import CustomBreadcrumb from '../CustomBreadcrumb'; -const ToolBoxes = () => { +import UserPropertyHandler from './UserPropertyHandler'; +import { SecretsProps } from '../../interfaces/BuildPageInterfaces'; + +const ToolBoxes = ({ data }: SecretsProps) => { return ( <>
-
-
- Coming soon... -
+
+
diff --git a/app/src/client/static/open-saas-banner.png b/app/src/client/static/open-saas-banner.png index 8103ef5d..20ceb62c 100644 --- a/app/src/client/static/open-saas-banner.png +++ b/app/src/client/static/open-saas-banner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae48a6c16d3cb8582b1740fb35546ea8b4b74cdbcb7e48cd2af50ec6132641b7 -size 860453 +oid sha256:fae4608a644c2ba8f93f0547fb4ee133accc07ddc6daf0a9ff7296df164d860d +size 196930 diff --git a/app/src/client/utils/constants.ts b/app/src/client/utils/constants.ts index 25c0cdf5..e6ab5ca0 100644 --- a/app/src/client/utils/constants.ts +++ b/app/src/client/utils/constants.ts @@ -17,6 +17,7 @@ function deepFreeze(object: any) { export const propertyDependencyMap: PropertyDependencyMapProps = deepFreeze({ secret: [''], llm: ['secret'], + toolbox: [''], agent: ['secret', 'llm'], team: ['secret', 'llm', 'agent'], application: ['secret', 'llm', 'agent', 'team'], diff --git a/docker-compose.yaml b/docker-compose.yaml index 6acd1131..c0695c02 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,6 +18,7 @@ services: container_name: ${container_name} ports: - "8000:8000" + - "9000:9000" environment: - DOMAIN=${DOMAIN} - DATABASE_URL=${DATABASE_URL} diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md index 1909d65a..226041cb 100644 --- a/docs/docs/SUMMARY.md +++ b/docs/docs/SUMMARY.md @@ -8,6 +8,7 @@ search: - app - [ChatRequest](api/fastagency/app/ChatRequest.md) - [add_model](api/fastagency/app/add_model.md) + - [application_chat](api/fastagency/app/application_chat.md) - [chat](api/fastagency/app/chat.md) - [generate_chat_name](api/fastagency/app/generate_chat_name.md) - [get_all_models](api/fastagency/app/get_all_models.md) @@ -18,6 +19,7 @@ search: - [update_model](api/fastagency/app/update_model.md) - [validate_model](api/fastagency/app/validate_model.md) - [validate_secret_model](api/fastagency/app/validate_secret_model.md) + - [validate_toolbox](api/fastagency/app/validate_toolbox.md) - db - helpers - [find_model_using_raw](api/fastagency/db/helpers/find_model_using_raw.md) @@ -27,6 +29,7 @@ search: - [ping_handler](api/fastagency/faststream_app/ping_handler.md) - io - ionats + - [ErrorResoponseModel](api/fastagency/io/ionats/ErrorResoponseModel.md) - [IONats](api/fastagency/io/ionats/IONats.md) - [InitiateModel](api/fastagency/io/ionats/InitiateModel.md) - [InputRequestModel](api/fastagency/io/ionats/InputRequestModel.md) @@ -47,6 +50,9 @@ search: - web_surfer - [BingAPIKey](api/fastagency/models/agents/web_surfer/BingAPIKey.md) - [WebSurferAgent](api/fastagency/models/agents/web_surfer/WebSurferAgent.md) + - applications + - application + - [Application](api/fastagency/models/applications/application/Application.md) - base - [Model](api/fastagency/models/base/Model.md) - [ModelTypeFinder](api/fastagency/models/base/ModelTypeFinder.md) @@ -69,10 +75,18 @@ search: - teams - base - [TeamBaseModel](api/fastagency/models/teams/base/TeamBaseModel.md) + - [register_toolbox_functions](api/fastagency/models/teams/base/register_toolbox_functions.md) - multi_agent_team + - [AutogenMultiAgentTeam](api/fastagency/models/teams/multi_agent_team/AutogenMultiAgentTeam.md) - [MultiAgentTeam](api/fastagency/models/teams/multi_agent_team/MultiAgentTeam.md) - two_agent_teams + - [AutogenTwoAgentTeam](api/fastagency/models/teams/two_agent_teams/AutogenTwoAgentTeam.md) - [TwoAgentTeam](api/fastagency/models/teams/two_agent_teams/TwoAgentTeam.md) + - toolboxes + - toolbox + - [FunctionInfo](api/fastagency/models/toolboxes/toolbox/FunctionInfo.md) + - [OpenAPIAuth](api/fastagency/models/toolboxes/toolbox/OpenAPIAuth.md) + - [Toolbox](api/fastagency/models/toolboxes/toolbox/Toolbox.md) - openapi - client - [Client](api/fastagency/openapi/client/Client.md) diff --git a/docs/docs/en/api/fastagency/app/application_chat.md b/docs/docs/en/api/fastagency/app/application_chat.md new file mode 100644 index 00000000..7ce13144 --- /dev/null +++ b/docs/docs/en/api/fastagency/app/application_chat.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.app.application_chat diff --git a/docs/docs/en/api/fastagency/app/validate_toolbox.md b/docs/docs/en/api/fastagency/app/validate_toolbox.md new file mode 100644 index 00000000..bdd89af8 --- /dev/null +++ b/docs/docs/en/api/fastagency/app/validate_toolbox.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.app.validate_toolbox diff --git a/docs/docs/en/api/fastagency/io/ionats/ErrorResoponseModel.md b/docs/docs/en/api/fastagency/io/ionats/ErrorResoponseModel.md new file mode 100644 index 00000000..5121eb47 --- /dev/null +++ b/docs/docs/en/api/fastagency/io/ionats/ErrorResoponseModel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.io.ionats.ErrorResoponseModel diff --git a/docs/docs/en/api/fastagency/models/applications/application/Application.md b/docs/docs/en/api/fastagency/models/applications/application/Application.md new file mode 100644 index 00000000..a2b4350b --- /dev/null +++ b/docs/docs/en/api/fastagency/models/applications/application/Application.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.applications.application.Application diff --git a/docs/docs/en/api/fastagency/models/teams/base/register_toolbox_functions.md b/docs/docs/en/api/fastagency/models/teams/base/register_toolbox_functions.md new file mode 100644 index 00000000..3b6e1a48 --- /dev/null +++ b/docs/docs/en/api/fastagency/models/teams/base/register_toolbox_functions.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.teams.base.register_toolbox_functions diff --git a/docs/docs/en/api/fastagency/models/teams/multi_agent_team/AutogenMultiAgentTeam.md b/docs/docs/en/api/fastagency/models/teams/multi_agent_team/AutogenMultiAgentTeam.md new file mode 100644 index 00000000..202e106f --- /dev/null +++ b/docs/docs/en/api/fastagency/models/teams/multi_agent_team/AutogenMultiAgentTeam.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.teams.multi_agent_team.AutogenMultiAgentTeam diff --git a/docs/docs/en/api/fastagency/models/teams/two_agent_teams/AutogenTwoAgentTeam.md b/docs/docs/en/api/fastagency/models/teams/two_agent_teams/AutogenTwoAgentTeam.md new file mode 100644 index 00000000..b1e59e71 --- /dev/null +++ b/docs/docs/en/api/fastagency/models/teams/two_agent_teams/AutogenTwoAgentTeam.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.teams.two_agent_teams.AutogenTwoAgentTeam diff --git a/docs/docs/en/api/fastagency/models/toolboxes/toolbox/FunctionInfo.md b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/FunctionInfo.md new file mode 100644 index 00000000..740b1767 --- /dev/null +++ b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/FunctionInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.toolboxes.toolbox.FunctionInfo diff --git a/docs/docs/en/api/fastagency/models/toolboxes/toolbox/OpenAPIAuth.md b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/OpenAPIAuth.md new file mode 100644 index 00000000..2fe8c003 --- /dev/null +++ b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/OpenAPIAuth.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.toolboxes.toolbox.OpenAPIAuth diff --git a/docs/docs/en/api/fastagency/models/toolboxes/toolbox/Toolbox.md b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/Toolbox.md new file mode 100644 index 00000000..ed04a298 --- /dev/null +++ b/docs/docs/en/api/fastagency/models/toolboxes/toolbox/Toolbox.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.models.toolboxes.toolbox.Toolbox diff --git a/fastagency/app.py b/fastagency/app.py index 579ee75a..6c5b2f11 100644 --- a/fastagency/app.py +++ b/fastagency/app.py @@ -4,6 +4,8 @@ from typing import Any, Dict, List, Optional, Union from uuid import UUID +import httpx +import yaml from fastapi import FastAPI, HTTPException from openai import AsyncAzureOpenAI from prisma.models import Model @@ -15,6 +17,7 @@ get_wasp_db_url, ) from .models.registry import Registry, Schemas +from .models.toolboxes.toolbox import Toolbox logging.basicConfig(level=logging.INFO) @@ -27,10 +30,41 @@ async def get_models_schemas() -> Schemas: return schemas +async def validate_toolbox(toolbox: Toolbox) -> None: + try: + async with httpx.AsyncClient() as client: + resp = await client.get(toolbox.openapi_url) # type: ignore[arg-type] + except Exception as e: + raise HTTPException(status_code=422, detail="OpenAPI URL is invalid") from e + + if not (resp.status_code >= 200 and resp.status_code < 400): + raise HTTPException( + status_code=422, detail=f"OpenAPI URL returns error code {resp.status_code}" + ) + + try: + if "yaml" in toolbox.openapi_url or "yml" in toolbox.openapi_url: # type: ignore [operator] + openapi_spec = yaml.safe_load(resp.text) + else: + openapi_spec = resp.json() + + if "openapi" not in openapi_spec: + raise HTTPException( + status_code=422, + detail="OpenAPI URL does not contain a valid OpenAPI spec", + ) + except Exception as e: + raise HTTPException( + status_code=422, detail="OpenAPI URL does not contain a valid OpenAPI spec" + ) from e + + @app.post("/models/{type}/{name}/validate") async def validate_model(type: str, name: str, model: Dict[str, Any]) -> Dict[str, Any]: try: validated_model = Registry.get_default().validate(type, name, model) + if isinstance(validated_model, Toolbox): + await validate_toolbox(validated_model) return validated_model.model_dump() except ValidationError as e: raise HTTPException(status_code=422, detail=json.loads(e.json())) from e @@ -43,7 +77,8 @@ async def validate_secret_model( type: str = "secret" found_model = await find_model_using_raw(model_uuid=model_uuid) - model["api_key"] = found_model["json_str"]["api_key"] + if "api_key" in found_model["json_str"]: + model["api_key"] = found_model["json_str"]["api_key"] try: validated_model = Registry.get_default().validate(type, name, model) return validated_model.model_dump() @@ -88,7 +123,8 @@ async def get_all_models( ret_val = [] for model in ret_val_without_mask: if model["type_name"] == "secret": - model["json_str"]["api_key"] = await mask(model["json_str"]["api_key"]) + if "api_key" in model["json_str"]: + model["json_str"]["api_key"] = await mask(model["json_str"]["api_key"]) ret_val.append(model) return ret_val # type: ignore[no-any-return] diff --git a/fastagency/io/ionats.py b/fastagency/io/ionats.py index 05eb9559..4bf7a562 100644 --- a/fastagency/io/ionats.py +++ b/fastagency/io/ionats.py @@ -148,8 +148,10 @@ class InitiateModel(BaseModel): # patch this is tests -def create_team(team_id: UUID, user_id: UUID) -> Callable[[str], List[Dict[str, Any]]]: - team_dict = syncify(find_model_using_raw)(team_id) +async def create_team( + team_id: UUID, user_id: UUID +) -> Callable[[str], List[Dict[str, Any]]]: + team_dict = await find_model_using_raw(team_id) team_model: Union[TwoAgentTeam, MultiAgentTeam] if "initial_agent" in team_dict["json_str"]: @@ -159,7 +161,7 @@ def create_team(team_id: UUID, user_id: UUID) -> Callable[[str], List[Dict[str, else: raise ValueError(f"Unknown team model {team_dict['json_str']}") - autogen_team = team_model.create_autogen(team_id, user_id) + autogen_team = await team_model.create_autogen(team_id, user_id) return autogen_team.initiate_chat # type: ignore[no-any-return] @@ -190,7 +192,7 @@ def start_chat() -> Optional[List[Dict[str, Any]]]: # type: ignore [return] ) with IOStream.set_default(iostream): - initiate_chat = create_team( + initiate_chat = syncify(create_team)( team_id=body.team_id, user_id=body.user_id ) chat_result = initiate_chat(body.msg) diff --git a/fastagency/models/__init__.py b/fastagency/models/__init__.py index f40677ce..0c616f40 100644 --- a/fastagency/models/__init__.py +++ b/fastagency/models/__init__.py @@ -1,6 +1,6 @@ # from .registry import Registry # ModelSchema, ModelSchemas, Registry, Schemas -from . import agents, applications # noqa: F401 +from . import agents, applications, llms, secrets, teams, toolboxes # noqa: F401 from .registry import Registry __all__ = ["Registry"] diff --git a/fastagency/models/agents/assistant.py b/fastagency/models/agents/assistant.py index b7b4da02..020e87c1 100644 --- a/fastagency/models/agents/assistant.py +++ b/fastagency/models/agents/assistant.py @@ -2,10 +2,8 @@ from uuid import UUID import autogen -from asyncer import syncify from pydantic import Field -from ...db.helpers import find_model_using_raw from ..registry import register from .base import AgentBaseModel @@ -20,19 +18,20 @@ class AssistantAgent(AgentBaseModel): ] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + my_model = await cls.from_db(model_id) - llm_dict = syncify(find_model_using_raw)(my_model.llm.uuid) - llm_model = my_model.llm.get_data_model()(**llm_dict["json_str"]) - llm = llm_model.create_autogen(my_model.llm.uuid, user_id) + llm_model = await my_model.llm.get_data_model().from_db(my_model.llm.uuid) - agent_name = my_model_dict["model_name"] + llm = await llm_model.create_autogen(my_model.llm.uuid, user_id) + + functions = await my_model.get_functions_from_toolboxes(user_id) + + agent_name = my_model.name agent = autogen.agentchat.AssistantAgent( name=agent_name, llm_config=llm, system_message=my_model.system_message, ) - return agent + return agent, functions diff --git a/fastagency/models/agents/base.py b/fastagency/models/agents/base.py index 7b5f1900..9215161c 100644 --- a/fastagency/models/agents/base.py +++ b/fastagency/models/agents/base.py @@ -1,10 +1,13 @@ -from typing import Annotated, Union +from typing import Annotated, Dict, List, Optional, Union +from uuid import UUID from pydantic import Field from typing_extensions import TypeAlias +from ...db.helpers import find_model_using_raw from ..base import Model from ..registry import Registry +from ..toolboxes.toolbox import FunctionInfo, ToolboxRef __all__ = ["AgentBaseModel"] @@ -22,3 +25,50 @@ class AgentBaseModel(Model): description="LLM used by the agent for producing responses", ), ] + + toolbox_1: Annotated[ + Optional[ToolboxRef], + Field( + title="Toolbox", + description="Toolbox used by the agent for producing responses", + ), + ] = None + + toolbox_2: Annotated[ + Optional[ToolboxRef], + Field( + title="Toolbox", + description="Toolbox used by the agent for producing responses", + ), + ] = None + + toolbox_3: Annotated[ + Optional[ToolboxRef], + Field( + title="Toolbox", + description="Toolbox used by the agent for producing responses", + ), + ] = None + + async def get_clients_from_toolboxes( + self, user_id: UUID + ) -> Dict[str, List[FunctionInfo]]: + clients: Dict[str, List[FunctionInfo]] = {} + for i in range(3): + toolbox_property = getattr(self, f"toolbox_{i+1}") + if toolbox_property is None: + continue + + toolbox_dict = await find_model_using_raw(toolbox_property.uuid) + toolbox_model = toolbox_property.get_data_model()( + **toolbox_dict["json_str"] + ) + client = await toolbox_model.create_autogen(toolbox_property.uuid, user_id) + clients[f"client_{i+1}"] = client + return clients + + async def get_functions_from_toolboxes(self, user_id: UUID) -> List[FunctionInfo]: + clients = await self.get_clients_from_toolboxes(user_id) + functions = [x for _, xs in clients.items() for x in xs] + + return functions diff --git a/fastagency/models/agents/user_proxy.py b/fastagency/models/agents/user_proxy.py index 8d360d47..31f9eb8f 100644 --- a/fastagency/models/agents/user_proxy.py +++ b/fastagency/models/agents/user_proxy.py @@ -2,16 +2,14 @@ from uuid import UUID import autogen -from asyncer import syncify from pydantic import Field -from ...db.helpers import find_model_using_raw +from ..base import Model from ..registry import register -from .base import AgentBaseModel @register("agent") -class UserProxyAgent(AgentBaseModel): +class UserProxyAgent(Model): max_consecutive_auto_reply: Annotated[ Optional[int], Field( @@ -20,19 +18,13 @@ class UserProxyAgent(AgentBaseModel): ] = None @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + my_model = await cls.from_db(model_id) - llm_dict = syncify(find_model_using_raw)(my_model.llm.uuid) - llm_model = my_model.llm.get_data_model()(**llm_dict["json_str"]) - llm = llm_model.create_autogen(my_model.llm.uuid, user_id) - - agent_name = my_model_dict["model_name"] + agent_name = my_model.name agent = autogen.agentchat.UserProxyAgent( name=agent_name, - llm_config=llm, max_consecutive_auto_reply=my_model.max_consecutive_auto_reply, ) - return agent + return agent, [] diff --git a/fastagency/models/agents/web_surfer.py b/fastagency/models/agents/web_surfer.py index 5d15a408..ee64d928 100644 --- a/fastagency/models/agents/web_surfer.py +++ b/fastagency/models/agents/web_surfer.py @@ -2,11 +2,9 @@ from uuid import UUID import autogen.agentchat.contrib.web_surfer -from asyncer import syncify from pydantic import Field from typing_extensions import TypeAlias -from ...db.helpers import find_model_using_raw from ..base import Model from ..registry import register from .base import AgentBaseModel, llm_type_refs @@ -16,12 +14,11 @@ @register("secret") class BingAPIKey(Model): - api_key: Annotated[str, Field(description="The API Key from OpenAI")] + api_key: Annotated[str, Field(description="The API Key from Bing")] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: + my_model = await cls.from_db(model_id) return my_model.api_key @@ -46,21 +43,20 @@ class WebSurferAgent(AgentBaseModel): ] = None @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + my_model = await cls.from_db(model_id) - llm_dict = syncify(find_model_using_raw)(my_model.llm.uuid) - llm_model = my_model.llm.get_data_model()(**llm_dict["json_str"]) - llm = llm_model.create_autogen(my_model.llm.uuid, user_id) + llm_model = await my_model.llm.get_data_model().from_db(my_model.llm.uuid) - summarizer_llm_dict = syncify(find_model_using_raw)( + llm = await llm_model.create_autogen(my_model.llm.uuid, user_id) + + clients = await my_model.get_clients_from_toolboxes(user_id) # noqa: F841 + + summarizer_llm_model = await my_model.summarizer_llm.get_data_model().from_db( my_model.summarizer_llm.uuid ) - summarizer_llm_model = my_model.summarizer_llm.get_data_model()( - **summarizer_llm_dict["json_str"] - ) - summarizer_llm = summarizer_llm_model.create_autogen( + + summarizer_llm = await summarizer_llm_model.create_autogen( my_model.summarizer_llm.uuid, user_id ) @@ -68,7 +64,7 @@ def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: "viewport_size": my_model.viewport_size, "bing_api_key": my_model.bing_api_key, } - agent_name = my_model_dict["model_name"] + agent_name = my_model.name agent = autogen.agentchat.contrib.web_surfer.WebSurferAgent( name=agent_name, @@ -76,4 +72,5 @@ def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: summarizer_llm_config=summarizer_llm, browser_config=browser_config, ) - return agent + + return agent, [] diff --git a/fastagency/models/applications/application.py b/fastagency/models/applications/application.py index 4a30cde4..ed4e0567 100644 --- a/fastagency/models/applications/application.py +++ b/fastagency/models/applications/application.py @@ -26,5 +26,5 @@ class Application(Model): ] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: raise NotImplementedError diff --git a/fastagency/models/base.py b/fastagency/models/base.py index 41ea0ad0..8decdf99 100644 --- a/fastagency/models/base.py +++ b/fastagency/models/base.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, Field, create_model, model_validator from typing_extensions import TypeAlias +from ..db.helpers import find_model_using_raw + M = TypeVar("M", bound="Model") __all__ = [ @@ -15,9 +17,12 @@ ] +T = TypeVar("T", bound="Model") + + # abstract class class Model(BaseModel, ABC): - name: Annotated[str, Field(..., description="The name of the model", min_length=1)] + name: Annotated[str, Field(..., description="The name of the item", min_length=1)] _reference_model: "Optional[Type[ObjectReference]]" = None @classmethod @@ -28,10 +33,17 @@ def get_reference_model(cls) -> "Type[ObjectReference]": @classmethod @abstractmethod - def create_autogen( + async def create_autogen( cls, model_id: UUID, user_id: UUID ) -> Any: ... # pragma: no cover + @classmethod + async def from_db(cls: Type[T], model_id: UUID) -> T: + my_model_dict = await find_model_using_raw(model_id) + my_model = cls(**my_model_dict["json_str"]) + + return my_model + class ObjectReference(BaseModel): type: Annotated[str, Field(description="The name of the type of the data")] = "" diff --git a/fastagency/models/llms/azure.py b/fastagency/models/llms/azure.py index 52ad09ff..ec15e235 100644 --- a/fastagency/models/llms/azure.py +++ b/fastagency/models/llms/azure.py @@ -1,12 +1,10 @@ from typing import Annotated, Any, Dict, Literal from uuid import UUID -from asyncer import syncify from pydantic import AfterValidator, Field, HttpUrl from typing_extensions import TypeAlias from ...constants import AZURE_API_VERSIONS_LITERAL -from ...db.helpers import find_model_using_raw from ..base import Model from ..registry import register @@ -21,9 +19,8 @@ class AzureOAIAPIKey(Model): api_key: Annotated[str, Field(description="The API Key from Azure OpenAI")] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: + my_model = await cls.from_db(model_id) return my_model.api_key @@ -63,13 +60,13 @@ class AzureOAI(Model): ] = "latest" @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Dict[str, Any]: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Dict[str, Any]: + my_model = await cls.from_db(model_id) - api_key_dict = syncify(find_model_using_raw)(my_model.api_key.uuid) - api_key_model = my_model.api_key.get_data_model()(**api_key_dict["json_str"]) - api_key = api_key_model.create_autogen(my_model.api_key.uuid, user_id) + api_key_model = await my_model.api_key.get_data_model().from_db( + my_model.api_key.uuid + ) + api_key = await api_key_model.create_autogen(my_model.api_key.uuid, user_id) config_list = [ { diff --git a/fastagency/models/llms/openai.py b/fastagency/models/llms/openai.py index a78c5e24..ee5bfafe 100644 --- a/fastagency/models/llms/openai.py +++ b/fastagency/models/llms/openai.py @@ -1,12 +1,10 @@ from typing import Annotated, Any, Dict, Literal from uuid import UUID -from asyncer import syncify from pydantic import AfterValidator, Field, HttpUrl from typing_extensions import TypeAlias from ...constants import OPENAI_MODELS_LITERAL -from ...db.helpers import find_model_using_raw from ..base import Model from ..registry import register @@ -27,9 +25,8 @@ class OpenAIAPIKey(Model): ] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> str: + my_model: OpenAIAPIKey = await cls.from_db(model_id) return my_model.api_key @@ -60,13 +57,14 @@ class OpenAI(Model): ] = "openai" @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Dict[str, Any]: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Dict[str, Any]: + my_model: OpenAI = await cls.from_db(model_id) - api_key_dict = syncify(find_model_using_raw)(my_model.api_key.uuid) - api_key_model = my_model.api_key.get_data_model()(**api_key_dict["json_str"]) - api_key = api_key_model.create_autogen(my_model.api_key.uuid, user_id) + api_key_model: OpenAIAPIKey = await my_model.api_key.get_data_model().from_db( + my_model.api_key.uuid + ) + + api_key = await api_key_model.create_autogen(my_model.api_key.uuid, user_id) config_list = [ { diff --git a/fastagency/models/registry.py b/fastagency/models/registry.py index da93879e..7b448fa0 100644 --- a/fastagency/models/registry.py +++ b/fastagency/models/registry.py @@ -28,7 +28,7 @@ class ModelSchema(BaseModel): - name: Annotated[str, Field(description="The name of the model")] + name: Annotated[str, Field(description="The name of the item")] json_schema: Annotated[ Dict[str, Any], Field(description="The schema for the model") ] diff --git a/fastagency/models/secrets/__init__.py b/fastagency/models/secrets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastagency/models/teams/base.py b/fastagency/models/teams/base.py index 5a70c769..f939a212 100644 --- a/fastagency/models/teams/base.py +++ b/fastagency/models/teams/base.py @@ -1,10 +1,12 @@ -from typing import Annotated, Literal, Union +from typing import Annotated, List, Literal, Union +from autogen.agentchat import ConversableAgent from pydantic import Field from typing_extensions import TypeAlias from ..base import Model from ..registry import Registry +from ..toolboxes.toolbox import FunctionInfo __all__ = ["TeamBaseModel", "agent_type_refs"] @@ -29,3 +31,18 @@ class TeamBaseModel(Model): description="Mode for human input", ), ] = "ALWAYS" + + +def register_toolbox_functions( + agent: ConversableAgent, + execution_agents: List[ConversableAgent], + function_infos: List[FunctionInfo], +) -> None: + for function_info in function_infos: + agent.register_for_llm( + name=function_info.name, + description=function_info.description, + )(function_info.function) + + for execution_agent in execution_agents: + execution_agent.register_for_execution()(function_info.function) diff --git a/fastagency/models/teams/multi_agent_team.py b/fastagency/models/teams/multi_agent_team.py index 1c479f4b..10f89489 100644 --- a/fastagency/models/teams/multi_agent_team.py +++ b/fastagency/models/teams/multi_agent_team.py @@ -1,20 +1,42 @@ -from typing import Annotated, Any, Dict, List, Optional +from typing import Annotated, Any, Dict, List, Optional, Tuple from uuid import UUID -import autogen -from asyncer import syncify -from autogen import GroupChat, GroupChatManager +from autogen import ConversableAgent, GroupChat, GroupChatManager from pydantic import Field -from ...db.helpers import find_model_using_raw from ..registry import Registry -from .base import TeamBaseModel, agent_type_refs +from ..toolboxes.toolbox import FunctionInfo +from .base import TeamBaseModel, agent_type_refs, register_toolbox_functions __all__ = ["MultiAgentTeam"] registry = Registry.get_default() +class AutogenMultiAgentTeam: + def __init__( + self, + agents_and_functions: List[Tuple[ConversableAgent, List[FunctionInfo]]], + ) -> None: + self.agents = [agent for agent, _ in agents_and_functions] + self.functions = [functions for _, functions in agents_and_functions] + + for i, (agent, functions) in enumerate(agents_and_functions): + other_agents = [ + other_agent + for j, (other_agent, _) in enumerate(agents_and_functions) + if i != j + ] + register_toolbox_functions(agent, other_agents, functions) + + def initiate_chat(self, message: str) -> List[Dict[str, Any]]: + groupchat = GroupChat(agents=self.agents, messages=[]) + manager = GroupChatManager(groupchat=groupchat) + return self.agents[0].initiate_chat( # type: ignore[no-any-return] + recipient=manager, message=message + ) + + @registry.register("team") class MultiAgentTeam(TeamBaseModel): agent_1: Annotated[ @@ -54,74 +76,22 @@ class MultiAgentTeam(TeamBaseModel): ] = None @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + my_model = await cls.from_db(model_id) - agents = {} + agents_and_functions: List[Tuple[ConversableAgent, List[FunctionInfo]]] = [] for i in range(5): agent_property = getattr(my_model, f"agent_{i+1}") if agent_property is None: continue - agent_dict = syncify(find_model_using_raw)( - getattr(my_model, f"agent_{i+1}").uuid - ) - agent_model = getattr(my_model, f"agent_{i+1}").get_data_model()( - **agent_dict["json_str"] + agent_model = await agent_property.get_data_model().from_db( + agent_property.uuid ) - agent = agent_model.create_autogen( + + agent, functions = await agent_model.create_autogen( getattr(my_model, f"agent_{i+1}").uuid, user_id ) - agents[f"agent_{i+1}"] = agent - - class AutogenMultiAgentTeam: - def __init__( - self, - agent_1: agent_type_refs, - agent_2: agent_type_refs, - agent_3: Optional[agent_type_refs] = None, - agent_4: Optional[agent_type_refs] = None, - agent_5: Optional[agent_type_refs] = None, - ) -> None: - self.agent_1 = agent_1 - self.agent_2 = agent_2 - self.agent_3 = agent_3 - self.agent_4 = agent_4 - self.agent_5 = agent_5 - self.agents = [ - getattr(self, f"agent_{i+1}") - for i in range(5) - if getattr(self, f"agent_{i+1}") is not None - ] - - if isinstance( - self.agent_1, autogen.agentchat.AssistantAgent - ) and isinstance(self.agent_2, autogen.agentchat.UserProxyAgent): - assistant_agent = self.agent_1 - user_proxy_agent = self.agent_2 - elif isinstance( - self.agent_1, autogen.agentchat.UserProxyAgent - ) and isinstance(self.agent_2, autogen.agentchat.AssistantAgent): - user_proxy_agent = self.agent_1 - assistant_agent = self.agent_2 - else: - raise ValueError( - "Atleast one agent must be of type AssistantAgent and one must be of type UserProxyAgent" - ) - - @user_proxy_agent.register_for_execution() # type: ignore [misc] - @assistant_agent.register_for_llm( - description="Get weather forecast for a city" - ) # type: ignore [misc] - def get_forecast_for_city(city: str) -> str: - return f"The weather in {city} is sunny today." - - def initiate_chat(self, message: str) -> List[Dict[str, Any]]: - groupchat = GroupChat(agents=self.agents, messages=[]) - manager = GroupChatManager(groupchat=groupchat) - return self.agent_1.initiate_chat( # type: ignore[no-any-return] - recipient=manager, message=message - ) + agents_and_functions.append((agent, functions)) - return AutogenMultiAgentTeam(**agents) + return AutogenMultiAgentTeam(agents_and_functions) diff --git a/fastagency/models/teams/two_agent_teams.py b/fastagency/models/teams/two_agent_teams.py index a626f21a..9da03942 100644 --- a/fastagency/models/teams/two_agent_teams.py +++ b/fastagency/models/teams/two_agent_teams.py @@ -1,17 +1,40 @@ from typing import Annotated, Any, Dict, List from uuid import UUID -import autogen -from asyncer import syncify +from autogen import ConversableAgent from pydantic import Field -from ...db.helpers import find_model_using_raw from ..registry import Registry -from .base import TeamBaseModel, agent_type_refs +from ..toolboxes.toolbox import FunctionInfo +from .base import TeamBaseModel, agent_type_refs, register_toolbox_functions __all__ = ["TwoAgentTeam"] +class AutogenTwoAgentTeam: + def __init__( + self, + initial_agent: ConversableAgent, + initial_agent_functions: List[FunctionInfo], + secondary_agent: ConversableAgent, + secondary_agent_functions: List[FunctionInfo], + ) -> None: + self.initial_agent = initial_agent + self.secondary_agent = secondary_agent + + register_toolbox_functions( + initial_agent, [secondary_agent], initial_agent_functions + ) + register_toolbox_functions( + secondary_agent, [initial_agent], secondary_agent_functions + ) + + def initiate_chat(self, message: str) -> List[Dict[str, Any]]: + return self.initial_agent.initiate_chat( # type: ignore[no-any-return] + recipient=self.secondary_agent, message=message + ) + + @Registry.get_default().register("team") class TwoAgentTeam(TeamBaseModel): initial_agent: Annotated[ @@ -30,56 +53,32 @@ class TwoAgentTeam(TeamBaseModel): ] @classmethod - def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: - my_model_dict = syncify(find_model_using_raw)(model_id) - my_model = cls(**my_model_dict["json_str"]) + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Any: + my_model = await cls.from_db(model_id) - initial_agent_dict = syncify(find_model_using_raw)(my_model.initial_agent.uuid) - initial_agent_model = my_model.initial_agent.get_data_model()( - **initial_agent_dict["json_str"] + initial_agent_model = await my_model.initial_agent.get_data_model().from_db( + my_model.initial_agent.uuid ) - initial_agent = initial_agent_model.create_autogen( + ( + initial_agent, + initial_agent_functions, + ) = await initial_agent_model.create_autogen( my_model.initial_agent.uuid, user_id ) - secondary_agent_dict = syncify(find_model_using_raw)( + secondary_agent_model = await my_model.secondary_agent.get_data_model().from_db( my_model.secondary_agent.uuid ) - secondary_agent_model = my_model.secondary_agent.get_data_model()( - **secondary_agent_dict["json_str"] - ) - secondary_agent = secondary_agent_model.create_autogen( + ( + secondary_agent, + secondary_agent_functions, + ) = await secondary_agent_model.create_autogen( my_model.secondary_agent.uuid, user_id ) - class AutogenTwoAgentTeam: - def __init__( - self, initial_agent: agent_type_refs, secondary_agent: agent_type_refs - ) -> None: - self.initial_agent = initial_agent - self.secondary_agent = secondary_agent - - if isinstance(self.initial_agent, autogen.agentchat.AssistantAgent): - assistant_agent = self.initial_agent - user_proxy_agent = self.secondary_agent - elif isinstance(self.initial_agent, autogen.agentchat.UserProxyAgent): - user_proxy_agent = self.initial_agent - assistant_agent = self.secondary_agent - else: - raise ValueError( - "Agents must be of type AssistantAgent and UserProxyAgent" - ) - - @user_proxy_agent.register_for_execution() # type: ignore [misc] - @assistant_agent.register_for_llm( - description="Get weather forecast for a city" - ) # type: ignore [misc] - def get_forecast_for_city(city: str) -> str: - return f"The weather in {city} is sunny today." - - def initiate_chat(self, message: str) -> List[Dict[str, Any]]: - return self.initial_agent.initiate_chat( # type: ignore[no-any-return] - recipient=self.secondary_agent, message=message - ) - - return AutogenTwoAgentTeam(initial_agent, secondary_agent) + return AutogenTwoAgentTeam( + initial_agent, + initial_agent_functions, + secondary_agent, + secondary_agent_functions, + ) diff --git a/fastagency/models/toolboxes/__init__.py b/fastagency/models/toolboxes/__init__.py new file mode 100644 index 00000000..a3960fbc --- /dev/null +++ b/fastagency/models/toolboxes/__init__.py @@ -0,0 +1 @@ +from . import toolbox # noqa: F401 diff --git a/fastagency/models/toolboxes/toolbox.py b/fastagency/models/toolboxes/toolbox.py new file mode 100644 index 00000000..8ecef85b --- /dev/null +++ b/fastagency/models/toolboxes/toolbox.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from typing import Annotated, Any, Callable, List, Optional, Tuple +from uuid import UUID + +import httpx +from pydantic import AfterValidator, Field, HttpUrl +from typing_extensions import TypeAlias + +from ...openapi.client import Client +from ..base import Model +from ..registry import Registry + +# Pydantic adds trailing slash automatically to URLs, so we need to remove it +# https://github.com/pydantic/pydantic/issues/7186#issuecomment-1691594032 +URL = Annotated[HttpUrl, AfterValidator(lambda x: str(x).rstrip("/"))] + +__all__ = [ + "OpenAPIAuth", + "Toolbox", +] + + +@Registry.get_default().register("secret") +class OpenAPIAuth(Model): + username: Annotated[ + str, + Field( + description="username for openapi routes authentication", + ), + ] + password: Annotated[ + str, + Field( + description="password for openapi routes authentication", + ), + ] + + @classmethod + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Tuple[str, str]: + my_model = await cls.from_db(model_id) + + return my_model.username, my_model.password + + +OpenAPIAuthRef: TypeAlias = OpenAPIAuth.get_reference_model() # type: ignore[valid-type] + + +@dataclass +class FunctionInfo: + name: str + description: Optional[str] + function: Callable[..., Any] + + +@Registry.get_default().register("toolbox") +class Toolbox(Model): + openapi_url: Annotated[ + URL, + Field( + title="OpenAPI URL", + description="The URL of OpenAPI specification file", + ), + ] + openapi_auth: Annotated[ + Optional[OpenAPIAuthRef], + Field( + title="OpenAPI Auth", + description="Authentication information for the API mentioned in the OpenAPI specification", + ), + ] = None + + @classmethod + async def create_autogen(cls, model_id: UUID, user_id: UUID) -> List[FunctionInfo]: + my_model = await cls.from_db(model_id) + + # Download OpenAPI spec + with httpx.Client() as httpx_client: + response = httpx_client.get(my_model.openapi_url) # type: ignore[arg-type] + response.raise_for_status() + openapi_spec = response.text + + client = Client.create(openapi_spec) + + function_infos = [ + FunctionInfo( + name=f.__name__.strip(), + description=f.__doc__.strip() if f.__doc__ else None, + function=f, + ) + for f in client.registered_funcs + ] + + return function_infos + + +ToolboxRef: TypeAlias = Toolbox.get_reference_model() # type: ignore[valid-type] diff --git a/fastagency/openapi/client.py b/fastagency/openapi/client.py index 0dc3be42..800cc093 100644 --- a/fastagency/openapi/client.py +++ b/fastagency/openapi/client.py @@ -1,6 +1,7 @@ import importlib import inspect import re +import shutil import sys import tempfile from functools import wraps @@ -56,7 +57,7 @@ def _process_params( if hasattr(kwargs[body], "model_dump") else kwargs[body].dict() } - if body + if body and body in kwargs else {} ) body_dict["headers"] = {"Content-Type": "application/json"} @@ -69,7 +70,7 @@ def _request( self, method: Literal["put", "get", "post", "delete"], path: str, **kwargs: Any ) -> Callable[..., Dict[str, Any]]: def decorator(func: Callable[..., Any]) -> Callable[..., Dict[str, Any]]: - self.registered_funcs.append(func) + # self.registered_funcs.append(func) @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Dict[str, Any]: @@ -77,6 +78,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Dict[str, Any]: response = getattr(requests, method)(url, params=params, **body_dict) return response.json() # type: ignore [no-any-return] + self.registered_funcs.append(wrapper) + return wrapper return decorator # type: ignore [return-value] @@ -115,10 +118,29 @@ def create(cls, openapi_json: str, *, name: Optional[str] = None) -> "Client": output_dir=td, template_dir=cls._get_template_dir(), ) + # Use unique file name for main.py + main_name = f"main_{td.name}" + main_path = td / f"{main_name}.py" + shutil.move(td / "main.py", main_path) + + # Change "from models import" to "from models_unique_name import" + with open(main_path) as f: # noqa: PTH123 + main_py_code = f.read() + main_py_code = main_py_code.replace( + "from .models import", f"from models_{td.name} import" + ) + with open(main_path, "w") as f: # noqa: PTH123 + f.write(main_py_code) + + # Use unique file name for models.py + models_name = f"models_{td.name}" + models_path = td / f"{models_name}.py" + shutil.move(td / "models.py", models_path) + # add td to sys.path try: sys.path.append(str(td)) - main = importlib.import_module("main", package=td.name) + main = importlib.import_module(main_name, package=td.name) # nosemgrep finally: sys.path.remove(str(td)) diff --git a/fastagency/weather_app.py b/fastagency/weather_app.py new file mode 100644 index 00000000..613e9210 --- /dev/null +++ b/fastagency/weather_app.py @@ -0,0 +1,69 @@ +import datetime +import logging +from os import environ +from typing import List + +import python_weather +from fastapi import FastAPI +from pydantic import BaseModel + +logging.basicConfig(level=logging.INFO) + +host = environ.get("DOMAIN", "localhost") +port = 9000 + +weather_app = FastAPI( + servers=[{"url": f"http://{host}:{port}", "description": "Weather app server"}] +) + + +class HourlyForecast(BaseModel): + forecast_time: datetime.time + temperature: int + description: str + + +class DailyForecast(BaseModel): + forecast_date: datetime.date + temperature: int + hourly_forecasts: List[HourlyForecast] + + +class Weather(BaseModel): + city: str + temperature: int + daily_forecasts: List[DailyForecast] + + +@weather_app.get("/") +async def get_weather(city: str) -> Weather: + async with python_weather.Client(unit=python_weather.METRIC) as client: + # fetch a weather forecast from a city + weather = await client.get(city) + + daily_forecasts = [] + # get the weather forecast for a few days + for daily in weather.daily_forecasts: + hourly_forecasts = [ + HourlyForecast( + forecast_time=hourly.time, + temperature=hourly.temperature, + description=hourly.description, + ) + for hourly in daily.hourly_forecasts + ] + daily_forecasts.append( + DailyForecast( + forecast_date=daily.date, + temperature=daily.temperature, + hourly_forecasts=hourly_forecasts, + ) + ) + + weather_response = Weather( + city=city, + temperature=weather.temperature, + daily_forecasts=daily_forecasts, + hourly_forecasts=hourly_forecasts, + ) + return weather_response diff --git a/pyproject.toml b/pyproject.toml index 292b5318..4dab6a92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,8 @@ dependencies = [ "fastapi-code-generator==0.5.0", "asyncer==0.0.7", "markdownify==0.12.1", # Needed by autogen.WebSurferAgent but not included + "httpx==0.27.0", + "python-weather==2.0.3", ] [project.optional-dependencies] @@ -102,8 +104,8 @@ test-core = [ testing = [ "fastagency[test-core]", + "fastagency[server]", # Uvicorn is needed for testing "pydantic-settings==2.2.1", - "httpx==0.27.0", "PyYAML==6.0.1", "watchfiles==0.21.0", "email-validator==2.1.1", diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 254f4a00..b98e921b 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -3,4 +3,5 @@ set -e set -x +rm -rf docs/docs/en/api cd docs; python docs.py build diff --git a/scripts/run_server.sh b/scripts/run_server.sh index 492e292c..67ba2772 100755 --- a/scripts/run_server.sh +++ b/scripts/run_server.sh @@ -5,4 +5,6 @@ prisma generate --schema=schema.prisma --generator=pyclient faststream run fastagency.io.ionats:app --workers 2 > faststream.log 2>&1 & +uvicorn fastagency.weather_app:weather_app --workers 1 --host 0.0.0.0 --port 9000 > weather_app.log 2>&1 & + uvicorn fastagency.app:app --workers 2 --host 0.0.0.0 --proxy-headers diff --git a/templates/main.jinja2 b/templates/main.jinja2 index ddbe14b7..bad70610 100644 --- a/templates/main.jinja2 +++ b/templates/main.jinja2 @@ -1,9 +1,9 @@ +{{imports}} + from typing import * from fastagency.openapi.client import Client -from models import * - app = Client( {% if info %} {% for key,value in info.items() %} diff --git a/tests/app/test_get_schemas.py b/tests/app/test_get_schemas.py index c7d4aa7d..ba7f76f9 100644 --- a/tests/app/test_get_schemas.py +++ b/tests/app/test_get_schemas.py @@ -14,17 +14,25 @@ def test_return_all(self) -> None: schemas = Schemas(**response.json()) types = {schemas.name: schemas.schemas for schemas in schemas.list_of_schemas} - assert set(types.keys()) == {"secret", "llm", "agent", "team", "application"} + assert set(types.keys()) == { + "secret", + "llm", + "agent", + "team", + "toolbox", + "application", + } model_names = { type_name: {model.name for model in model_schema_list} for type_name, model_schema_list in types.items() } expected = { - "secret": {"AzureOAIAPIKey", "OpenAIAPIKey", "BingAPIKey"}, + "secret": {"AzureOAIAPIKey", "OpenAIAPIKey", "BingAPIKey", "OpenAPIAuth"}, "llm": {"AzureOAI", "OpenAI"}, "agent": {"AssistantAgent", "WebSurferAgent", "UserProxyAgent"}, "team": {"TwoAgentTeam", "MultiAgentTeam"}, + "toolbox": {"Toolbox"}, "application": {"Application"}, } - assert model_names == expected + assert model_names == expected, f"{model_names}!={expected}" diff --git a/tests/app/test_openai_extensively.py b/tests/app/test_openai_extensively.py index cd8757ad..4c46540e 100644 --- a/tests/app/test_openai_extensively.py +++ b/tests/app/test_openai_extensively.py @@ -3,11 +3,13 @@ from typing import Any, Dict import pytest +from fastapi import HTTPException from fastapi.testclient import TestClient -from fastagency.app import add_model, app +from fastagency.app import add_model, app, validate_toolbox from fastagency.models.llms.openai import OpenAI, OpenAIAPIKey from fastagency.models.registry import Schemas +from fastagency.models.toolboxes.toolbox import OpenAPIAuth, Toolbox client = TestClient(app) @@ -172,3 +174,164 @@ def test_get_schemas() -> None: schemas = Schemas(**response.json()) assert len(schemas.list_of_schemas) >= 2 + + +class TestToolbox: + @pytest.mark.db() + @pytest.mark.asyncio() + async def test_add_toolbox(self, user_uuid: str, fastapi_openapi_url: str) -> None: + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + response = client.post( + f"/user/{user_uuid}/models/secret/OpenAPIAuth/{openapi_auth_model_uuid}", + json=openapi_auth.model_dump(), + ) + assert response.status_code == 200 + + model_uuid = str(uuid.uuid4()) + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + toolbox_dump = toolbox.model_dump() + toolbox_dump["openapi_auth"]["uuid"] = str(toolbox_dump["openapi_auth"]["uuid"]) + + response = client.post( + f"/user/{user_uuid}/models/toolbox/Toolbox/{model_uuid}", + json=toolbox_dump, + ) + + assert response.status_code == 200 + expected = { + "name": "test_toolbox_constructor", + "openapi_url": fastapi_openapi_url, + "openapi_auth": { + "type": "secret", + "name": "OpenAPIAuth", + "uuid": str(openapi_auth_model_uuid), + }, + } + actual = response.json() + assert actual == expected + + @pytest.mark.asyncio() + async def test_validate_toolbox(self, fastapi_openapi_url: str) -> None: + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + await validate_toolbox(toolbox) + + @pytest.mark.asyncio() + async def test_validate_toolbox_route(self, fastapi_openapi_url: str) -> None: + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + toolbox_dump = toolbox.model_dump() + toolbox_dump["openapi_auth"]["uuid"] = str(toolbox_dump["openapi_auth"]["uuid"]) + + response = client.post( + "/models/toolbox/Toolbox/validate", + json=toolbox_dump, + ) + assert response.status_code == 200 + + @pytest.mark.asyncio() + async def test_validate_toolbox_with_404_url(self) -> None: + invalid_url = "http://i.dont.exist.airt.ai/openapi.json" + + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=invalid_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + with pytest.raises(HTTPException) as e: + await validate_toolbox(toolbox) + + assert e.value.status_code == 422 + assert e.value.detail == "OpenAPI URL is invalid" + + @pytest.mark.asyncio() + async def test_validate_toolbox_with_invalid_openapi_spec(self) -> None: + invalid_url = "http://echo.jsontest.com/key/value/one/two" + + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=invalid_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + with pytest.raises(HTTPException) as e: + await validate_toolbox(toolbox) + + assert e.value.status_code == 422 + assert e.value.detail == "OpenAPI URL does not contain a valid OpenAPI spec" + + @pytest.mark.asyncio() + async def test_validate_toolbox_with_yaml_openapi_spec(self) -> None: + invalid_url = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml" + + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=invalid_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + await validate_toolbox(toolbox) diff --git a/tests/conftest.py b/tests/conftest.py index 511c47a9..ebaefbb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,20 @@ +import contextlib import os import random +import socket +import threading +import time import uuid -from typing import Any, AsyncIterator, Dict +from platform import system +from typing import Any, AsyncIterator, Dict, Iterator, Optional +import httpx import openai import pytest import pytest_asyncio +import uvicorn +from fastapi import FastAPI +from pydantic import BaseModel from fastagency.db.helpers import get_db_connection, get_wasp_db_url @@ -66,3 +75,95 @@ def test_llm_config_fixture(llm_config: Dict[str, Any]) -> None: for k in ["model", "api_key", "base_url", "api_type", "api_version"]: assert len(llm_config["config_list"][0][k]) > 3 + + +# FastAPI app for testing + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + + +def create_fastapi_app(host: str, port: int) -> FastAPI: + app = FastAPI( + servers=[ + {"url": f"http://{host}:{port}", "description": "Local development server"} + ] + ) + + @app.get("/") + def read_root() -> Dict[str, str]: + return {"Hello": "World"} + + @app.get("/items/{item_id}") + def read_item(item_id: int, q: Optional[str] = None) -> Dict[str, Any]: + return {"item_id": item_id, "q": q} + + @app.post("/items") + async def create_item(item: Item) -> Item: + return item + + return app + + +def find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] # type: ignore [no-any-return] + + +def test_find_free_port() -> None: + port = find_free_port() + assert isinstance(port, int) + assert 1024 <= port <= 65535 + + +def run_server(app: FastAPI, host: str = "127.0.0.1", port: int = 8000) -> None: + uvicorn.run(app, host=host, port=port) + + +class Server(uvicorn.Server): # type: ignore [misc] + def install_signal_handlers(self) -> None: + pass + + @contextlib.contextmanager + def run_in_thread(self) -> Iterator[None]: + thread = threading.Thread(target=self.run) + thread.start() + try: + while not self.started: + time.sleep(1e-3) + yield + finally: + self.should_exit = True + thread.join() + + +@pytest.fixture(scope="session") +def fastapi_openapi_url() -> Iterator[str]: + host = "127.0.0.1" + port = find_free_port() + app = create_fastapi_app(host, port) + openapi_url = f"http://{host}:{port}/openapi.json" + + config = uvicorn.Config(app, host=host, port=port, log_level="info") + server = Server(config=config) + with server.run_in_thread(): + time.sleep(1 if system() != "Windows" else 5) # let the server start + + yield openapi_url + + +def test_fastapi_openapi(fastapi_openapi_url: str) -> None: + assert isinstance(fastapi_openapi_url, str) + + resp = httpx.get(fastapi_openapi_url) + assert resp.status_code == 200 + resp_json = resp.json() + assert "openapi" in resp_json + assert "servers" in resp_json + assert len(resp_json["servers"]) == 1 + assert resp_json["info"]["title"] == "FastAPI" diff --git a/tests/models/agents/test_assistant.py b/tests/models/agents/test_assistant.py index 86c1f55b..ce8a92c1 100644 --- a/tests/models/agents/test_assistant.py +++ b/tests/models/agents/test_assistant.py @@ -4,7 +4,6 @@ import autogen import pytest -from asyncer import asyncify from pydantic import ValidationError from fastagency.app import add_model @@ -12,6 +11,7 @@ from fastagency.models.base import Model from fastagency.models.llms.azure import AzureOAI, AzureOAIAPIKey from fastagency.models.llms.openai import OpenAI +from fastagency.models.toolboxes.toolbox import OpenAPIAuth, Toolbox class TestAssistantAgent: @@ -94,10 +94,39 @@ def test_assistant_model_schema(self) -> None: "title": "OpenAIRef", "type": "object", }, + "ToolboxRef": { + "properties": { + "type": { + "const": "toolbox", + "default": "toolbox", + "description": "The name of the type of the data", + "enum": ["toolbox"], + "title": "Type", + "type": "string", + }, + "name": { + "const": "Toolbox", + "default": "Toolbox", + "description": "The name of the data", + "enum": ["Toolbox"], + "title": "Name", + "type": "string", + }, + "uuid": { + "description": "The unique identifier", + "format": "uuid", + "title": "UUID", + "type": "string", + }, + }, + "required": ["uuid"], + "title": "ToolboxRef", + "type": "object", + }, }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -110,6 +139,24 @@ def test_assistant_model_schema(self) -> None: "description": "LLM used by the agent for producing responses", "title": "LLM", }, + "toolbox_1": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, + "toolbox_2": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, + "toolbox_3": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, "system_message": { "description": "The system message of the agent. This message is used to inform the agent about his role in the conversation", "title": "System Message", @@ -151,6 +198,7 @@ async def test_assistant_model_create_autogen( api_key_model: Model, llm_config: Dict[str, Any], user_uuid: str, + fastapi_openapi_url: str, monkeypatch: pytest.MonkeyPatch, ) -> None: # Add secret, llm, agent to database @@ -183,10 +231,45 @@ async def test_assistant_model_create_autogen( model=llm.model_dump(), ) + # add toolbox to database + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + await add_model( + user_uuid=user_uuid, + type_name="secret", + model_name=OpenAPIAuth.__name__, # type: ignore [attr-defined] + model_uuid=openapi_auth_model_uuid, + model=openapi_auth.model_dump(), + ) + + toolbox_uuid = str(uuid.uuid4()) + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + await add_model( + user_uuid=user_uuid, + type_name="toolbox", + model_name=Toolbox.__name__, # type: ignore [attr-defined] + model_uuid=toolbox_uuid, + model=toolbox.model_dump(), + ) + + # add agent to database weatherman_assistant_model = AssistantAgent( llm=llm.get_reference_model()(uuid=llm_model_uuid), name="Assistant", system_message="test system message", + toolbox_1=toolbox.get_reference_model()(uuid=toolbox_uuid), ) weatherman_assistant_model_uuid = str(uuid.uuid4()) await add_model( @@ -197,12 +280,16 @@ async def test_assistant_model_create_autogen( model=weatherman_assistant_model.model_dump(), ) + async def my_create_autogen(cls, model_id, user_id) -> Dict[str, Any]: # type: ignore [no-untyped-def] + return llm_config + # Monkeypatch llm and call create_autogen - monkeypatch.setattr( - AzureOAI, "create_autogen", lambda cls, model_id, user_id: llm_config - ) - agent = await asyncify(AssistantAgent.create_autogen)( + monkeypatch.setattr(AzureOAI, "create_autogen", my_create_autogen) + + agent, functions = await AssistantAgent.create_autogen( model_id=uuid.UUID(weatherman_assistant_model_uuid), user_id=uuid.UUID(user_uuid), ) assert isinstance(agent, autogen.agentchat.AssistantAgent) + assert isinstance(functions, list) + assert len(functions) == 3 diff --git a/tests/models/agents/test_user_proxy.py b/tests/models/agents/test_user_proxy.py index b0b9edca..27fbaf50 100644 --- a/tests/models/agents/test_user_proxy.py +++ b/tests/models/agents/test_user_proxy.py @@ -1,62 +1,21 @@ -import os import uuid -from typing import Any, Dict import autogen import pytest -from asyncer import asyncify from fastagency.app import add_model from fastagency.models.agents.user_proxy import UserProxyAgent -from fastagency.models.base import Model -from fastagency.models.llms.azure import AzureOAI, AzureOAIAPIKey class TestUserProxyAgent: @pytest.mark.asyncio() @pytest.mark.db() - @pytest.mark.parametrize("llm_model,api_key_model", [(AzureOAI, AzureOAIAPIKey)]) # noqa: PT006 async def test_user_proxy_model_create_autogen( self, - llm_model: Model, - api_key_model: Model, - llm_config: Dict[str, Any], user_uuid: str, - monkeypatch: pytest.MonkeyPatch, ) -> None: - # Add secret, llm, agent to database - api_key = api_key_model( # type: ignore [operator] - api_key=os.getenv("AZURE_OPENAI_API_KEY"), - name="api_key_model_name", - ) - api_key_model_uuid = str(uuid.uuid4()) - await add_model( - user_uuid=user_uuid, - type_name="secret", - model_name=api_key_model.__name__, # type: ignore [attr-defined] - model_uuid=api_key_model_uuid, - model=api_key.model_dump(), - ) - - llm = llm_model( # type: ignore [operator] - name="llm_model_name", - model=os.getenv("AZURE_GPT35_MODEL"), - api_key=api_key.get_reference_model()(uuid=api_key_model_uuid), - base_url=os.getenv("AZURE_API_ENDPOINT"), - api_version=os.getenv("AZURE_API_VERSION"), - ) - llm_model_uuid = str(uuid.uuid4()) - await add_model( - user_uuid=user_uuid, - type_name="llm", - model_name=llm_model.__name__, # type: ignore [attr-defined] - model_uuid=llm_model_uuid, - model=llm.model_dump(), - ) - user_proxy_model = UserProxyAgent( - llm=llm.get_reference_model()(uuid=llm_model_uuid), - name="Assistant", + name="User proxy", system_message="test system message", ) user_proxy_model_uuid = str(uuid.uuid4()) @@ -68,12 +27,9 @@ async def test_user_proxy_model_create_autogen( model=user_proxy_model.model_dump(), ) - # Monkeypatch llm and call create_autogen - monkeypatch.setattr( - AzureOAI, "create_autogen", lambda cls, model_id, user_id: llm_config - ) - agent = await asyncify(UserProxyAgent.create_autogen)( + agent, functions = await UserProxyAgent.create_autogen( model_id=uuid.UUID(user_proxy_model_uuid), user_id=uuid.UUID(user_uuid), ) assert isinstance(agent, autogen.agentchat.UserProxyAgent) + assert functions == [] diff --git a/tests/models/agents/test_web_surfer.py b/tests/models/agents/test_web_surfer.py index 64acc18f..d960316b 100644 --- a/tests/models/agents/test_web_surfer.py +++ b/tests/models/agents/test_web_surfer.py @@ -4,7 +4,6 @@ import autogen.agentchat.contrib.web_surfer import pytest -from asyncer import asyncify from pydantic import ValidationError from fastagency.app import add_model @@ -125,10 +124,39 @@ def test_web_surfer_model_schema(self) -> None: "title": "OpenAIRef", "type": "object", }, + "ToolboxRef": { + "properties": { + "type": { + "const": "toolbox", + "default": "toolbox", + "description": "The name of the type of the data", + "enum": ["toolbox"], + "title": "Type", + "type": "string", + }, + "name": { + "const": "Toolbox", + "default": "Toolbox", + "description": "The name of the data", + "enum": ["Toolbox"], + "title": "Name", + "type": "string", + }, + "uuid": { + "description": "The unique identifier", + "format": "uuid", + "title": "UUID", + "type": "string", + }, + }, + "required": ["uuid"], + "title": "ToolboxRef", + "type": "object", + }, }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -141,6 +169,24 @@ def test_web_surfer_model_schema(self) -> None: "description": "LLM used by the agent for producing responses", "title": "LLM", }, + "toolbox_1": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, + "toolbox_2": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, + "toolbox_3": { + "anyOf": [{"$ref": "#/$defs/ToolboxRef"}, {"type": "null"}], + "default": None, + "description": "Toolbox used by the agent for producing responses", + "title": "Toolbox", + }, "summarizer_llm": { "anyOf": [ {"$ref": "#/$defs/AzureOAIRef"}, @@ -245,15 +291,18 @@ async def test_web_surfer_model_create_autogen( model=web_surfer_model.model_dump(), ) + async def my_create_autogen(cls, model_id, user_id) -> Dict[str, Any]: # type: ignore [no-untyped-def] + return llm_config + # Monkeypatch llm and call create_autogen - monkeypatch.setattr( - AzureOAI, "create_autogen", lambda cls, model_id, user_id: llm_config - ) - agent = await asyncify(WebSurferAgent.create_autogen)( + monkeypatch.setattr(AzureOAI, "create_autogen", my_create_autogen) + + agent, functions = await WebSurferAgent.create_autogen( model_id=uuid.UUID(web_surfer_model_uuid), user_id=uuid.UUID(user_uuid), ) assert isinstance(agent, autogen.agentchat.contrib.web_surfer.WebSurferAgent) + assert functions == [] class TestBingAPIKey: @@ -280,7 +329,7 @@ async def test_bing_api_key_model_create_autogen( ) # Call create_autogen - actual_api_key = await asyncify(AzureOAIAPIKey.create_autogen)( + actual_api_key = await AzureOAIAPIKey.create_autogen( model_id=uuid.UUID(api_key_model_uuid), user_id=uuid.UUID(user_uuid), ) diff --git a/tests/models/applications/test_application.py b/tests/models/applications/test_application.py index 6d9be25a..5222ebeb 100644 --- a/tests/models/applications/test_application.py +++ b/tests/models/applications/test_application.py @@ -91,7 +91,7 @@ def test_application_model_schema(self) -> None: }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", diff --git a/tests/models/llms/test_azure.py b/tests/models/llms/test_azure.py index 931dad62..bdc030f8 100644 --- a/tests/models/llms/test_azure.py +++ b/tests/models/llms/test_azure.py @@ -3,7 +3,6 @@ from typing import Any, Dict import pytest -from asyncer import asyncify from fastagency.app import add_model from fastagency.models.base import Model @@ -75,7 +74,7 @@ def test_azure_model_schema(self) -> None: }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -159,13 +158,13 @@ async def test_azure_model_create_autogen( model=llm.model_dump(), ) + async def my_create_autogen(cls, model_id, user_id) -> Any: # type: ignore [no-untyped-def] + return api_key.api_key + # Monkeypatch api_key and call create_autogen - monkeypatch.setattr( - AzureOAIAPIKey, - "create_autogen", - lambda cls, model_id, user_id: api_key.api_key, - ) - actual_llm_config = await asyncify(AzureOAI.create_autogen)( + monkeypatch.setattr(AzureOAIAPIKey, "create_autogen", my_create_autogen) + + actual_llm_config = await AzureOAI.create_autogen( model_id=uuid.UUID(llm_model_uuid), user_id=uuid.UUID(user_uuid), ) @@ -199,7 +198,7 @@ async def test_azure_api_key_model_create_autogen( ) # Call create_autogen - actual_api_key = await asyncify(AzureOAIAPIKey.create_autogen)( + actual_api_key = await AzureOAIAPIKey.create_autogen( model_id=uuid.UUID(api_key_model_uuid), user_id=uuid.UUID(user_uuid), ) diff --git a/tests/models/llms/test_openai.py b/tests/models/llms/test_openai.py index a315b9e7..58b978ff 100644 --- a/tests/models/llms/test_openai.py +++ b/tests/models/llms/test_openai.py @@ -2,7 +2,6 @@ from typing import Any, Dict import pytest -from asyncer import asyncify from pydantic_core import Url from fastagency.app import add_model @@ -70,7 +69,7 @@ def test_openai_schema(self) -> None: }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -149,12 +148,12 @@ async def test_openai_model_create_autogen( ) # Monkeypatch api_key and call create_autogen - monkeypatch.setattr( - OpenAIAPIKey, - "create_autogen", - lambda cls, model_id, user_id: api_key.api_key, - ) - actual_llm_config = await asyncify(OpenAI.create_autogen)( + async def my_create_autogen(cls, model_id, user_id) -> Any: # type: ignore [no-untyped-def] + return api_key.api_key + + monkeypatch.setattr(OpenAIAPIKey, "create_autogen", my_create_autogen) + + actual_llm_config = await OpenAI.create_autogen( model_id=uuid.UUID(llm_model_uuid), user_id=uuid.UUID(user_uuid), ) @@ -219,7 +218,7 @@ async def test_openai_api_key_model_create_autogen( ) # Call create_autogen - actual_api_key = await asyncify(OpenAIAPIKey.create_autogen)( + actual_api_key = await OpenAIAPIKey.create_autogen( model_id=uuid.UUID(api_key_model_uuid), user_id=uuid.UUID(user_uuid), ) diff --git a/tests/models/teams/test_base.py b/tests/models/teams/test_base.py new file mode 100644 index 00000000..4f1fc344 --- /dev/null +++ b/tests/models/teams/test_base.py @@ -0,0 +1,115 @@ +from typing import Annotated, Any, Dict, List, Optional + +import pytest +from autogen.agentchat import AssistantAgent, UserProxyAgent + +from fastagency.models.teams.base import register_toolbox_functions +from fastagency.models.toolboxes.toolbox import FunctionInfo + + +class TestRegisterToolboxFunctions: + @pytest.fixture() + def function_infos(self) -> List[FunctionInfo]: + def f(i: int, s: str) -> str: + return str(i) + s + + def g(i: int, s: str, f: Optional[float]) -> str: + return str(i) + s + + def h(name: Annotated[str, "Name of the person"]) -> str: + return name + + return [ + FunctionInfo(name="f", description="Function f", function=f), + FunctionInfo(name="g", description="Function g", function=g), + FunctionInfo(name="h", description="Function h", function=h), + ] + + def llm_config(self) -> Dict[str, Any]: + dummy_openai_api_key = "sk-sUeBP9asw6GiYHXqtg70T3BlbkFJJuLwJFco90bOpU0Ntest" # pragma: allowlist secret + + return { + "config_list": [ + { + "model": "gpt-3.5-turbo", + "api_key": dummy_openai_api_key, + "base_url": "https://api.openai.com/v1", + "api_type": "openai", + } + ], + "temperature": 0, + } + + def test_register_toolbox_functions( + self, function_infos: List[FunctionInfo], llm_config: Dict[str, Any] + ) -> None: + agent = AssistantAgent(name="agent 007", llm_config=llm_config) + execution_agents = [ + AssistantAgent(name="agent 008", llm_config=llm_config), + UserProxyAgent(name="agent 009"), + ] + + register_toolbox_functions(agent, execution_agents, function_infos) + + expected = [ + { + "type": "function", + "function": { + "description": "Function f", + "name": "f", + "parameters": { + "type": "object", + "properties": { + "i": {"type": "integer", "description": "i"}, + "s": {"type": "string", "description": "s"}, + }, + "required": ["i", "s"], + }, + }, + }, + { + "type": "function", + "function": { + "description": "Function g", + "name": "g", + "parameters": { + "type": "object", + "properties": { + "i": {"type": "integer", "description": "i"}, + "s": {"type": "string", "description": "s"}, + "f": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "description": "f", + }, + }, + "required": ["i", "s", "f"], + }, + }, + }, + { + "type": "function", + "function": { + "description": "Function h", + "name": "h", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the person", + } + }, + "required": ["name"], + }, + }, + }, + ] + assert agent.llm_config["tools"] == expected + + expected_2 = { + function_info.name: function_info.function + for function_info in function_infos + } + for execution_agent in execution_agents: + actual = {k: f._origin for k, f in execution_agent._function_map.items()} + assert actual == expected_2 diff --git a/tests/models/teams/test_multi_agents_team.py b/tests/models/teams/test_multi_agents_team.py index dbdb0c82..ae848964 100644 --- a/tests/models/teams/test_multi_agents_team.py +++ b/tests/models/teams/test_multi_agents_team.py @@ -6,7 +6,6 @@ import autogen import pytest -from asyncer import asyncify from autogen.io.console import IOConsole from pydantic import ValidationError @@ -18,6 +17,7 @@ from fastagency.models.llms.azure import AzureOAI, AzureOAIAPIKey from fastagency.models.llms.openai import OpenAI from fastagency.models.teams.multi_agent_team import MultiAgentTeam +from fastagency.models.toolboxes.toolbox import FunctionInfo class TestMultiAgentTeam: @@ -153,7 +153,7 @@ def test_multi_agent_model_schema(self) -> None: }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -376,28 +376,39 @@ async def test_multi_agent_team_autogen( get_forecast_for_city_mock = MagicMock() - @user_proxy_agent.register_for_execution() # type: ignore [misc] - @weatherman_agent_1.register_for_llm( - description="Get weather forecast for a city" - ) # type: ignore [misc] + # @user_proxy_agent.register_for_execution() # type: ignore [misc] + # @weatherman_agent_1.register_for_llm( + # description="Get weather forecast for a city" + # ) # type: ignore [misc] def get_forecast_for_city(city: str) -> str: get_forecast_for_city_mock(city) return f"The weather in {city} is sunny today." + async def weatherman_create_autogen( # type: ignore [no-untyped-def] + cls, model_id, user_id + ) -> autogen.agentchat.AssistantAgent: + f_info = FunctionInfo( + function=get_forecast_for_city, + description="Get weather forecast for a city", + name="get_forecast_for_city", + ) + return weatherman_agent_1, [f_info] + + async def user_proxy_create_autogen( # type: ignore [no-untyped-def] + cls, model_id, user_id + ) -> autogen.agentchat.UserProxyAgent: + return user_proxy_agent, [] + if enable_monkeypatch: monkeypatch.setattr( - AssistantAgent, - "create_autogen", - lambda cls, model_id, user_id: weatherman_agent_1, + AssistantAgent, "create_autogen", weatherman_create_autogen ) monkeypatch.setattr( - UserProxyAgent, - "create_autogen", - lambda cls, model_id, user_id: user_proxy_agent, + UserProxyAgent, "create_autogen", user_proxy_create_autogen ) - team = await asyncify(MultiAgentTeam.create_autogen)( + team = await MultiAgentTeam.create_autogen( model_id=uuid.UUID(team_model_uuid), user_id=uuid.UUID(user_uuid) ) @@ -423,8 +434,8 @@ def input(prompt: str, d: Dict[str, int] = d) -> str: last_message = chat_result.chat_history[-1] if enable_monkeypatch: - # get_forecast_for_city_mock.assert_called_once_with("New York") - get_forecast_for_city_mock.assert_not_called() + get_forecast_for_city_mock.assert_called_once_with("New York") + # get_forecast_for_city_mock.assert_not_called() assert "sunny" in last_message["content"] else: # assert "sunny" not in last_message["content"] diff --git a/tests/models/teams/test_two_agents_team.py b/tests/models/teams/test_two_agents_team.py index 1f196e5d..76ed60f7 100644 --- a/tests/models/teams/test_two_agents_team.py +++ b/tests/models/teams/test_two_agents_team.py @@ -6,7 +6,6 @@ import autogen import pytest -from asyncer import asyncify from autogen.io.console import IOConsole from pydantic import ValidationError @@ -18,6 +17,7 @@ from fastagency.models.llms.azure import AzureOAI, AzureOAIAPIKey from fastagency.models.llms.openai import OpenAI from fastagency.models.teams.two_agent_teams import TwoAgentTeam +from fastagency.models.toolboxes.toolbox import FunctionInfo class TestTwoAgentTeam: @@ -147,7 +147,7 @@ def test_two_agent_model_schema(self) -> None: }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -332,27 +332,34 @@ async def test_two_agent_team_autogen( get_forecast_for_city_mock = MagicMock() - @user_proxy_agent.register_for_execution() # type: ignore [misc] - @weatherman_agent.register_for_llm( - description="Get weather forecast for a city" - ) # type: ignore [misc] - def get_forecast_for_city(city: str) -> str: - get_forecast_for_city_mock(city) - return f"The weather in {city} is sunny today." + async def weatherman_create_autogen( + cls: Model, model_id: uuid.UUID, user_id: uuid.UUID + ) -> autogen.agentchat.AssistantAgent: + def get_forecast_for_city(city: str) -> str: + get_forecast_for_city_mock(city) + return f"The weather in {city} is sunny today." + + f_info = FunctionInfo( + name="get_forecast_for_city", + description="Get weather forecast for a city", + function=get_forecast_for_city, + ) + return weatherman_agent, [f_info] + + async def user_proxy_create_autogen( + cls: Model, model_id: uuid.UUID, user_id: uuid.UUID + ) -> autogen.agentchat.UserProxyAgent: + return user_proxy_agent, [] if enable_monkeypatch: monkeypatch.setattr( - AssistantAgent, - "create_autogen", - lambda cls, model_id, user_id: weatherman_agent, + AssistantAgent, "create_autogen", weatherman_create_autogen ) monkeypatch.setattr( - UserProxyAgent, - "create_autogen", - lambda cls, model_id, user_id: user_proxy_agent, + UserProxyAgent, "create_autogen", user_proxy_create_autogen ) - team = await asyncify(TwoAgentTeam.create_autogen)( + team = await TwoAgentTeam.create_autogen( model_id=uuid.UUID(team_model_uuid), user_id=uuid.UUID(user_uuid) ) @@ -378,8 +385,7 @@ def input(prompt: str, d: Dict[str, int] = d) -> str: last_message = chat_result.chat_history[-1] if enable_monkeypatch: - # get_forecast_for_city_mock.assert_called_once_with("New York") - get_forecast_for_city_mock.assert_not_called() + get_forecast_for_city_mock.assert_called_once_with("New York") assert "sunny" in last_message["content"] else: # assert "sunny" not in last_message["content"] diff --git a/tests/models/test_registry.py b/tests/models/test_registry.py index 61603c8e..7fff6fe4 100644 --- a/tests/models/test_registry.py +++ b/tests/models/test_registry.py @@ -107,7 +107,7 @@ class MyModel(Model): json_schema={ "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", @@ -174,7 +174,7 @@ class MyModel(Model): }, "properties": { "name": { - "description": "The name of the model", + "description": "The name of the item", "minLength": 1, "title": "Name", "type": "string", diff --git a/tests/models/toolboxes/__init__.py b/tests/models/toolboxes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/toolboxes/test_toolbox.py b/tests/models/toolboxes/test_toolbox.py new file mode 100644 index 00000000..5b957d6d --- /dev/null +++ b/tests/models/toolboxes/test_toolbox.py @@ -0,0 +1,144 @@ +import uuid +from typing import Optional + +import pytest +from pydantic import BaseModel + +from fastagency.app import add_model +from fastagency.models.toolboxes.toolbox import OpenAPIAuth, OpenAPIAuthRef, Toolbox + + +class TestOpenAPIAuth: + def test_openapi_auth(self) -> None: + model = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + + expected = { + "name": "openapi_auth_secret", + "username": "test", + "password": "password", # pragma: allowlist secret + } + assert model.model_dump() == expected + + @pytest.mark.db() + @pytest.mark.asyncio() + async def test_azure_api_key_model_create_autogen( + self, + user_uuid: str, + ) -> None: + # Add secret to database + openapi_auth = OpenAPIAuth( # type: ignore [operator] + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + model_uuid = str(uuid.uuid4()) + await add_model( + user_uuid=user_uuid, + type_name="secret", + model_name=OpenAPIAuth.__name__, # type: ignore [attr-defined] + model_uuid=model_uuid, + model=openapi_auth.model_dump(), + ) + + # Call create_autogen + actual = await OpenAPIAuth.create_autogen( + model_id=uuid.UUID(model_uuid), + user_id=uuid.UUID(user_uuid), + ) + + expected = ("test", "password") + assert actual == expected + + +class TestToolbox: + def test_toolbox_constructor(self, fastapi_openapi_url: str) -> None: + auth_uuid = uuid.uuid4() + openapi_auth_ref = OpenAPIAuthRef(uuid=auth_uuid) + + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth_ref, + ) + + assert toolbox + assert toolbox.name == "test_toolbox_constructor" + assert str(toolbox.openapi_url) == fastapi_openapi_url + assert toolbox.openapi_auth.uuid == auth_uuid # type: ignore[union-attr] + + @pytest.mark.db() + @pytest.mark.asyncio() + async def test_toolbox_create_autogen( + self, + user_uuid: str, + fastapi_openapi_url: str, + ) -> None: + openapi_auth = OpenAPIAuth( + name="openapi_auth_secret", + username="test", + password="password", # pragma: allowlist secret + ) + openapi_auth_model_uuid = str(uuid.uuid4()) + + await add_model( + user_uuid=user_uuid, + type_name="secret", + model_name=OpenAPIAuth.__name__, # type: ignore [attr-defined] + model_uuid=openapi_auth_model_uuid, + model=openapi_auth.model_dump(), + ) + + model_uuid = str(uuid.uuid4()) + toolbox = Toolbox( + name="test_toolbox_constructor", + openapi_url=fastapi_openapi_url, + openapi_auth=openapi_auth.get_reference_model()( + uuid=openapi_auth_model_uuid + ), + ) + + await add_model( + user_uuid=user_uuid, + type_name="toolbox", + model_name=Toolbox.__name__, # type: ignore [attr-defined] + model_uuid=model_uuid, + model=toolbox.model_dump(), + ) + + function_infos = await Toolbox.create_autogen( + model_id=uuid.UUID(model_uuid), + user_id=uuid.UUID(user_uuid), + ) + + assert len(function_infos) == 3, len(function_infos) + + expected = { + "create_item_items_post": "Create Item", + "read_item_items__item_id__get": "Read Item", + "read_root__get": "Read Root", + } + actual = {x.name: x.description for x in function_infos} + + assert actual == expected, actual + + actual = function_infos[0].function() + expected = {"Hello": "World"} + assert actual == expected, actual + + actual = function_infos[2].function(item_id=1, q="test") + expected = {"item_id": 1, "q": "test"} # type: ignore[dict-item] + assert actual == expected, actual + + class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + + actual = function_infos[1].function(body=Item(name="item", price=1.0)) + expected = {"name": "item", "description": None, "price": 1.0, "tax": None} # type: ignore[dict-item] + assert actual == expected, actual diff --git a/tests/openapi/templates/openapi2.json b/tests/openapi/templates/openapi2.json new file mode 100644 index 00000000..f7d40c37 --- /dev/null +++ b/tests/openapi/templates/openapi2.json @@ -0,0 +1,189 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/tests/openapi/templates/test_fastapi_codegen_template.py b/tests/openapi/templates/test_fastapi_codegen_template.py index 1a27b9c2..bc22855a 100644 --- a/tests/openapi/templates/test_fastapi_codegen_template.py +++ b/tests/openapi/templates/test_fastapi_codegen_template.py @@ -39,10 +39,17 @@ def test_fastapi_codegen_template(monkeypatch: MonkeyPatch) -> None: template_dir=TEMPLATE_DIR, ) + main_path = td / "main.py" + with open(main_path) as f: # noqa: PTH123 + main_py_code = f.read() + main_py_code = main_py_code.replace("from .models import", "from models import") + with open(main_path, "w") as f: # noqa: PTH123 + f.write(main_py_code) + # add td to sys.path try: sys.path.append(str(td)) - main = importlib.import_module("main", package=td.name) + main = importlib.import_module("main", package=td.name) # nosemgrep finally: sys.path.remove(str(td)) diff --git a/tests/openapi/test_client.py b/tests/openapi/test_client.py index 5d9c000b..93c37d05 100644 --- a/tests/openapi/test_client.py +++ b/tests/openapi/test_client.py @@ -14,7 +14,7 @@ def test_create_client(self) -> None: assert client is not None assert isinstance(client, Client) - assert len(client.registered_funcs) == 1 + assert len(client.registered_funcs) == 1, client.registered_funcs assert ( client.registered_funcs[0].__name__ == "update_item_items__item_id__ships__ship__put" @@ -25,3 +25,22 @@ def test_create_client(self) -> None: Update Item """ ) + + json2_path = Path(__file__).parent / "templates" / "openapi2.json" + assert json2_path.exists(), json2_path.resolve() + + openapi2_json = json2_path.read_text() + client2 = Client.create(openapi2_json) + + assert client2 is not None + assert isinstance(client2, Client) + + assert len(client2.registered_funcs) == 3, client2.registered_funcs + + actual = [x.__name__ for x in client2.registered_funcs] + expected = [ + "list_pets", + "create_pets", + "show_pet_by_id", + ] + assert actual == expected, actual diff --git a/tests/test_nats.py b/tests/test_nats.py index ca92e98a..028c6a96 100644 --- a/tests/test_nats.py +++ b/tests/test_nats.py @@ -86,7 +86,7 @@ def get_forecast_for_city(city: str) -> str: @pytest.mark.azure_oai() @pytest.mark.nats() @pytest.mark.asyncio() - async def test_ionats( # noqa: C901 + async def test_ionats_success( # noqa: C901 self, llm_config: Dict[str, Any], monkeypatch: pytest.MonkeyPatch ) -> None: user_id = uuid.uuid4() @@ -128,7 +128,7 @@ async def client_input_handler(msg: ServerResponseModel) -> None: get_forecast_for_city_mock = MagicMock() - def create_team( + async def create_team( team_id: uuid.UUID, user_id: uuid.UUID ) -> Callable[[str], List[Dict[str, Any]]]: weather_man = autogen.agentchat.AssistantAgent( @@ -272,7 +272,7 @@ async def client_input_handler(msg: ServerResponseModel) -> None: ### end sending inputs to server - def create_team( + async def create_team( team_id: uuid.UUID, user_id: uuid.UUID ) -> Callable[[str], List[Dict[str, Any]]]: raise ValueError("Triggering error in test") @@ -314,8 +314,8 @@ async def test_ionats_e2e( user_uuid: str, llm_model: Model, api_key_model: Model, - llm_config: Dict[str, Any], - monkeypatch: pytest.MonkeyPatch, + # llm_config: Dict[str, Any], + # monkeypatch: pytest.MonkeyPatch, ) -> None: thread_id = uuid.uuid4() diff --git a/tests/test_weather_app.py b/tests/test_weather_app.py new file mode 100644 index 00000000..81ac3672 --- /dev/null +++ b/tests/test_weather_app.py @@ -0,0 +1,32 @@ +import datetime + +from fastapi.testclient import TestClient + +from fastagency.weather_app import weather_app + +client = TestClient(weather_app) + + +def test_weather_route() -> None: + response = client.get("/?city=Chennai") + assert response.status_code == 200 + resp_json = response.json() + assert resp_json.get("city") == "Chennai" + assert resp_json.get("temperature") > 0 + + assert len(resp_json.get("daily_forecasts")) > 0 + daily_forecasts = resp_json.get("daily_forecasts") + assert isinstance(daily_forecasts, list) + + first_daily_forecast = daily_forecasts[0] + assert ( + first_daily_forecast.get("forecast_date") == datetime.date.today().isoformat() + ) + assert first_daily_forecast.get("temperature") > 0 + assert len(first_daily_forecast.get("hourly_forecasts")) > 0 + + first_hourly_forecast = first_daily_forecast.get("hourly_forecasts")[0] + assert isinstance(first_hourly_forecast, dict) + assert first_hourly_forecast.get("forecast_time") is not None + assert first_hourly_forecast.get("temperature") > 0 # type: ignore + assert first_hourly_forecast.get("description") is not None From 59722635060381bd0af8108b3765b1b048a05e83 Mon Sep 17 00:00:00 2001 From: Kumaran Rajendhiran Date: Wed, 5 Jun 2024 10:52:10 +0530 Subject: [PATCH 3/7] Update non ssl port to 9001 and prepare for https (#287) * Update non ssl port to 9001 and prepare for https * Add --proxy-headers flag --- docker-compose.yaml | 2 +- fastagency/weather_app.py | 2 +- scripts/run_server.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index c0695c02..2f44cf56 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,7 +18,7 @@ services: container_name: ${container_name} ports: - "8000:8000" - - "9000:9000" + - "9001:9001" environment: - DOMAIN=${DOMAIN} - DATABASE_URL=${DATABASE_URL} diff --git a/fastagency/weather_app.py b/fastagency/weather_app.py index 613e9210..1c8b9117 100644 --- a/fastagency/weather_app.py +++ b/fastagency/weather_app.py @@ -13,7 +13,7 @@ port = 9000 weather_app = FastAPI( - servers=[{"url": f"http://{host}:{port}", "description": "Weather app server"}] + servers=[{"url": f"https://{host}:{port}", "description": "Weather app server"}] ) diff --git a/scripts/run_server.sh b/scripts/run_server.sh index 67ba2772..d31b347a 100755 --- a/scripts/run_server.sh +++ b/scripts/run_server.sh @@ -5,6 +5,6 @@ prisma generate --schema=schema.prisma --generator=pyclient faststream run fastagency.io.ionats:app --workers 2 > faststream.log 2>&1 & -uvicorn fastagency.weather_app:weather_app --workers 1 --host 0.0.0.0 --port 9000 > weather_app.log 2>&1 & +uvicorn fastagency.weather_app:weather_app --workers 1 --host 0.0.0.0 --port 9001 --proxy-headers > weather_app.log 2>&1 & uvicorn fastagency.app:app --workers 2 --host 0.0.0.0 --proxy-headers From 1d84760dc3e25718677f22c78fd8cc04f24deba7 Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Wed, 5 Jun 2024 11:13:25 +0530 Subject: [PATCH 4/7] Update deploy instructions (#288) --- app/src/client/components/DynamicFormBuilder.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/client/components/DynamicFormBuilder.tsx b/app/src/client/components/DynamicFormBuilder.tsx index bb6fbee5..ca5e2737 100644 --- a/app/src/client/components/DynamicFormBuilder.tsx +++ b/app/src/client/components/DynamicFormBuilder.tsx @@ -198,12 +198,12 @@ Before you begin, ensure you have the following: 5.6 The hostname is the URL of your application. Open the URL in your browser to launch your application. Application customization (Optional): - You can perform basic customization such as changing the app name and adding a support email address in the generated application by - setting the below optional environment variables. + setting the below optional "repository variables" (not repository secrets). Click here to learn how to set repository variables. ================================================================================== REACT_APP_NAME: <--- Your App Name ---> REACT_APP_SUPPORT_EMAIL: <--- Your Support Email Address ---> ================================================================================== -- After setting the environment variables, you can manualy trigger the Fly Deployment Pipeline workflow (refer 4.2 and 4.3) to deploy the changes. +- After setting the repository variables, you can manualy trigger the Fly Deployment Pipeline workflow (refer 4.2 and 4.3) to deploy the changes. - For further customization, you can refer to the Wasp documentation. Troubleshooting: If you encounter any issues during the deployment, check the following common problems: From 942f51625530d53b495af8ff37de226471d51b41 Mon Sep 17 00:00:00 2001 From: Kumaran Rajendhiran Date: Wed, 5 Jun 2024 11:38:27 +0530 Subject: [PATCH 5/7] Remove latest as api version option (#289) --- fastagency/constants.py | 2 +- fastagency/models/llms/azure.py | 4 ++-- tests/models/llms/test_azure.py | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/fastagency/constants.py b/fastagency/constants.py index bafb314b..3f23d576 100644 --- a/fastagency/constants.py +++ b/fastagency/constants.py @@ -11,4 +11,4 @@ ### Azure OpenAI -AZURE_API_VERSIONS_LITERAL = Literal["2024-02-15-preview", "latest"] +AZURE_API_VERSIONS_LITERAL = Literal["2024-02-15-preview"] diff --git a/fastagency/models/llms/azure.py b/fastagency/models/llms/azure.py index ec15e235..77355625 100644 --- a/fastagency/models/llms/azure.py +++ b/fastagency/models/llms/azure.py @@ -55,9 +55,9 @@ class AzureOAI(Model): api_version: Annotated[ AZURE_API_VERSIONS_LITERAL, Field( - description="The version of the Azure OpenAI API, e.g. '2024-02-15-preview' or 'latest" + description="The version of the Azure OpenAI API, e.g. '2024-02-15-preview'" ), - ] = "latest" + ] = "2024-02-15-preview" @classmethod async def create_autogen(cls, model_id: UUID, user_id: UUID) -> Dict[str, Any]: diff --git a/tests/models/llms/test_azure.py b/tests/models/llms/test_azure.py index bdc030f8..4104c0fb 100644 --- a/tests/models/llms/test_azure.py +++ b/tests/models/llms/test_azure.py @@ -34,7 +34,7 @@ def test_azure_constructor(self) -> None: }, "base_url": "https://my-model.openai.azure.com", "api_type": "azure", - "api_version": "latest", + "api_version": "2024-02-15-preview", } assert model.model_dump() == expected @@ -104,9 +104,10 @@ def test_azure_model_schema(self) -> None: "type": "string", }, "api_version": { - "default": "latest", - "description": "The version of the Azure OpenAI API, e.g. '2024-02-15-preview' or 'latest", - "enum": ["2024-02-15-preview", "latest"], + "const": "2024-02-15-preview", + "default": "2024-02-15-preview", + "description": "The version of the Azure OpenAI API, e.g. '2024-02-15-preview'", + "enum": ["2024-02-15-preview"], "title": "Api Version", "type": "string", }, From 6a7897b7f4a24128f820cab454bcb459c149803d Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Wed, 5 Jun 2024 12:04:19 +0530 Subject: [PATCH 6/7] Update hero image (#293) --- app/src/client/static/open-saas-banner.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/client/static/open-saas-banner.png b/app/src/client/static/open-saas-banner.png index 20ceb62c..8c12c992 100644 --- a/app/src/client/static/open-saas-banner.png +++ b/app/src/client/static/open-saas-banner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fae4608a644c2ba8f93f0547fb4ee133accc07ddc6daf0a9ff7296df164d860d -size 196930 +oid sha256:0f31b375978e1cf8513f385b96b45b7d87264b1c45cc1b52c7f5d716842c00fa +size 198194 From 8172893510aca539d4e0b67fe256a0fe450d40bb Mon Sep 17 00:00:00 2001 From: Kumaran Rajendhiran Date: Wed, 5 Jun 2024 12:51:27 +0530 Subject: [PATCH 7/7] Temporarily disable multi agent team (#294) * Temporarily disable multi agent team * use proper protocol based on domain --- fastagency/models/teams/multi_agent_team.py | 2 +- fastagency/weather_app.py | 5 +- tests/app/test_get_schemas.py | 3 +- tests/models/applications/test_application.py | 46 ++++--------------- tests/models/teams/test_multi_agents_team.py | 1 + 5 files changed, 18 insertions(+), 39 deletions(-) diff --git a/fastagency/models/teams/multi_agent_team.py b/fastagency/models/teams/multi_agent_team.py index 10f89489..7d4c7185 100644 --- a/fastagency/models/teams/multi_agent_team.py +++ b/fastagency/models/teams/multi_agent_team.py @@ -37,7 +37,7 @@ def initiate_chat(self, message: str) -> List[Dict[str, Any]]: ) -@registry.register("team") +# @registry.register("team") class MultiAgentTeam(TeamBaseModel): agent_1: Annotated[ agent_type_refs, diff --git a/fastagency/weather_app.py b/fastagency/weather_app.py index 1c8b9117..3e957997 100644 --- a/fastagency/weather_app.py +++ b/fastagency/weather_app.py @@ -11,9 +11,12 @@ host = environ.get("DOMAIN", "localhost") port = 9000 +protocol = "http" if host == "localhost" else "https" weather_app = FastAPI( - servers=[{"url": f"https://{host}:{port}", "description": "Weather app server"}] + servers=[ + {"url": f"{protocol}://{host}:{port}", "description": "Weather app server"} + ] ) diff --git a/tests/app/test_get_schemas.py b/tests/app/test_get_schemas.py index ba7f76f9..67cf5c56 100644 --- a/tests/app/test_get_schemas.py +++ b/tests/app/test_get_schemas.py @@ -31,7 +31,8 @@ def test_return_all(self) -> None: "secret": {"AzureOAIAPIKey", "OpenAIAPIKey", "BingAPIKey", "OpenAPIAuth"}, "llm": {"AzureOAI", "OpenAI"}, "agent": {"AssistantAgent", "WebSurferAgent", "UserProxyAgent"}, - "team": {"TwoAgentTeam", "MultiAgentTeam"}, + # "team": {"TwoAgentTeam", "MultiAgentTeam"}, + "team": {"TwoAgentTeam"}, "toolbox": {"Toolbox"}, "application": {"Application"}, } diff --git a/tests/models/applications/test_application.py b/tests/models/applications/test_application.py index 5222ebeb..86905965 100644 --- a/tests/models/applications/test_application.py +++ b/tests/models/applications/test_application.py @@ -10,7 +10,10 @@ class TestApplication: - @pytest.mark.parametrize("team_model", [TwoAgentTeam, MultiAgentTeam]) + @pytest.mark.parametrize( + "team_model", + [TwoAgentTeam, pytest.param(MultiAgentTeam, marks=pytest.mark.skip)], + ) def test_application_constructor(self, team_model: Model) -> None: team_uuid = uuid.uuid4() team = team_model.get_reference_model()(uuid=team_uuid) @@ -30,35 +33,6 @@ def test_application_model_schema(self) -> None: schema = Application.model_json_schema() expected = { "$defs": { - "MultiAgentTeamRef": { - "properties": { - "type": { - "const": "team", - "default": "team", - "description": "The name of the type of the data", - "enum": ["team"], - "title": "Type", - "type": "string", - }, - "name": { - "const": "MultiAgentTeam", - "default": "MultiAgentTeam", - "description": "The name of the data", - "enum": ["MultiAgentTeam"], - "title": "Name", - "type": "string", - }, - "uuid": { - "description": "The unique identifier", - "format": "uuid", - "title": "UUID", - "type": "string", - }, - }, - "required": ["uuid"], - "title": "MultiAgentTeamRef", - "type": "object", - }, "TwoAgentTeamRef": { "properties": { "type": { @@ -87,7 +61,7 @@ def test_application_model_schema(self) -> None: "required": ["uuid"], "title": "TwoAgentTeamRef", "type": "object", - }, + } }, "properties": { "name": { @@ -97,10 +71,7 @@ def test_application_model_schema(self) -> None: "type": "string", }, "team": { - "anyOf": [ - {"$ref": "#/$defs/MultiAgentTeamRef"}, - {"$ref": "#/$defs/TwoAgentTeamRef"}, - ], + "allOf": [{"$ref": "#/$defs/TwoAgentTeamRef"}], "description": "The team that is used in the application", "title": "Team name", }, @@ -112,7 +83,10 @@ def test_application_model_schema(self) -> None: # print(f"{schema=}") assert schema == expected - @pytest.mark.parametrize("team_model", [TwoAgentTeam, MultiAgentTeam]) + @pytest.mark.parametrize( + "team_model", + [TwoAgentTeam, pytest.param(MultiAgentTeam, marks=pytest.mark.skip)], + ) def test_assistant_model_validation(self, team_model: Model) -> None: team_uuid = uuid.uuid4() team = team_model.get_reference_model()(uuid=team_uuid) diff --git a/tests/models/teams/test_multi_agents_team.py b/tests/models/teams/test_multi_agents_team.py index ae848964..8caed79f 100644 --- a/tests/models/teams/test_multi_agents_team.py +++ b/tests/models/teams/test_multi_agents_team.py @@ -20,6 +20,7 @@ from fastagency.models.toolboxes.toolbox import FunctionInfo +@pytest.mark.skip(reason="Temporarily disabling multi agent team") class TestMultiAgentTeam: @pytest.mark.parametrize("llm_model", [OpenAI, AzureOAI]) def test_multi_agent_constructor(self, llm_model: Model) -> None: