diff --git a/README.md b/README.md index f67764936..4a1668430 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Welcome to `agentcloud`. This project comprises three main components: -1. **Agent Backend**: A Python application running crewai, communicating LLM messages through socket.io +1. **Agent Backend**: A Python application running LangGraph, CrewAI, and AG2 (formerly AutoGen), communicating LLM messages through socket.io 2. **Webapp**: A UI built using next.js, tailwind, and an express custom server. 3. **Vector Proxy:** A Rust application which communicates with Qdrant vector Database diff --git a/agent-backend/conftest.py b/agent-backend/conftest.py new file mode 100644 index 000000000..136b38fb7 --- /dev/null +++ b/agent-backend/conftest.py @@ -0,0 +1,19 @@ +""" +Root conftest: ensure src/ is first on sys.path so local modules +(ag2, crew, chat, ...) take precedence over any installed packages +with the same name. +""" +import sys +import os + +src_path = os.path.join(os.path.dirname(__file__), "src") +if src_path not in sys.path: + sys.path.insert(0, src_path) + +# If the installed 'ag2' (AutoGen) package was already cached in sys.modules +# before our src/ag2 could be found, evict it so the local module wins. +for key in list(sys.modules.keys()): + if key == "ag2" or key.startswith("ag2."): + mod = sys.modules[key] + if hasattr(mod, "__file__") and mod.__file__ and src_path not in mod.__file__: + del sys.modules[key] diff --git a/agent-backend/poetry.lock b/agent-backend/poetry.lock index f155dc93a..5e2c26d48 100644 --- a/agent-backend/poetry.lock +++ b/agent-backend/poetry.lock @@ -1,5 +1,94 @@ # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +[[package]] +name = "ag2" +version = "0.11.2" +description = "A programming framework for agentic AI" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "ag2-0.11.2-py3-none-any.whl", hash = "sha256:8df8df5efeb7e1ad78aa795cdd3606a3b9f66969171b791d20394182de6a677c"}, + {file = "ag2-0.11.2.tar.gz", hash = "sha256:016e84a7c4fda245b12e0268de9ec796e11bb61763b10ee31fd51e650e3a824e"}, +] + +[package.dependencies] +anyio = ">=3.0.0,<5.0.0" +diskcache = "*" +docker = "*" +httpx = ">=0.28.1,<1" +openai = {version = ">=1.99.3", optional = true, markers = "extra == \"openai\""} +packaging = "*" +pydantic = ">=2.6.1,<3" +python-dotenv = "*" +termcolor = "*" +tiktoken = "*" + +[package.extras] +a2a = ["a2a-sdk[http-server] (>=0.3.11,<0.4)"] +ag-ui = ["ag-ui-protocol (>=0.1.10,<0.2)"] +anthropic = ["anthropic[vertex] (>=0.79.0)"] +autobuild = ["chromadb (>=1.3.0,<1.4)", "huggingface-hub", "sentence-transformers (>=5.2.2,<=6)"] +bedrock = ["boto3 (>=1.34.149)"] +blendsearch = ["flaml[blendsearch]"] +browser-use = ["browser-use (==0.1.37)"] +captainagent = ["chromadb (>=1.3.0,<1.4)", "huggingface-hub", "pandas", "sentence-transformers (>=5.2.2,<=6)"] +cerebras = ["cerebras-cloud-sdk (>=1.0.0)"] +cohere = ["cohere (>=5.13.5)"] +commsagent-discord = ["discord-py (>=2.4.0,<2.7)"] +commsagent-slack = ["slack-sdk (>=3.33.0,<3.40)"] +commsagent-telegram = ["telethon (>=1.38.1,<2)"] +cosmosdb = ["azure-cosmos (>=4.2.0)"] +crawl4ai = ["crawl4ai (>=0.5.0,<0.9)"] +deepseek = ["openai (>=1.99.3)"] +dev = ["a2a-sdk[http-server] (>=0.3.11,<0.4)", "ag-ui-protocol (>=0.1.10,<0.2)", "aiofiles (>=24.1.0,<26.0.0)", "cairosvg", "codespell (==2.4.1)", "detect-secrets (==1.5.0)", "dirty-equals (==0.9.0)", "fastapi (==0.116.1)", "freezegun (==1.5.5)", "ipykernel (==7.1.0)", "jinja2 (==3.1.6)", "mcp (>=1.11.0)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.4.7)", "mkdocs-glightbox (==0.5.1)", "mkdocs-literate-nav (==0.6.2)", "mkdocs-macros-plugin (==1.3.9)", "mkdocs-material (==9.6.19)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-redirects (==1.2.2)", "mkdocstrings[python] (==0.30.0)", "mypy (==1.17.1)", "nbclient (==0.10.2)", "nbconvert (==7.16.6)", "nbformat (==5.10.4)", "openai (>=1.99.3)", "opentelemetry-api (>=1.20)", "opentelemetry-exporter-otlp-proto-grpc (>=1.20)", "opentelemetry-sdk (>=1.20)", "pandas (==2.3.2)", "pdoc3 (==0.11.6)", "pillow", "pre-commit (==4.3.0)", "pytest (==8.4.2)", "pytest-asyncio (==1.1.0)", "pytest-cov (==6.3.0)", "pyupgrade-directories (==0.3.0)", "pyyaml (==6.0.2)", "ruff (==0.12.12)", "termcolor (==3.1.0)", "toml (==0.10.2)", "typer (==0.17.4)", "types-decorator", "types-pycurl", "types-python-dateutil", "types-pyyaml", "types-requests", "types-ujson", "uv (==0.8.15)"] +docs = ["a2a-sdk[http-server] (>=0.3.11,<0.4)", "ag-ui-protocol (>=0.1.10,<0.2)", "cairosvg", "jinja2 (==3.1.6)", "mcp (>=1.11.0)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.4.7)", "mkdocs-glightbox (==0.5.1)", "mkdocs-literate-nav (==0.6.2)", "mkdocs-macros-plugin (==1.3.9)", "mkdocs-material (==9.6.19)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-redirects (==1.2.2)", "mkdocstrings[python] (==0.30.0)", "nbclient (==0.10.2)", "opentelemetry-api (>=1.20)", "opentelemetry-exporter-otlp-proto-grpc (>=1.20)", "opentelemetry-sdk (>=1.20)", "pdoc3 (==0.11.6)", "pillow", "pyyaml (==6.0.2)", "termcolor (==3.1.0)", "typer (==0.17.4)"] +duckduckgo = ["duckduckgo-search (>=8.0.2)"] +flaml = ["flaml", "numpy (>=1.24.0,<2.0.0) ; python_version < \"3.13\"", "numpy (>=2.1) ; python_version >= \"3.13\""] +gemini = ["google-api-core", "google-auth", "google-cloud-aiplatform", "google-genai (>=1.20.0,<2.0)", "jsonschema", "pillow"] +gemini-realtime = ["google-api-core", "google-auth", "google-cloud-aiplatform", "google-genai (>=1.20.0,<2.0)", "jsonschema", "pillow", "websockets (>=14.0,<16)"] +google-api = ["google-api-python-client (>=2.163.0,<3.0)", "google-auth-httplib2 (>=0.2.0,<0.3)", "google-auth-oauthlib (>=1.2.1,<2.0)"] +google-client = ["google-api-python-client (>=2.163.0,<3.0)"] +google-search = ["google-api-python-client (>=2.163.0,<3.0)"] +graph = ["matplotlib", "networkx"] +graph-rag-falkor-db = ["falkordb (>=1.0.10)", "graphrag-sdk (==0.8.2)"] +groq = ["groq (>=0.9.0)"] +interop = ["crewai[tools] (>=0.76,<1) ; python_version >= \"3.10\" and python_version < \"3.13\"", "fasta2a", "langchain-community (>=0.3.12,<1)", "litellm (<=1.76.3)", "pydantic-ai (>=1.0.12)", "weaviate-client (>=4,<5) ; python_version >= \"3.10\" and python_version < \"3.13\""] +interop-crewai = ["crewai[tools] (>=0.76,<1) ; python_version >= \"3.10\" and python_version < \"3.13\"", "litellm (<=1.76.3)", "weaviate-client (>=4,<5) ; python_version >= \"3.10\" and python_version < \"3.13\""] +interop-langchain = ["langchain-community (>=0.3.12,<1)"] +interop-pydantic-ai = ["fasta2a", "pydantic-ai (>=1.0.12)"] +jupyter-executor = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "requests", "websocket-client"] +lint = ["codespell (==2.4.1)", "pyupgrade-directories (==0.3.0)", "ruff (==0.12.12)"] +lmm = ["pillow", "replicate"] +long-context = ["llmlingua (<0.3)"] +mathchat = ["sympy", "wolframalpha"] +mcp = ["mcp (>=1.11.0)"] +mcp-proxy-gen = ["fastapi (>=0.112,<1)", "fastapi-code-generator (>=0.5.4)", "pyyaml", "requests", "typer"] +mistral = ["mistralai (>=1.0.1)"] +neo4j = ["docx2txt (==0.9)", "llama-index (>=0.12,<0.14)", "llama-index-core (>=0.12,<0.14)", "llama-index-graph-stores-neo4j (>=0.4,<0.6)", "llama-index-readers-web (>=0.4,<0.6)"] +ollama = ["fix-busted-json (>=0.0.18)", "ollama (>=0.4.7)"] +openai = ["openai (>=1.99.3)"] +openai-realtime = ["openai (>=1.99.3)", "openai[realtime]"] +rag = ["chromadb (>=0.5,<2)", "docling (>=2.15.1,<3)", "llama-index (>=0.14.13,<0.15)", "llama-index-core (>=0.14.13,<0.15)", "llama-index-embeddings-huggingface (>=0.5,<0.7)", "llama-index-embeddings-openai (>=0.3,<0.6)", "llama-index-llms-langchain (>=0.6,<0.8)", "llama-index-llms-openai (>=0.4,<0.6)", "llama-index-vector-stores-chroma (>=0.4,<0.6)", "llama-index-vector-stores-mongodb (>=0.6,<0.9)", "requests (>=2.32.3,<3)", "selenium (>=4.28.1,<5)", "webdriver-manager (==4.0.2)"] +redis = ["redis"] +remyx = ["docker (>=6.0.0)", "remyxai (>=0.2.0)"] +retrievechat = ["beautifulsoup4", "chromadb (>=1.4.1,<1.5)", "ipython", "markdownify", "protobuf (==6.33.5)", "pypdf", "sentence-transformers (>=5.2.2,<=6)"] +retrievechat-couchbase = ["beautifulsoup4", "chromadb (>=1.4.1,<1.5)", "couchbase (>=4.3.0)", "ipython", "markdownify", "numpy", "protobuf (==6.33.5)", "pypdf", "sentence-transformers (>=5.2.2,<=6)"] +retrievechat-mongodb = ["beautifulsoup4", "chromadb (>=1.4.1,<1.5)", "ipython", "markdownify", "numpy", "protobuf (==6.33.5)", "pymongo (>=4.0.0)", "pypdf", "sentence-transformers (>=5.2.2,<=6)"] +retrievechat-pgvector = ["beautifulsoup4", "chromadb (>=1.4.1,<1.5)", "ipython", "markdownify", "pgvector (>=0.2.5)", "protobuf (==6.33.5)", "psycopg (>=3.1.18) ; platform_system == \"Linux\"", "psycopg[binary] (>=3.1.18) ; platform_system == \"Windows\" or platform_system == \"Darwin\"", "pypdf", "sentence-transformers (>=5.2.2,<=6)"] +retrievechat-qdrant = ["beautifulsoup4", "chromadb (>=1.4.1,<1.5)", "fastembed (>=0.3.1)", "ipython", "markdownify", "protobuf (==6.33.5)", "pypdf", "qdrant-client", "sentence-transformers (>=5.2.2,<=6)"] +tavily = ["tavily-python (>=0.7.4)"] +teachable = ["chromadb"] +test = ["aiofiles (>=24.1.0,<26.0.0)", "dirty-equals (==0.9.0)", "fastapi (==0.116.1)", "freezegun (==1.5.5)", "ipykernel (==7.1.0)", "mcp (>=1.11.0)", "nbconvert (==7.16.6)", "nbformat (==5.10.4)", "pandas (==2.3.2)", "pytest (==8.4.2)", "pytest-asyncio (==1.1.0)", "pytest-cov (==6.3.0)"] +together = ["together (>=1.2)"] +tracing = ["opentelemetry-api (>=1.20)", "opentelemetry-exporter-otlp-proto-grpc (>=1.20)", "opentelemetry-sdk (>=1.20)"] +twilio = ["fastapi (>=0.115.0,<1)", "twilio (>=9.3.2)", "uvicorn (>=0.30.6,<1)"] +types = ["a2a-sdk[http-server] (>=0.3.11,<0.4)", "ag-ui-protocol (>=0.1.10,<0.2)", "aiofiles (>=24.1.0,<26.0.0)", "dirty-equals (==0.9.0)", "fastapi (==0.116.1)", "freezegun (==1.5.5)", "ipykernel (==7.1.0)", "mcp (>=1.11.0)", "mypy (==1.17.1)", "nbconvert (==7.16.6)", "nbformat (==5.10.4)", "openai (>=1.99.3)", "pandas (==2.3.2)", "pytest (==8.4.2)", "pytest-asyncio (==1.1.0)", "pytest-cov (==6.3.0)", "types-decorator", "types-pycurl", "types-python-dateutil", "types-pyyaml", "types-requests", "types-ujson"] +websockets = ["websockets (>=14.0,<16)"] +websurfer = ["beautifulsoup4", "markdownify", "pathvalidate", "pdfminer-six"] +wikipedia = ["wikipedia-api (>=0.8.1,<1.0)"] +yepcode = ["python-dotenv", "yepcode-run (>=1.6.1)"] + [[package]] name = "aiohappyeyeballs" version = "2.4.0" @@ -1093,6 +1182,29 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "docker" +version = "7.1.0" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, + {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, +] + +[package.dependencies] +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" + +[package.extras] +dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] +docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] +ssh = ["paramiko (>=2.4.3)"] +websockets = ["websocket-client (>=1.3.0)"] + [[package]] name = "docstring-parser" version = "0.16" @@ -2298,14 +2410,14 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.27.1" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "httpx-0.27.1-py3-none-any.whl", hash = "sha256:10877266452bbb4c7949734e5febc52d040fdf66f9e7c6d644a679f276d37de4"}, - {file = "httpx-0.27.1.tar.gz", hash = "sha256:ce9d45798d6d047b7db67d7949e371f2b453105a53d8df315564b2483d346878"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -2314,7 +2426,6 @@ certifi = "*" h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] @@ -2822,26 +2933,23 @@ images = ["pillow (>=10.1.0,<11.0.0)"] [[package]] name = "langchain-google-vertexai" -version = "1.0.10" +version = "1.0.8" description = "An integration package connecting Google VertexAI and LangChain" optional = false python-versions = "<4.0,>=3.8.1" groups = ["main"] files = [ - {file = "langchain_google_vertexai-1.0.10-py3-none-any.whl", hash = "sha256:7b745378c17ed22d8c38003a1da994bd0dd44d3288e2c3c9f82dba5ed4b3de6b"}, - {file = "langchain_google_vertexai-1.0.10.tar.gz", hash = "sha256:8c163d198a084217a21c7e4c0e40954876e0bc1de08364f2e6f4b160a8652217"}, + {file = "langchain_google_vertexai-1.0.8-py3-none-any.whl", hash = "sha256:e35fb910604bfb05704f191bf17998e7f841a92f21677a0219c4f3a97de945c3"}, + {file = "langchain_google_vertexai-1.0.8.tar.gz", hash = "sha256:db5b28aca9ae2cd0a775d97ec25374f9efac05e0ad336cb1cd5ca665e47b3fc3"}, ] [package.dependencies] google-cloud-aiplatform = ">=1.56.0,<2.0.0" google-cloud-storage = ">=2.17.0,<3.0.0" -httpx = ">=0.27.0,<0.28.0" -httpx-sse = ">=0.4.0,<0.5.0" -langchain-core = ">=0.2.33,<0.3" +langchain-core = ">=0.2.25,<0.3" [package.extras] anthropic = ["anthropic[vertexai] (>=0.30.0,<1)"] -mistral = ["langchain-mistralai (>=0.1.12,<1)"] [[package]] name = "langchain-groq" @@ -3601,18 +3709,19 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "ollama" -version = "0.3.1" +version = "0.6.1" description = "The official Python client for Ollama." optional = false -python-versions = "<4.0,>=3.8" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "ollama-0.3.1-py3-none-any.whl", hash = "sha256:db50034c73d6350349bdfba19c3f0d54a3cea73eb97b35f9d7419b2fc7206454"}, - {file = "ollama-0.3.1.tar.gz", hash = "sha256:032572fb494a4fba200c65013fe937a65382c846b5f358d9e8918ecbc9ac44b5"}, + {file = "ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c"}, + {file = "ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6"}, ] [package.dependencies] -httpx = ">=0.27.0,<0.28.0" +httpx = ">=0.27" +pydantic = ">=2.9" [[package]] name = "onnx" @@ -3702,14 +3811,14 @@ sympy = "*" [[package]] name = "openai" -version = "1.42.0" +version = "1.109.1" description = "The official Python library for the openai API" optional = false -python-versions = ">=3.7.1" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.42.0-py3-none-any.whl", hash = "sha256:dc91e0307033a4f94931e5d03cc3b29b9717014ad5e73f9f2051b6cb5eda4d80"}, - {file = "openai-1.42.0.tar.gz", hash = "sha256:c9d31853b4e0bc2dc8bd08003b462a006035655a701471695d0bfdc08529cde3"}, + {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, + {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, ] [package.dependencies] @@ -3723,7 +3832,10 @@ tqdm = ">4" typing-extensions = ">=4.11,<5" [package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +realtime = ["websockets (>=13,<16)"] +voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "opentelemetry-api" @@ -4417,122 +4529,133 @@ files = [ [[package]] name = "pydantic" -version = "2.9.0b1" +version = "2.11.10" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.9.0b1-py3-none-any.whl", hash = "sha256:7e7171d13425d10975008195d6733c6824489f359aba3ea5772fed4f5f6875ef"}, - {file = "pydantic-2.9.0b1.tar.gz", hash = "sha256:f7cdbd6dca8ee14953b8ecb6c0ea93b8f1fc3668c4bee58cadabbcd7b05e34a3"}, + {file = "pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a"}, + {file = "pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.23.0" -typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} -tzdata = {version = "*", markers = "python_version >= \"3.9\""} +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" -version = "2.23.0" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c3ed715355de44fd1ad5e2347ae9d8b48c12423a938174accd98f3c21cd35141"}, - {file = "pydantic_core-2.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e1eaba0496f00923f125bea4ce2ec2ad7b32753d037ad456ea8b971888ccebf"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cd64a16bdd44ea2120e5592818b1391b22adb02632ca4438897b10c07ef62a"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:843c98c21b7cd80b0e6f8a44e84fc6ebe63a823b991c337361d1e9141a7d753d"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:848727b22f9117b826a5cc769a874c8a218ab994a455a739fee1aaab611e10f8"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3afe0158f5ee4ba5a0b468dbd3adc46e1c460a2de10ab6af19cab7ccf2831aaa"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5d6c3a0a5f3a1c0c7d57acbb1cce592525959414b8f0b79e00d123b8f979d8b"}, - {file = "pydantic_core-2.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f4806078005609da8957757ec41632da08ae33d2e12ce9b82a03e0f9bd9296a"}, - {file = "pydantic_core-2.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f288373c599466a3977b6305e8ff21ed81465b8c5b5ba2b1a160c89ad6a09285"}, - {file = "pydantic_core-2.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b0749590fa48c99d93670d2b6a452e0b08c0accd39d86e7579ed176fd8ce9687"}, - {file = "pydantic_core-2.23.0-cp310-none-win32.whl", hash = "sha256:f6f2083e9e2250ea7563a809dc28f4df1c4f78a0f8191fc82755bb9edff438a4"}, - {file = "pydantic_core-2.23.0-cp310-none-win_amd64.whl", hash = "sha256:30bab73daf1cbd751fb925fbf43c932dc5edfd694af2ee6d207bf9daccf86abd"}, - {file = "pydantic_core-2.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c174a6a3885f04da4a94ca15e2ce5ec4db535b8468ee71de246bf709188630e6"}, - {file = "pydantic_core-2.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:38ce3939035e86c775427135a59f4d0da6a2fd567851fc921b24993898d2c584"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:433ae51fcd7e32e82a922aa67b2233a94f72e58d1d5674322eeeedc2a2a5de1f"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be25600552428fb43a4d94da3d9001f2a519bde715515bf8704acbd80495b279"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ec667c44d32995b71959edfbb3687508686cd06b16fecd8c1d0b97e4195c06"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b73ee3051e98a096027f529a5169560e22c61de067d44858b680fc21b0bd6c20"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100fc9d3a681864212dc5d0d25bbac545205d4c31e5f93095f9b6b1f799e8be7"}, - {file = "pydantic_core-2.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e082678c0bf14e9204175db2d0364410549f6457dcbd297ef642011d369283f2"}, - {file = "pydantic_core-2.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ba66866a945f96dd20a34c2205822131e8786c2402ce5620e8d73b3b3da665e1"}, - {file = "pydantic_core-2.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f476419afe9b70d7f25fef7bec7033f69c2e3e1f35de23dad5dc5cc098759ce2"}, - {file = "pydantic_core-2.23.0-cp311-none-win32.whl", hash = "sha256:c886deed32c95db3d3f46195316aea5b87b754e2b84ab14da0aea491df38a997"}, - {file = "pydantic_core-2.23.0-cp311-none-win_amd64.whl", hash = "sha256:d3eda2b28ed432709f4eae83316fe8a64e5ab79a9d5d9a45950e89c8d43d17d0"}, - {file = "pydantic_core-2.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:638f8bae1d32f1cff46ed7742782b255d8175c4b006f1f41dccfa68ae7bbd2d2"}, - {file = "pydantic_core-2.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe9743b50b86bf75b2f0b48c899665da30ae4e332963a0054d4c74548ba72ab0"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8681973c8648bca5f18aae7a949a90b2177d894a30763b54d35b264c6b3cb02"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e9935b6c89171bab3e4d3f67c4d4d19df60ab2db5676d27894cd39a834b97dde"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dafe486b2cd19b119d6dce9cd4b2545eb04a679ef4faa6e5b895e0c2838d020"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21b2d238fd200162e64303fd9e805b27d96ffe24164b31350a97eba75bf712e9"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0abc8460e7f214631bd14af8fde2ad69dcc54ae5facabdeb93150c5d3bd2abf3"}, - {file = "pydantic_core-2.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb6f9b87d5e1a558f0a8ac9a9282f76505b1ac96695d6bac4a036dce88d927ec"}, - {file = "pydantic_core-2.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:262d77d4c4fa31f4bdfd838d21f265c35c469e980cc422a47d0b2ae796f38d07"}, - {file = "pydantic_core-2.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:781ba2aa6023a14bf865d7368d674db44c99a8630fc76ba73e850ca7b5a1febb"}, - {file = "pydantic_core-2.23.0-cp312-none-win32.whl", hash = "sha256:2670f3a68d1a574ae1b0f3da016c010dd08d0a422d14a3b78be47aebbae9a310"}, - {file = "pydantic_core-2.23.0-cp312-none-win_amd64.whl", hash = "sha256:29904fc5b7c2795b236e1a54680b3cb203c50b1968938a06a4ca7aaee45e71cc"}, - {file = "pydantic_core-2.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be43d9249acf772be100ca1f687680d0e2e2bd5fbaecc3e904f5a65762d45a86"}, - {file = "pydantic_core-2.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9c3b124d0c1f2b6d462aac3cd194457d419e62d62bbd7fc60392e10eb179d10"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2933accfb2e181d67ae37eb2c40abb1a226841392e90edaa62900c0fe7f47e0e"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eac616e3571f9083d972accae44e98ca7020e7c345ac9317fcc8b5638175cadc"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6264eee5c7fced027286ed143c329f531878ec9ae11e7be3c06635045ecbf81f"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10a6b04f4f86e3816b50dc9785bb662d87e5eda8423f15a80e6f245f6695828a"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d9053f4c9ff5ed98270937938efaadf8a4beb6826ce6cee1e66614b7613c969"}, - {file = "pydantic_core-2.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00eb370a47bd63d6f0e59bae8080c43eb21aca033785ecaeba3fd36f2cfe0877"}, - {file = "pydantic_core-2.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6c093a00bce0de23102c3f4a8fab5629f6c0e2dc09eeb7f275c03895fcee8598"}, - {file = "pydantic_core-2.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ee96b28ffefe34ddf5e83e28c0ece5f96f7e3a4caa51b1683b2a71d990f8235e"}, - {file = "pydantic_core-2.23.0-cp313-none-win32.whl", hash = "sha256:a69297f237984b358275cde98bf81b816a6528f579cf3818954d6266bd64c48d"}, - {file = "pydantic_core-2.23.0-cp313-none-win_amd64.whl", hash = "sha256:8ff9bd3024412d9766343fe60e1dd6e590d30202dd20d27a881dd790b9b1b72d"}, - {file = "pydantic_core-2.23.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:690a49c803412267758d114dd8f1e2a819436e1ec3df5043b7ea25bcbfabe40a"}, - {file = "pydantic_core-2.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:987503763bbdc403821e8b0e618888eae02531f416964aeb1f867ca53f0d1e72"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7f929d7fffe5017aa0b50414a94a57ac21d96004673856147ea2af90beac7b"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a17d5524f10afd850a0b4365f38931079dc5b00e3417e6b1774d0b8683a5244"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec9897bde1b4654d513e065a97d7af50de5bfdf3419ba4ef3fed0fbe2d57451e"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7d0a3607a63c328762b904e8ecb471b2fbe0223bfc01d30e0e962cb71ca508"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b1f24ac7a09c430929234d43011e5a4195c1b3d32dd3a80fb6f175dcf301b"}, - {file = "pydantic_core-2.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f186dc1c1e0fe14a453ccc9a90201c03c12fbdda30e1624e405cf67dcf1dc00"}, - {file = "pydantic_core-2.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9add1a74e7d8008f4ed9bd4b7cb62de6fd28f81b28f7f6def8c0389a50a98127"}, - {file = "pydantic_core-2.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d129992e977ecf672d4eb7c7ba0e5f13725995d0fac246c673f699b7722c9ae5"}, - {file = "pydantic_core-2.23.0-cp38-none-win32.whl", hash = "sha256:b75989878eb94f1a63a6a558735cee1c8d6c7d24007b61df41efc8cdbe819f70"}, - {file = "pydantic_core-2.23.0-cp38-none-win_amd64.whl", hash = "sha256:08dae83791aee669f7160a94e801162e8f17612c4a821c86e17f9a802389a011"}, - {file = "pydantic_core-2.23.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8e18fb1effad8d28ae510d35507eecf79abcba96c09627443c51acdfbfa30f93"}, - {file = "pydantic_core-2.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84667cd32f432bb7f1e0bc5f57c4ea2767d9493e9beef8f195657c16ea3a6076"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5202456ff1f71c2146ba63ffe4538be7fa5d0b6697de14f706d4266aa4e7bf47"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ae227d46fd1fa1b5a02c18a5a7c80ccff014b7bf596731af5bb157f33e058c6"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a0d09b436ab65a35b5ad90fc295c4a0e545c2456e2212f242809c7474e60c6"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49dea6930ab48b3ca233e7d541d5f6480f6ffa67a1cc2352cd8395c6f3c14745"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e21b2ee6273cf0d637a7b1622a6ece8761f111083822f1e814e7522d5efb1772"}, - {file = "pydantic_core-2.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:873e2453ff6451bdf5619c3c1122fa9998a39552d2fd4c2d9c34f61611e026d6"}, - {file = "pydantic_core-2.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c0eb1577f5073cfb336e5d08cb91e717197c0727803962850561d529c07e53c"}, - {file = "pydantic_core-2.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c97f1fb88ec947522ec5863630cb17e3b3ec9f6ca09f954107ad3d35ca3abfe8"}, - {file = "pydantic_core-2.23.0-cp39-none-win32.whl", hash = "sha256:804a81e50dc43508068964a2b7abacaf99850e2cb7cbd0ae3a8e0145cb89ba1f"}, - {file = "pydantic_core-2.23.0-cp39-none-win_amd64.whl", hash = "sha256:54a4da481e9130eeba5b02fcfe95fcf26e188747b5b9c6a3bc434ee6f7573b04"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e8e33bf23a61b07456a1666a07ba453c0906ed74c1330a60ba4c10691628310"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b28dadb0c7494e47304e5c0fec95c500ed914e88b53018ccb85d700fbb1d2bdc"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92137a6a2236223ae5aa4f610ca2a4fe5c8770941deeb6b295f477c71257e618"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3482e7e003b2b391ad2441c98ba219cbd71e3de12b3a5f422c5d9151479c827"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e34ae0eed62065a917f527296a0da2dc71056d53e5b7c8849d48ad69cc73a486"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:81b84e88ed517a4911f8ebef141b41f5b1d21f8100209b0fc118079accede06e"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:837cbeec0ad83ebd57ae9f4e8bd18c8ed34acfe3ec809e2ceceac253b0bc3fd7"}, - {file = "pydantic_core-2.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39d4ac2cf7a25f1ceec7588a5c9b86ccdf330a5941753e1779974c08a9d763c4"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:865f96455c0f5af1b1e73c4f52659899a635af4c1e4db5ae40f848ba456dfd24"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff54894bc04f5982db8fa6ff6d336afa2e4b66cccc76952a35772ff7485d5553"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3734dd979f69b322ec7ddff60123264474801ef9620255f120e10d33596c3a6"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca5363fb0f39135cd6cbdbaa542c24a91246cf9d08fb5c3ba23f774ef4e91f66"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d00380dbb87abf553d173e2b0c4863a4e2e8573e36ee865af7237a065adc3519"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5fc73dd963497080b1961c16e6a01c054acb525fb8108c6aeed208d109d7385e"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:151968f31efca42a9a5ae25cd00a62a67fb368ed4fdf05c861c629138e5cf674"}, - {file = "pydantic_core-2.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95e912ae3ccbd9c20f44b85fc7f3e1e55db8f8688c8cf519aa517580ae7ba399"}, - {file = "pydantic_core-2.23.0.tar.gz", hash = "sha256:954509ea58b1adf6614390056da194e86feb83423679984f8453416db7cfa8c9"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -4818,7 +4941,7 @@ description = "Python for Window Extensions" optional = false python-versions = "*" groups = ["main"] -markers = "platform_system == \"Windows\"" +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, @@ -5807,6 +5930,21 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" version = "2024.1" @@ -6169,19 +6307,19 @@ files = [ [[package]] name = "wikibase-rest-api-client" -version = "0.1.4" +version = "0.2.5" description = "A client library for accessing the Wikibase REST API" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "wikibase_rest_api_client-0.1.4-py3-none-any.whl", hash = "sha256:8d6565041d54ec67fde59a4b19895fa86d2da9db4113411e04834d3bebe593cd"}, - {file = "wikibase_rest_api_client-0.1.4.tar.gz", hash = "sha256:d8445252e6a1c2926ce98de8a754f75ec4c7eca78fd1cf13a3a31c1cabce1aa7"}, + {file = "wikibase_rest_api_client-0.2.5-py3-none-any.whl", hash = "sha256:96c6075a38458e78ae4b689b64848ee3c72f464c60814f6d67675f9a844de7c5"}, + {file = "wikibase_rest_api_client-0.2.5.tar.gz", hash = "sha256:c3a24caaf721af5cec72a5add0ef243ab098079e5212be8f3cef75a28c5cdaec"}, ] [package.dependencies] attrs = ">=21.3.0" -httpx = ">=0.20.0,<0.28.0" +httpx = ">=0,<1" python-dateutil = ">=2.8.0,<3.0.0" [[package]] @@ -6452,4 +6590,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "4e99a24ffc437397221d456b0e6fc0ad199160ab036b91e4559e2f63ff5899c7" +content-hash = "1a0c71ccee49de989cefdc65d626960c4d28ac0aec1d85a91af510b00a9ca415" diff --git a/agent-backend/pyproject.toml b/agent-backend/pyproject.toml index 616060ddd..650f8d06c 100644 --- a/agent-backend/pyproject.toml +++ b/agent-backend/pyproject.toml @@ -31,7 +31,7 @@ arxiv = "^2.1.0" lark = "^1.1.9" wikipedia = "^1.4.0" mediawikiapi = "^1.2" -wikibase-rest-api-client = "^0.1.3" +wikibase-rest-api-client = "^0.2.5" duckduckgo-search = "^5.3.0" stackapi = "^0.3.0" youtube-search = "^2.1.2" @@ -42,6 +42,7 @@ google-cloud-logging = "^3.10.0" langchain-groq = "^0.1.4" apify-client = "^1.7.0" langgraph = "^0.2.16" +ag2 = {version = ">=0.11.0", extras = ["openai"]} langchain-ollama = "^0.1.1" pinecone-client = "^5.0.1" @@ -50,6 +51,9 @@ motor = "^3.5.1" minio = "^7.2.8" ftfy = "^6.1.3" +[tool.pytest.ini_options] +pythonpath = ["src"] + [tool.flake8] max-line-length = 200 ignore = ["E501"] diff --git a/agent-backend/src/ag2/__init__.py b/agent-backend/src/ag2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/agent-backend/src/ag2/builder.py b/agent-backend/src/ag2/builder.py new file mode 100644 index 000000000..0ab4b421e --- /dev/null +++ b/agent-backend/src/ag2/builder.py @@ -0,0 +1,269 @@ +""" +AG2 (formerly AutoGen) runtime for AgentCloud. + +Supports three orchestration modes (set via crew.config["ag2_mode"]): + two_agent — AssistantAgent + UserProxyAgent (Docker sandbox for code execution) + group_chat — GroupChat with LLM-driven speaker selection + reasoning — ReasoningAgent with tree-of-thought beam search + +Usage: AG2Builder("session-id").run("user message") +""" +from __future__ import annotations + +import logging +import uuid +from datetime import datetime + +from autogen import ( + AssistantAgent, + ConversableAgent, + GroupChat, + GroupChatManager, + LLMConfig, + UserProxyAgent, +) +from autogen.agents.experimental import ReasoningAgent +from socketio import SimpleClient +from socketio.exceptions import ConnectionError as ConnError + +from ag2.streaming import make_socket_reply_func +from ag2.tool_adapter import build_ag2_tools +from init.env_variables import AGENT_BACKEND_SOCKET_TOKEN, SOCKET_URL +from init.mongo_session import start_mongo_session +from models.mongo import Agent, Model, Session +from models.sockets import Message, SocketEvents, SocketMessage +from messaging.send_message_to_socket import send + +logger = logging.getLogger(__name__) + +_mongo = start_mongo_session() + + +class AG2Builder: + """ + Builds and runs an AG2 multi-agent session. + Reads agent/model config from MongoDB; follows the ChatAssistant pattern. + ag2_mode is stored in crew.config["ag2_mode"] (defaults to "group_chat"). + """ + + def __init__(self, session_id: str): + self.session_id = session_id + self.socket = SimpleClient() + self._init_socket() + self._init_app_state() + + # ── Initialisation ──────────────────────────────────────────────────────── + + def _init_socket(self) -> None: + try: + custom_headers = {"x-agent-backend-socket-token": AGENT_BACKEND_SOCKET_TOKEN} + self.socket.connect(url=SOCKET_URL, headers=custom_headers) + self.socket.emit("join_room", f"_{self.session_id}") + except ConnError as ce: + logger.error("Socket connection error: %s", ce) + raise + + def _init_app_state(self) -> None: + session: Session = _mongo.get_session(self.session_id) + _app, the_crew, _tasks, crew_agents = _mongo.get_crew(session) + + self.agents_config: list[Agent] = list(crew_agents) if crew_agents else [] + self.ag2_mode: str = (the_crew.config or {}).get("ag2_mode", "group_chat") + + # Build shared LLMConfig from the first agent's model + primary = self.agents_config[0] if self.agents_config else None + if primary: + model: Model = _mongo.get_single_model_by_id("models", Model, primary.modelId) + cfg = model.config or {} + self.llm_config = LLMConfig( + { + "model": cfg.get("model", "gpt-4o-mini"), + "api_key": cfg.get("api_key", ""), + "base_url": cfg.get("base_url", "https://api.openai.com/v1"), + }, + temperature=model.temperature if model.temperature is not None else 0.3, + cache_seed=None, + ) + else: + self.llm_config = None + + # ── Dispatch ────────────────────────────────────────────────────────────── + + def run(self, message: str) -> None: + handler = { + "two_agent": self._run_two_agent, + "group_chat": self._run_group_chat, + "reasoning": self._run_reasoning, + }.get(self.ag2_mode, self._run_group_chat) + try: + handler(message) + except Exception as exc: + logger.exception("AG2 runtime error in session %s", self.session_id) + self._send_to_socket(f"AG2 runtime error: {exc}") + finally: + self._send_stop() + + # ── Two-agent mode ──────────────────────────────────────────────────────── + + def _run_two_agent(self, message: str) -> None: + reply_fn = make_socket_reply_func(self.socket, self.session_id) + system_msg = ( + "\n".join([self.agents_config[0].role, self.agents_config[0].goal]) + if self.agents_config + else "You are a helpful AI assistant." + ) + tools = build_ag2_tools(self._platform_tools()) + + assistant = AssistantAgent( + name="assistant", + system_message=system_msg, + llm_config=self.llm_config, + ) + user_proxy = UserProxyAgent( + name="user_proxy", + human_input_mode="NEVER", + # use_docker=True: agent-generated code runs in an isolated container, + # not in the platform's Python process. Required for a managed platform. + # Ensure the Docker socket is accessible from the agent-backend container. + code_execution_config={"work_dir": f"/tmp/ag2_{self.session_id}", "use_docker": True}, + is_termination_msg=lambda m: "TERMINATE" in (m.get("content") or ""), + ) + for tool_fn, tool_meta in tools: + assistant.register_for_llm(**tool_meta)(tool_fn) + user_proxy.register_for_execution(**tool_meta)(tool_fn) + + assistant.register_reply(trigger=ConversableAgent, reply_func=reply_fn, position=0) + user_proxy.initiate_chat(assistant, message=message) + + # ── Group chat mode ─────────────────────────────────────────────────────── + + def _run_group_chat(self, message: str) -> None: + reply_fn = make_socket_reply_func(self.socket, self.session_id) + tools = build_ag2_tools(self._platform_tools()) + + agents = [ + AssistantAgent( + name=a.name.replace(" ", "_"), + system_message="\n".join([a.role, a.goal, a.backstory]), + llm_config=self.llm_config, + ) + for a in self.agents_config + ] or [ + AssistantAgent( + name="assistant", + system_message="You are a helpful assistant.", + llm_config=self.llm_config, + ) + ] + + executor = UserProxyAgent( + name="executor", + human_input_mode="NEVER", + code_execution_config=False, + is_termination_msg=lambda m: "TERMINATE" in (m.get("content") or ""), + ) + for tool_fn, tool_meta in tools: + agents[0].register_for_llm(**tool_meta)(tool_fn) + executor.register_for_execution(**tool_meta)(tool_fn) + + for agent in agents: + agent.register_reply(trigger=ConversableAgent, reply_func=reply_fn, position=0) + + gc = GroupChat( + agents=[executor] + agents, + messages=[], + max_round=15, + speaker_selection_method="auto", + ) + manager = GroupChatManager(groupchat=gc, llm_config=self.llm_config) + executor.initiate_chat(manager, message=message) + + # ── Reasoning mode ──────────────────────────────────────────────────────── + + def _run_reasoning(self, message: str) -> None: + reply_fn = make_socket_reply_func(self.socket, self.session_id) + system_msg = ( + "\n".join([self.agents_config[0].role, self.agents_config[0].goal]) + if self.agents_config + else "You are a careful reasoning agent." + ) + agent = ReasoningAgent( + name="reasoning_agent", + system_message=system_msg, + llm_config=self.llm_config, + reason_config={"method": "beam_search", "beam_size": 3, "max_depth": 4}, + ) + user_proxy = UserProxyAgent( + name="user_proxy", + human_input_mode="NEVER", + code_execution_config=False, + is_termination_msg=lambda m: "TERMINATE" in (m.get("content") or ""), + ) + agent.register_reply(trigger=ConversableAgent, reply_func=reply_fn, position=0) + user_proxy.initiate_chat(agent, message=message) + + # ── Helpers ─────────────────────────────────────────────────────────────── + + def _platform_tools(self) -> list[dict]: + """ + Returns tool configs as {name, description, fn} dicts for all tools + attached to the first agent in the crew. Extend here to support + per-agent tool assignment in group_chat mode. + """ + if not self.agents_config or not self.agents_config[0].toolIds: + return [] + from models.mongo import Tool + tools = _mongo.get_models_by_ids("tools", Tool, self.agents_config[0].toolIds) or [] + result = [] + for tool in tools: + # Wrap tool.run as a plain callable for AG2 registration + def _make_fn(t): + def fn(query: str) -> str: + return t.run(query) + fn.__name__ = t.name.replace(" ", "_").lower() + fn.__doc__ = t.description or t.name + return fn + result.append({ + "name": tool.name.replace(" ", "_").lower(), + "description": tool.description or tool.name, + "fn": _make_fn(tool), + }) + return result + + def _send_to_socket(self, text: str) -> None: + send( + self.socket, + SocketEvents.MESSAGE, + SocketMessage( + room=self.session_id, + authorName="AG2", + message=Message( + chunkId=str(uuid.uuid4()), + text=text, + first=True, + tokens=1, + timestamp=int(datetime.now().timestamp() * 1000), + displayType="bubble", + ), + ), + "both", + ) + + def _send_stop(self) -> None: + send( + self.socket, + SocketEvents.STOP_GENERATING, + SocketMessage( + room=self.session_id, + authorName="AG2", + message=Message( + chunkId=str(uuid.uuid4()), + text="", + first=True, + tokens=0, + timestamp=int(datetime.now().timestamp() * 1000), + displayType="bubble", + ), + ), + "both", + ) diff --git a/agent-backend/src/ag2/streaming.py b/agent-backend/src/ag2/streaming.py new file mode 100644 index 000000000..2641dc607 --- /dev/null +++ b/agent-backend/src/ag2/streaming.py @@ -0,0 +1,50 @@ +""" +Socket.IO streaming hook for AG2 agents. +Injects a reply_func that forwards complete agent messages to the webapp. + +AG2 also supports token-level streaming via ConversableAgent.generate_reply hooks, +which would allow the webapp to render partial tokens as they arrive. That can be +enabled here once the platform's Socket.IO protocol is extended to support +incremental message chunks. +""" +from __future__ import annotations + +import uuid +from datetime import datetime +from typing import Any + +from socketio import SimpleClient + +from messaging.send_message_to_socket import send +from models.sockets import Message, SocketEvents, SocketMessage + + +def make_socket_reply_func(socket: SimpleClient, session_id: str): + """ + Returns an AG2 reply_func that emits each complete agent message over Socket.IO. + Register as: agent.register_reply(trigger=ConversableAgent, reply_func=fn, position=0) + """ + def reply_func(recipient: Any, messages: list, sender: Any, config: Any): + last = messages[-1] if messages else {} + content = last.get("content", "") + if content: + send( + socket, + SocketEvents.MESSAGE, + SocketMessage( + room=session_id, + authorName=recipient.name, + message=Message( + chunkId=str(uuid.uuid4()), + text=str(content), + first=True, + tokens=1, + timestamp=int(datetime.now().timestamp() * 1000), + displayType="bubble", + ), + ), + "both", + ) + return False, None # don't intercept — let AG2 continue normal reply flow + + return reply_func diff --git a/agent-backend/src/ag2/tool_adapter.py b/agent-backend/src/ag2/tool_adapter.py new file mode 100644 index 000000000..01c5adb59 --- /dev/null +++ b/agent-backend/src/ag2/tool_adapter.py @@ -0,0 +1,26 @@ +""" +Adapts AgentCloud platform tools to AG2-compatible Python functions. +Platform tools are expected as { name, description, fn } dicts. +AG2 tools are plain callables registered via register_for_llm / register_for_execution. + +AG2 also ships a native MCP client (from autogen.mcp import MCPClient, ag2[mcp] extra) +that connects to external MCP servers over SSE transport with no additional adapters. +This can be wired into AG2Builder in a follow-up once the platform surfaces MCP server +configuration in the app config UI. +""" +from __future__ import annotations + +from typing import Callable + + +def build_ag2_tools(tool_configs: list[dict]) -> list[tuple[Callable, dict]]: + """ + Convert platform tool configs to (callable, metadata) tuples for AG2 registration. + Each entry in tool_configs must have keys: name, description, fn. + """ + result = [] + for tc in tool_configs: + fn = tc["fn"] + meta = {"name": tc["name"], "description": tc["description"]} + result.append((fn, meta)) + return result diff --git a/agent-backend/src/messaging/client.py b/agent-backend/src/messaging/client.py index de99973e9..a00b145a8 100644 --- a/agent-backend/src/messaging/client.py +++ b/agent-backend/src/messaging/client.py @@ -5,6 +5,7 @@ from crew.exceptions import CrewAIBuilderException from crew.get_crew_components import construct_crew, looping_app, session_terminated from chat import ChatAssistant +from ag2.builder import AG2Builder from models.mongo import AppType from utils.log_exception_context_manager import log_exception from bullmq import Worker, Job @@ -14,8 +15,13 @@ async def process(job: Job, token: str): print(f'Running session ID: {job.data.get("sessionId")}') - # Send job to the correct executor based on the job type - target = execute_chat_task if job.data.get('type') == AppType.CHAT else execute_task + job_type = job.data.get('type') + if job_type == AppType.CHAT: + target = execute_chat_task + elif job_type == AppType.AG2: + target = execute_ag2_task + else: + target = execute_task thread = threading.Thread(target=target, args=[job.data]) thread.start() return True @@ -51,6 +57,13 @@ def execute_task(data: dict): crew_builder.run_crew() +def execute_ag2_task(data: dict): + with log_exception(): + session_id = data.get("sessionId") + builder = AG2Builder(session_id) + builder.run(data.get("message", "")) + + def execute_chat_task(data: dict): chat = ChatAssistant(data.get('sessionId')) chat.run() diff --git a/agent-backend/src/models/mongo.py b/agent-backend/src/models/mongo.py index 96bbe7eca..f8051975e 100644 --- a/agent-backend/src/models/mongo.py +++ b/agent-backend/src/models/mongo.py @@ -308,6 +308,7 @@ class Datasource(BaseModel): class AppType(str, Enum): CHAT = "chat" PROCESS = "process" + AG2 = "ag2" class ChatAppConfig(BaseModel): diff --git a/agent-backend/src/test/test_ag2/__init__.py b/agent-backend/src/test/test_ag2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/agent-backend/src/test/test_ag2/conftest.py b/agent-backend/src/test/test_ag2/conftest.py new file mode 100644 index 000000000..3f934c393 --- /dev/null +++ b/agent-backend/src/test/test_ag2/conftest.py @@ -0,0 +1,49 @@ +""" +Pre-mock agentcloud infrastructure modules so ag2.builder can be imported +without a live MongoDB, socket, or full agentcloud stack. +""" +import sys +from enum import Enum +from types import ModuleType +from unittest.mock import MagicMock + + +class _AppType(str, Enum): + AG2 = "ag2" + CREW = "crew" + LANGGRAPH = "langgraph" + + +def _stub_module(name: str, **attrs) -> ModuleType: + m = ModuleType(name) + for k, v in attrs.items(): + setattr(m, k, v) + sys.modules[name] = m + return m + + +# Stub out agentcloud modules that ag2.builder imports at module level +_stub_module("mongo") +_stub_module("mongo.queries", MongoClientConnection=MagicMock()) +_stub_module("init") +_stub_module("init.mongo_session", start_mongo_session=MagicMock()) +_stub_module("init.env_variables", + AGENT_BACKEND_SOCKET_TOKEN="test-token", + SOCKET_URL="http://localhost:9999") +_stub_module("models") +_stub_module("models.mongo", Agent=MagicMock(), Model=MagicMock(), Session=MagicMock(), AppType=_AppType) +_stub_module("models.sockets", + Message=MagicMock(), + SocketEvents=MagicMock(), + SocketMessage=MagicMock()) +_stub_module("socketio", SimpleClient=MagicMock()) +_stub_module("socketio.exceptions", ConnectionError=ConnectionError) +_stub_module("messaging") +_stub_module("messaging.send_message_to_socket", send=MagicMock()) + +# Pre-import local ag2 submodules so patch("ag2.builder.*") can resolve them. +# Python's pkgutil.resolve_name("ag2.builder") does __import__("ag2") then +# getattr(ag2, "builder") — that getattr only works if ag2.builder was already +# imported. The root conftest has already inserted src/ into sys.path. +import ag2.builder # noqa: E402 (must come after stubs) +import ag2.streaming # noqa: E402 diff --git a/agent-backend/src/test/test_ag2/test_builder.py b/agent-backend/src/test/test_ag2/test_builder.py new file mode 100644 index 000000000..fe16849b0 --- /dev/null +++ b/agent-backend/src/test/test_ag2/test_builder.py @@ -0,0 +1,208 @@ +""" +Unit tests for ag2/builder.py — no LLM calls, no network, no MongoDB. +All external dependencies (mongo, socket, AG2 agents) are mocked. +""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch, PropertyMock +import pytest + + +# --------------------------------------------------------------------------- +# Helpers to build minimal mock objects that mirror the MongoDB models +# --------------------------------------------------------------------------- + +def _make_agent(name="researcher", role="Researcher", goal="Find information.", backstory="Expert."): + agent = MagicMock() + agent.name = name + agent.role = role + agent.goal = goal + agent.backstory = backstory + agent.modelId = "model-id-123" + agent.toolIds = [] + return agent + + +def _make_model(model_name="gpt-4o-mini", api_key="test-key", temperature=0.3): + model = MagicMock() + model.config = {"model": model_name, "api_key": api_key, "base_url": "https://api.openai.com/v1"} + model.temperature = temperature + return model + + +def _make_crew(ag2_mode="group_chat"): + crew = MagicMock() + crew.config = {"ag2_mode": ag2_mode} + return crew + + +# --------------------------------------------------------------------------- +# Fixture: patch all external dependencies before AG2Builder is instantiated +# --------------------------------------------------------------------------- + +@pytest.fixture() +def builder(request): + """ + Returns a fully constructed AG2Builder with mocked MongoDB and socket. + Pass `ag2_mode` via pytest.mark.parametrize or request.param (default: "group_chat"). + """ + mode = getattr(request, "param", "group_chat") + agent = _make_agent() + crew = _make_crew(mode) + model = _make_model() + + mock_mongo = MagicMock() + mock_mongo.get_session.return_value = MagicMock() + mock_mongo.get_crew.return_value = (MagicMock(), crew, [], [agent]) + mock_mongo.get_single_model_by_id.return_value = model + + mock_socket = MagicMock() + mock_socket.connected = True + + with ( + patch("ag2.builder.start_mongo_session", return_value=mock_mongo), + patch("ag2.builder._mongo", mock_mongo), + patch("ag2.builder.SimpleClient", return_value=mock_socket), + patch("ag2.builder.LLMConfig") as mock_llm_config, + ): + mock_llm_config.return_value = MagicMock() + from ag2.builder import AG2Builder + b = AG2Builder.__new__(AG2Builder) + b.session_id = "session-test" + b.socket = mock_socket + b.agents_config = [agent] + b.ag2_mode = mode + b.llm_config = mock_llm_config.return_value + yield b + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +class TestAG2BuilderDispatch: + @pytest.mark.parametrize("mode,method", [ + ("two_agent", "_run_two_agent"), + ("group_chat", "_run_group_chat"), + ("reasoning", "_run_reasoning"), + ]) + def test_run_dispatches_to_correct_handler(self, mode, method): + agent = _make_agent() + crew = _make_crew(mode) + model = _make_model() + mock_mongo = MagicMock() + mock_mongo.get_session.return_value = MagicMock() + mock_mongo.get_crew.return_value = (MagicMock(), crew, [], [agent]) + mock_mongo.get_single_model_by_id.return_value = model + mock_socket = MagicMock() + + with ( + patch("ag2.builder.start_mongo_session", return_value=mock_mongo), + patch("ag2.builder._mongo", mock_mongo), + patch("ag2.builder.SimpleClient", return_value=mock_socket), + patch("ag2.builder.LLMConfig"), + ): + from ag2.builder import AG2Builder + b = AG2Builder.__new__(AG2Builder) + b.session_id = "s" + b.socket = mock_socket + b.agents_config = [agent] + b.ag2_mode = mode + b.llm_config = MagicMock() + + handler = MagicMock() + with patch.object(b, method, handler): + with patch.object(b, "_send_stop"): + b.run("test message") + handler.assert_called_once_with("test message") + + def test_unknown_mode_falls_back_to_group_chat(self): + agent = _make_agent() + mock_mongo = MagicMock() + mock_socket = MagicMock() + crew = _make_crew("nonexistent_mode") + mock_mongo.get_session.return_value = MagicMock() + mock_mongo.get_crew.return_value = (MagicMock(), crew, [], [agent]) + mock_mongo.get_single_model_by_id.return_value = _make_model() + + with ( + patch("ag2.builder.start_mongo_session", return_value=mock_mongo), + patch("ag2.builder._mongo", mock_mongo), + patch("ag2.builder.SimpleClient", return_value=mock_socket), + patch("ag2.builder.LLMConfig"), + ): + from ag2.builder import AG2Builder + b = AG2Builder.__new__(AG2Builder) + b.session_id = "s" + b.socket = mock_socket + b.agents_config = [agent] + b.ag2_mode = "nonexistent_mode" + b.llm_config = MagicMock() + + group_chat_called = [] + with patch.object(b, "_run_group_chat", side_effect=lambda m: group_chat_called.append(m)): + with patch.object(b, "_send_stop"): + b.run("hello") + assert group_chat_called == ["hello"] + + def test_send_stop_always_called(self, builder): + """_send_stop must fire even when the handler raises.""" + with patch.object(builder, "_run_group_chat", side_effect=RuntimeError("boom")): + with patch.object(builder, "_send_stop") as mock_stop: + with patch.object(builder, "_send_to_socket"): + builder.run("test") + mock_stop.assert_called_once() + + +class TestAG2BuilderLLMConfig: + def test_llm_config_reads_from_model_config(self): + agent = _make_agent() + crew = _make_crew("group_chat") + model = _make_model(model_name="gpt-4o", api_key="sk-secret", temperature=0.5) + mock_mongo = MagicMock() + mock_mongo.get_session.return_value = MagicMock() + mock_mongo.get_crew.return_value = (MagicMock(), crew, [], [agent]) + mock_mongo.get_single_model_by_id.return_value = model + mock_socket = MagicMock() + + captured_args = {} + + def capture_llm_config(*args, **kwargs): + captured_args["args"] = args + captured_args["kwargs"] = kwargs + return MagicMock() + + with ( + patch("ag2.builder.start_mongo_session", return_value=mock_mongo), + patch("ag2.builder._mongo", mock_mongo), + patch("ag2.builder.SimpleClient", return_value=mock_socket), + patch("ag2.builder.LLMConfig", side_effect=capture_llm_config), + patch("ag2.builder.AG2Builder._init_socket"), + ): + from ag2.builder import AG2Builder + b = AG2Builder.__new__(AG2Builder) + b.session_id = "s" + b.socket = mock_socket + b._init_app_state() + + config_dict = captured_args["args"][0] + assert config_dict["model"] == "gpt-4o" + assert config_dict["api_key"] == "sk-secret" + assert captured_args["kwargs"]["temperature"] == 0.5 + + +class TestAG2BuilderPlatformTools: + def test_platform_tools_empty_when_no_tool_ids(self, builder): + builder.agents_config[0].toolIds = [] + assert builder._platform_tools() == [] + + def test_platform_tools_empty_when_no_agents(self, builder): + builder.agents_config = [] + assert builder._platform_tools() == [] + + +class TestAppTypeEnum: + def test_ag2_app_type_value(self): + from models.mongo import AppType + assert AppType.AG2 == "ag2" + assert AppType.AG2 in list(AppType) diff --git a/agent-backend/src/test/test_ag2/test_streaming.py b/agent-backend/src/test/test_ag2/test_streaming.py new file mode 100644 index 000000000..08b4461ba --- /dev/null +++ b/agent-backend/src/test/test_ag2/test_streaming.py @@ -0,0 +1,94 @@ +"""Unit tests for ag2/streaming.py — no network, no AG2 LLM calls.""" +from unittest.mock import MagicMock, patch +import pytest + +# Patch send and models before importing the module under test +with patch("ag2.streaming.send"), patch("ag2.streaming.SocketEvents"), patch("ag2.streaming.SocketMessage"), patch("ag2.streaming.Message"): + from ag2.streaming import make_socket_reply_func + + +@pytest.fixture() +def socket(): + return MagicMock() + + +@pytest.fixture() +def recipient(): + m = MagicMock() + m.name = "assistant" + return m + + +class TestMakeSocketReplyFunc: + def test_returns_callable(self, socket): + with patch("ag2.streaming.send"), patch("ag2.streaming.SocketEvents"), \ + patch("ag2.streaming.SocketMessage"), patch("ag2.streaming.Message"): + fn = make_socket_reply_func(socket, "s1") + assert callable(fn) + + def test_reply_func_signature_returns_false_none(self, socket, recipient): + """reply_func must return (False, None) so AG2 continues its normal flow.""" + with ( + patch("ag2.streaming.send"), + patch("ag2.streaming.SocketEvents"), + patch("ag2.streaming.SocketMessage"), + patch("ag2.streaming.Message"), + ): + fn = make_socket_reply_func(socket, "s1") + result = fn(recipient, [{"content": "Hello!"}], None, None) + assert result == (False, None) + + def test_emits_on_non_empty_content(self, socket, recipient): + with ( + patch("ag2.streaming.send") as mock_send, + patch("ag2.streaming.SocketEvents"), + patch("ag2.streaming.SocketMessage"), + patch("ag2.streaming.Message"), + ): + fn = make_socket_reply_func(socket, "session-abc") + fn(recipient, [{"content": "Here is the answer."}], None, None) + + mock_send.assert_called_once() + + def test_silent_on_empty_content(self, socket, recipient): + with ( + patch("ag2.streaming.send") as mock_send, + patch("ag2.streaming.SocketEvents"), + patch("ag2.streaming.SocketMessage"), + patch("ag2.streaming.Message"), + ): + fn = make_socket_reply_func(socket, "session-abc") + fn(recipient, [{"content": ""}], None, None) + + mock_send.assert_not_called() + + def test_silent_on_empty_messages(self, socket, recipient): + with ( + patch("ag2.streaming.send") as mock_send, + patch("ag2.streaming.SocketEvents"), + patch("ag2.streaming.SocketMessage"), + patch("ag2.streaming.Message"), + ): + fn = make_socket_reply_func(socket, "session-abc") + fn(recipient, [], None, None) + + mock_send.assert_not_called() + + def test_uses_recipient_name_as_author(self, socket): + recipient = MagicMock() + recipient.name = "researcher" + captured = {} + + def capture_send(sock, event, msg, mode): + captured["msg"] = msg + + with ( + patch("ag2.streaming.send", side_effect=capture_send), + patch("ag2.streaming.SocketEvents"), + patch("ag2.streaming.SocketMessage", side_effect=lambda **kw: kw) as mock_sm, + patch("ag2.streaming.Message"), + ): + fn = make_socket_reply_func(socket, "session-abc") + fn(recipient, [{"content": "Found it."}], None, None) + + assert mock_sm.call_args.kwargs["authorName"] == "researcher" diff --git a/agent-backend/src/test/test_ag2/test_tool_adapter.py b/agent-backend/src/test/test_ag2/test_tool_adapter.py new file mode 100644 index 000000000..10763745b --- /dev/null +++ b/agent-backend/src/test/test_ag2/test_tool_adapter.py @@ -0,0 +1,40 @@ +"""Unit tests for ag2/tool_adapter.py — no imports beyond stdlib.""" +from ag2.tool_adapter import build_ag2_tools + + +class TestBuildAg2Tools: + def test_empty_input_returns_empty_list(self): + assert build_ag2_tools([]) == [] + + def test_returns_list_of_tuples(self): + fn = lambda q: "result" + configs = [{"name": "search", "description": "Search the web.", "fn": fn}] + result = build_ag2_tools(configs) + assert len(result) == 1 + assert isinstance(result[0], tuple) + assert len(result[0]) == 2 + + def test_callable_is_preserved(self): + fn = lambda q: "result" + configs = [{"name": "search", "description": "Search the web.", "fn": fn}] + result_fn, _ = build_ag2_tools(configs)[0] + assert result_fn is fn + + def test_metadata_keys_are_correct(self): + fn = lambda q: "result" + configs = [{"name": "search", "description": "Search the web.", "fn": fn}] + _, meta = build_ag2_tools(configs)[0] + assert meta["name"] == "search" + assert meta["description"] == "Search the web." + + def test_multiple_tools_all_returned(self): + fn1 = lambda q: "r1" + fn2 = lambda q: "r2" + configs = [ + {"name": "tool_a", "description": "A tool.", "fn": fn1}, + {"name": "tool_b", "description": "B tool.", "fn": fn2}, + ] + result = build_ag2_tools(configs) + assert len(result) == 2 + assert result[0][0] is fn1 + assert result[1][0] is fn2 diff --git a/webapp/src/components/CrewAppForm2.tsx b/webapp/src/components/CrewAppForm2.tsx index a0c486a4f..374311814 100644 --- a/webapp/src/components/CrewAppForm2.tsx +++ b/webapp/src/components/CrewAppForm2.tsx @@ -103,7 +103,10 @@ export default function CrewAppForm({ const [isEditing, setIsEditing] = useState(false); const [hasLaunched, setHasLaunched] = useState(false); - const [appName, setAppName] = useState(app?.name || 'Untitled Crew App'); + const [appName, setAppName] = useState(app?.name || 'Untitled Process App'); + const [framework, setFramework] = useState<'ag2' | 'crew'>( + app?.type === AppType.CREW ? 'crew' : 'ag2' + ); function getInitialData(initData) { const { agents, tasks } = initData; @@ -194,7 +197,7 @@ export default function CrewAppForm({ managerModelId: managerModel?.value, tasks: tasksState.map(x => x.value), iconId: icon?.id, - type: AppType.CREW, + type: framework === 'ag2' ? AppType.AG2 : AppType.CREW, run, sharingMode, sharingEmails: sharingEmailState.map(x => x?.label.trim()).filter(x => x), @@ -386,7 +389,7 @@ export default function CrewAppForm({ >

Create App

> -

Crew App

+

Process App

{hasLaunched ? ( @@ -445,6 +448,47 @@ export default function CrewAppForm({ /> +
+ +
+ + +
+
diff --git a/webapp/src/controllers/app.ts b/webapp/src/controllers/app.ts index e359b2e22..478a1de98 100644 --- a/webapp/src/controllers/app.ts +++ b/webapp/src/controllers/app.ts @@ -25,7 +25,7 @@ import createAccount from 'lib/account/create'; import { chainValidations } from 'lib/utils/validationutils'; import toObjectId from 'misc/toobjectid'; import { ObjectId } from 'mongodb'; -import { AppType } from 'struct/app'; +import { AppType, isProcessApp } from 'struct/app'; import { IconAttachment } from 'struct/asset'; import { CollectionName } from 'struct/db'; import { ChatAppAllowedModels } from 'struct/model'; @@ -220,7 +220,7 @@ export async function addAppApi(req, res, next) { }, { field: 'type', - validation: { notEmpty: true, inSet: new Set([AppType.CHAT, AppType.CREW]) } + validation: { notEmpty: true, inSet: new Set([AppType.CHAT, AppType.CREW, AppType.AG2]) } }, // { field: 'name', validation: { notEmpty: true, ofType: 'string' } }, // { field: 'description', validation: { notEmpty: true, ofType: 'string' } }, @@ -312,7 +312,7 @@ export async function addAppApi(req, res, next) { ); let addedCrew, chatAgent; - if ((type as AppType) === AppType.CREW) { + if (isProcessApp(type as AppType)) { addedCrew = await addCrew({ orgId: res.locals.matchingOrg.id, teamId: toObjectId(req.params.resourceSlug), @@ -364,7 +364,7 @@ export async function addAppApi(req, res, next) { } : null, type, - ...((type as AppType) === AppType.CREW + ...(isProcessApp(type as AppType) ? { crewId: addedCrew ? addedCrew.insertedId : null, memory: memory === true, @@ -604,7 +604,7 @@ export async function editAppApi(req, res, next) { description, tags: (tags || []).map(tag => tag.trim()).filter(x => x), icon: iconId ? attachedIconToApp : null, - ...(app.type === AppType.CREW + ...(isProcessApp(app.type) ? { memory: memory === true, cache: cache === true diff --git a/webapp/src/controllers/session.ts b/webapp/src/controllers/session.ts index 361b929b0..e573d7c7e 100644 --- a/webapp/src/controllers/session.ts +++ b/webapp/src/controllers/session.ts @@ -35,7 +35,7 @@ import { ObjectId } from 'mongodb'; import { sessionTaskQueue } from 'queue/bull'; import { client } from 'redis/redis'; import { v4 as uuidv4 } from 'uuid'; -import { App, AppType } from 'struct/app'; +import { App, AppType, isProcessApp } from 'struct/app'; import { SessionStatus } from 'struct/session'; import { Variable } from 'struct/variable'; import { chainValidations } from 'utils/validationutils'; @@ -84,6 +84,7 @@ export async function sessionData(req, res, _next) { let avatarMap = {}; switch (app?.type) { case AppType.CREW: + case AppType.AG2: const foundCrew = await getCrewById(req.params.resourceSlug, app?.crewId); const taskPromises = foundCrew.tasks.map(t => @@ -162,6 +163,7 @@ export async function publicSessionData(req, res, _next) { let avatarMap = {}; switch (app?.type) { case AppType.CREW: + case AppType.AG2: const foundCrew = await unsafeGetCrewById(app?.crewId); avatarMap = await unsafeGetAgentNameMap(foundCrew?.agents); break; @@ -358,7 +360,7 @@ export async function addSessionApi(req, res, next) { let crewId; let hasVariables = false; - if (app?.type === AppType.CREW) { + if (isProcessApp(app?.type)) { const crew = await getCrewById(req.params.resourceSlug, app?.crewId); if (!crew) { return dynamicResponse(req, res, 400, { error: 'Invalid inputs' }); diff --git a/webapp/src/controllers/sharelink.ts b/webapp/src/controllers/sharelink.ts index 7a65a43e0..55832438a 100644 --- a/webapp/src/controllers/sharelink.ts +++ b/webapp/src/controllers/sharelink.ts @@ -10,7 +10,7 @@ import { getTaskById } from 'db/task'; import { chainValidations } from 'lib/utils/validationutils'; import toObjectId from 'misc/toobjectid'; import { sessionTaskQueue } from 'queue/bull'; -import { App, AppType } from 'struct/app'; +import { App, AppType, isProcessApp } from 'struct/app'; import { SessionStatus } from 'struct/session'; import { ShareLinkTypes } from 'struct/sharelink'; import { SharingMode } from 'struct/sharing'; @@ -66,7 +66,7 @@ export async function handleRedirect(req, res, next) { let crewId; let hasVariables = false; - if (app?.type === AppType.CREW) { + if (isProcessApp(app?.type)) { const crew = await getCrewById(req.params.resourceSlug, app?.crewId); if (!crew) { return dynamicResponse(req, res, 400, { error: 'Invalid inputs' }); diff --git a/webapp/src/lib/struct/app.ts b/webapp/src/lib/struct/app.ts index 0c6b24675..f19a25010 100644 --- a/webapp/src/lib/struct/app.ts +++ b/webapp/src/lib/struct/app.ts @@ -6,7 +6,12 @@ import { SharingConfig } from 'struct/sharing'; export enum AppType { CHAT = 'chat', - CREW = 'crew' + CREW = 'crew', + AG2 = 'ag2' +} + +export function isProcessApp(type: AppType | string): boolean { + return type === AppType.CREW || type === AppType.AG2; } export type ChatAppConfig = { diff --git a/webapp/src/pages/[resourceSlug]/app/add.tsx b/webapp/src/pages/[resourceSlug]/app/add.tsx index 014ad439b..9252fa123 100644 --- a/webapp/src/pages/[resourceSlug]/app/add.tsx +++ b/webapp/src/pages/[resourceSlug]/app/add.tsx @@ -25,7 +25,8 @@ const chatAppTaglines = [ ]; const processAppTaglines = [ - 'Build Multi-Agent Process Apps (powered by Crew AI)', + 'Build Multi-Agent Process Apps', + 'Choose AG2 (AutoGen) or Crew AI orchestration', 'Integrate RAG datasources', 'Add custom code tools', 'Add tasks', diff --git a/webapp/src/pages/[resourceSlug]/apps.tsx b/webapp/src/pages/[resourceSlug]/apps.tsx index d99d64aed..7312a0d57 100644 --- a/webapp/src/pages/[resourceSlug]/apps.tsx +++ b/webapp/src/pages/[resourceSlug]/apps.tsx @@ -37,7 +37,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Blockies from 'react-blockies'; import { useAgentStore } from 'store/agent'; -import { App, AppType } from 'struct/app'; +import { App, AppType, isProcessApp } from 'struct/app'; import { SharingMode } from 'struct/sharing'; import { toast } from 'react-toastify'; import ChatAppForm2 from 'components/ChatAppForm2'; @@ -277,7 +277,7 @@ export default function Apps(props) { setOpenDeleteDialog={setOpenDeleteDialog} onDelete={() => deleteApp(selectedAgentId)} /> - {selectedApp?.type === 'crew' ? ( + {isProcessApp(selectedApp?.type) ? ( { const prevMessage = mi > 0 ? marr[mi - 1] : null; - if (m?.isFeedback && app?.type === AppType.CREW) { + if (m?.isFeedback && isProcessApp(app?.type)) { return null; } const authorName = m?.authorName || m?.message?.authorName;