agno 0.1.2__py3-none-any.whl → 2.3.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/__init__.py +8 -0
- agno/agent/__init__.py +44 -5
- agno/agent/agent.py +10531 -2975
- agno/api/agent.py +14 -53
- agno/api/api.py +7 -46
- agno/api/evals.py +22 -0
- agno/api/os.py +17 -0
- agno/api/routes.py +6 -25
- agno/api/schemas/__init__.py +9 -0
- agno/api/schemas/agent.py +6 -9
- agno/api/schemas/evals.py +16 -0
- agno/api/schemas/os.py +14 -0
- agno/api/schemas/team.py +10 -10
- agno/api/schemas/utils.py +21 -0
- agno/api/schemas/workflows.py +16 -0
- agno/api/settings.py +53 -0
- agno/api/team.py +22 -26
- agno/api/workflow.py +28 -0
- agno/cloud/aws/base.py +214 -0
- agno/cloud/aws/s3/__init__.py +2 -0
- agno/cloud/aws/s3/api_client.py +43 -0
- agno/cloud/aws/s3/bucket.py +195 -0
- agno/cloud/aws/s3/object.py +57 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/__init__.py +24 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +946 -0
- agno/db/dynamo/__init__.py +3 -0
- agno/db/dynamo/dynamo.py +2781 -0
- agno/db/dynamo/schemas.py +442 -0
- agno/db/dynamo/utils.py +743 -0
- agno/db/firestore/__init__.py +3 -0
- agno/db/firestore/firestore.py +2379 -0
- agno/db/firestore/schemas.py +181 -0
- agno/db/firestore/utils.py +376 -0
- agno/db/gcs_json/__init__.py +3 -0
- agno/db/gcs_json/gcs_json_db.py +1791 -0
- agno/db/gcs_json/utils.py +228 -0
- agno/db/in_memory/__init__.py +3 -0
- agno/db/in_memory/in_memory_db.py +1312 -0
- agno/db/in_memory/utils.py +230 -0
- agno/db/json/__init__.py +3 -0
- agno/db/json/json_db.py +1777 -0
- agno/db/json/utils.py +230 -0
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +635 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +17 -0
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +2597 -0
- agno/db/mongo/schemas.py +119 -0
- agno/db/mongo/utils.py +276 -0
- agno/db/mysql/__init__.py +4 -0
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +2923 -0
- agno/db/mysql/schemas.py +186 -0
- agno/db/mysql/utils.py +488 -0
- agno/db/postgres/__init__.py +4 -0
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +2870 -0
- agno/db/postgres/schemas.py +187 -0
- agno/db/postgres/utils.py +442 -0
- agno/db/redis/__init__.py +3 -0
- agno/db/redis/redis.py +2141 -0
- agno/db/redis/schemas.py +159 -0
- agno/db/redis/utils.py +346 -0
- agno/db/schemas/__init__.py +4 -0
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +34 -0
- agno/db/schemas/knowledge.py +40 -0
- agno/db/schemas/memory.py +61 -0
- agno/db/singlestore/__init__.py +3 -0
- agno/db/singlestore/schemas.py +179 -0
- agno/db/singlestore/singlestore.py +2877 -0
- agno/db/singlestore/utils.py +384 -0
- agno/db/sqlite/__init__.py +4 -0
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +181 -0
- agno/db/sqlite/sqlite.py +2908 -0
- agno/db/sqlite/utils.py +429 -0
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +118 -0
- agno/eval/__init__.py +24 -0
- agno/eval/accuracy.py +666 -276
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +779 -0
- agno/eval/reliability.py +241 -62
- agno/eval/utils.py +120 -0
- agno/exceptions.py +143 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/__init__.py +3 -0
- agno/integrations/discord/client.py +203 -0
- agno/knowledge/__init__.py +5 -1
- agno/{document → knowledge}/chunking/agentic.py +22 -14
- agno/{document → knowledge}/chunking/document.py +2 -2
- agno/{document → knowledge}/chunking/fixed.py +7 -6
- agno/knowledge/chunking/markdown.py +151 -0
- agno/{document → knowledge}/chunking/recursive.py +15 -3
- agno/knowledge/chunking/row.py +39 -0
- agno/knowledge/chunking/semantic.py +91 -0
- agno/knowledge/chunking/strategy.py +165 -0
- agno/knowledge/content.py +74 -0
- agno/knowledge/document/__init__.py +5 -0
- agno/{document → knowledge/document}/base.py +12 -2
- agno/knowledge/embedder/__init__.py +5 -0
- agno/knowledge/embedder/aws_bedrock.py +343 -0
- agno/knowledge/embedder/azure_openai.py +210 -0
- agno/{embedder → knowledge/embedder}/base.py +8 -0
- agno/knowledge/embedder/cohere.py +323 -0
- agno/knowledge/embedder/fastembed.py +62 -0
- agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
- agno/knowledge/embedder/google.py +258 -0
- agno/knowledge/embedder/huggingface.py +94 -0
- agno/knowledge/embedder/jina.py +182 -0
- agno/knowledge/embedder/langdb.py +22 -0
- agno/knowledge/embedder/mistral.py +206 -0
- agno/knowledge/embedder/nebius.py +13 -0
- agno/knowledge/embedder/ollama.py +154 -0
- agno/knowledge/embedder/openai.py +195 -0
- agno/knowledge/embedder/sentence_transformer.py +63 -0
- agno/{embedder → knowledge/embedder}/together.py +1 -1
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +165 -0
- agno/knowledge/knowledge.py +3006 -0
- agno/knowledge/reader/__init__.py +7 -0
- agno/knowledge/reader/arxiv_reader.py +81 -0
- agno/knowledge/reader/base.py +95 -0
- agno/knowledge/reader/csv_reader.py +164 -0
- agno/knowledge/reader/docx_reader.py +82 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/firecrawl_reader.py +201 -0
- agno/knowledge/reader/json_reader.py +88 -0
- agno/knowledge/reader/markdown_reader.py +137 -0
- agno/knowledge/reader/pdf_reader.py +431 -0
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +313 -0
- agno/knowledge/reader/s3_reader.py +89 -0
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +127 -0
- agno/knowledge/reader/web_search_reader.py +325 -0
- agno/knowledge/reader/website_reader.py +455 -0
- agno/knowledge/reader/wikipedia_reader.py +91 -0
- agno/knowledge/reader/youtube_reader.py +78 -0
- agno/knowledge/remote_content/remote_content.py +88 -0
- agno/knowledge/reranker/__init__.py +3 -0
- agno/{reranker → knowledge/reranker}/base.py +1 -1
- agno/{reranker → knowledge/reranker}/cohere.py +2 -2
- agno/knowledge/reranker/infinity.py +195 -0
- agno/knowledge/reranker/sentence_transformer.py +54 -0
- agno/knowledge/types.py +39 -0
- agno/knowledge/utils.py +234 -0
- agno/media.py +439 -95
- agno/memory/__init__.py +16 -3
- agno/memory/manager.py +1474 -123
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/__init__.py +5 -0
- agno/models/aimlapi/aimlapi.py +62 -0
- agno/models/anthropic/__init__.py +4 -0
- agno/models/anthropic/claude.py +960 -496
- agno/models/aws/__init__.py +15 -0
- agno/models/aws/bedrock.py +686 -451
- agno/models/aws/claude.py +190 -183
- agno/models/azure/__init__.py +18 -1
- agno/models/azure/ai_foundry.py +489 -0
- agno/models/azure/openai_chat.py +89 -40
- agno/models/base.py +2477 -550
- agno/models/cerebras/__init__.py +12 -0
- agno/models/cerebras/cerebras.py +565 -0
- agno/models/cerebras/cerebras_openai.py +131 -0
- agno/models/cohere/__init__.py +4 -0
- agno/models/cohere/chat.py +306 -492
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +74 -0
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +90 -0
- agno/models/deepinfra/__init__.py +5 -0
- agno/models/deepinfra/deepinfra.py +45 -0
- agno/models/deepseek/__init__.py +4 -0
- agno/models/deepseek/deepseek.py +110 -9
- agno/models/fireworks/__init__.py +4 -0
- agno/models/fireworks/fireworks.py +19 -22
- agno/models/google/__init__.py +3 -7
- agno/models/google/gemini.py +1717 -662
- agno/models/google/utils.py +22 -0
- agno/models/groq/__init__.py +4 -0
- agno/models/groq/groq.py +391 -666
- agno/models/huggingface/__init__.py +4 -0
- agno/models/huggingface/huggingface.py +266 -538
- agno/models/ibm/__init__.py +5 -0
- agno/models/ibm/watsonx.py +432 -0
- agno/models/internlm/__init__.py +3 -0
- agno/models/internlm/internlm.py +20 -3
- agno/models/langdb/__init__.py +1 -0
- agno/models/langdb/langdb.py +60 -0
- agno/models/litellm/__init__.py +14 -0
- agno/models/litellm/chat.py +503 -0
- agno/models/litellm/litellm_openai.py +42 -0
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/lmstudio/__init__.py +5 -0
- agno/models/lmstudio/lmstudio.py +25 -0
- agno/models/message.py +361 -39
- agno/models/meta/__init__.py +12 -0
- agno/models/meta/llama.py +502 -0
- agno/models/meta/llama_openai.py +79 -0
- agno/models/metrics.py +120 -0
- agno/models/mistral/__init__.py +4 -0
- agno/models/mistral/mistral.py +293 -393
- agno/models/nebius/__init__.py +3 -0
- agno/models/nebius/nebius.py +53 -0
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/__init__.py +4 -0
- agno/models/nvidia/nvidia.py +22 -3
- agno/models/ollama/__init__.py +4 -2
- agno/models/ollama/chat.py +257 -492
- agno/models/openai/__init__.py +7 -0
- agno/models/openai/chat.py +725 -770
- agno/models/openai/like.py +16 -2
- agno/models/openai/responses.py +1121 -0
- agno/models/openrouter/__init__.py +4 -0
- agno/models/openrouter/openrouter.py +62 -5
- agno/models/perplexity/__init__.py +5 -0
- agno/models/perplexity/perplexity.py +203 -0
- agno/models/portkey/__init__.py +3 -0
- agno/models/portkey/portkey.py +82 -0
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +69 -0
- agno/models/response.py +177 -7
- agno/models/sambanova/__init__.py +4 -0
- agno/models/sambanova/sambanova.py +23 -4
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +42 -0
- agno/models/together/__init__.py +4 -0
- agno/models/together/together.py +21 -164
- agno/models/utils.py +266 -0
- agno/models/vercel/__init__.py +3 -0
- agno/models/vercel/v0.py +43 -0
- agno/models/vertexai/__init__.py +0 -1
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/__init__.py +3 -0
- agno/models/vllm/vllm.py +83 -0
- agno/models/xai/__init__.py +2 -0
- agno/models/xai/xai.py +111 -7
- agno/os/__init__.py +3 -0
- agno/os/app.py +1027 -0
- agno/os/auth.py +244 -0
- agno/os/config.py +126 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +249 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/__init__.py +3 -0
- agno/os/interfaces/agui/agui.py +47 -0
- agno/os/interfaces/agui/router.py +147 -0
- agno/os/interfaces/agui/utils.py +574 -0
- agno/os/interfaces/base.py +25 -0
- agno/os/interfaces/slack/__init__.py +3 -0
- agno/os/interfaces/slack/router.py +148 -0
- agno/os/interfaces/slack/security.py +30 -0
- agno/os/interfaces/slack/slack.py +47 -0
- agno/os/interfaces/whatsapp/__init__.py +3 -0
- agno/os/interfaces/whatsapp/router.py +210 -0
- agno/os/interfaces/whatsapp/security.py +55 -0
- agno/os/interfaces/whatsapp/whatsapp.py +36 -0
- agno/os/mcp.py +293 -0
- agno/os/middleware/__init__.py +9 -0
- agno/os/middleware/jwt.py +797 -0
- agno/os/router.py +258 -0
- agno/os/routers/__init__.py +3 -0
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/__init__.py +3 -0
- agno/os/routers/evals/evals.py +450 -0
- agno/os/routers/evals/schemas.py +174 -0
- agno/os/routers/evals/utils.py +231 -0
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/__init__.py +3 -0
- agno/os/routers/knowledge/knowledge.py +1008 -0
- agno/os/routers/knowledge/schemas.py +178 -0
- agno/os/routers/memory/__init__.py +3 -0
- agno/os/routers/memory/memory.py +661 -0
- agno/os/routers/memory/schemas.py +88 -0
- agno/os/routers/metrics/__init__.py +3 -0
- agno/os/routers/metrics/metrics.py +190 -0
- agno/os/routers/metrics/schemas.py +47 -0
- agno/os/routers/session/__init__.py +3 -0
- agno/os/routers/session/session.py +997 -0
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +534 -0
- agno/os/scopes.py +469 -0
- agno/{playground → os}/settings.py +7 -15
- agno/os/utils.py +973 -0
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +67 -0
- agno/reasoning/deepseek.py +63 -0
- agno/reasoning/default.py +97 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +71 -0
- agno/reasoning/helpers.py +24 -1
- agno/reasoning/ollama.py +67 -0
- agno/reasoning/openai.py +86 -0
- agno/reasoning/step.py +2 -1
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +822 -0
- agno/run/base.py +247 -0
- agno/run/cancel.py +81 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +767 -0
- agno/run/workflow.py +708 -0
- agno/session/__init__.py +10 -0
- agno/session/agent.py +260 -0
- agno/session/summary.py +265 -0
- agno/session/team.py +342 -0
- agno/session/workflow.py +501 -0
- agno/table.py +10 -0
- agno/team/__init__.py +37 -0
- agno/team/team.py +9536 -0
- agno/tools/__init__.py +7 -0
- agno/tools/agentql.py +120 -0
- agno/tools/airflow.py +22 -12
- agno/tools/api.py +122 -0
- agno/tools/apify.py +276 -83
- agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
- agno/tools/aws_lambda.py +28 -7
- agno/tools/aws_ses.py +66 -0
- agno/tools/baidusearch.py +11 -4
- agno/tools/bitbucket.py +292 -0
- agno/tools/brandfetch.py +213 -0
- agno/tools/bravesearch.py +106 -0
- agno/tools/brightdata.py +367 -0
- agno/tools/browserbase.py +209 -0
- agno/tools/calcom.py +32 -23
- agno/tools/calculator.py +24 -37
- agno/tools/cartesia.py +187 -0
- agno/tools/{clickup_tool.py → clickup.py} +17 -28
- agno/tools/confluence.py +91 -26
- agno/tools/crawl4ai.py +139 -43
- agno/tools/csv_toolkit.py +28 -22
- agno/tools/dalle.py +36 -22
- agno/tools/daytona.py +475 -0
- agno/tools/decorator.py +169 -14
- agno/tools/desi_vocal.py +23 -11
- agno/tools/discord.py +32 -29
- agno/tools/docker.py +716 -0
- agno/tools/duckdb.py +76 -81
- agno/tools/duckduckgo.py +43 -40
- agno/tools/e2b.py +703 -0
- agno/tools/eleven_labs.py +65 -54
- agno/tools/email.py +13 -5
- agno/tools/evm.py +129 -0
- agno/tools/exa.py +324 -42
- agno/tools/fal.py +39 -35
- agno/tools/file.py +196 -30
- agno/tools/file_generation.py +356 -0
- agno/tools/financial_datasets.py +288 -0
- agno/tools/firecrawl.py +108 -33
- agno/tools/function.py +960 -122
- agno/tools/giphy.py +34 -12
- agno/tools/github.py +1294 -97
- agno/tools/gmail.py +922 -0
- agno/tools/google_bigquery.py +117 -0
- agno/tools/google_drive.py +271 -0
- agno/tools/google_maps.py +253 -0
- agno/tools/googlecalendar.py +607 -107
- agno/tools/googlesheets.py +377 -0
- agno/tools/hackernews.py +20 -12
- agno/tools/jina.py +24 -14
- agno/tools/jira.py +48 -19
- agno/tools/knowledge.py +218 -0
- agno/tools/linear.py +82 -43
- agno/tools/linkup.py +58 -0
- agno/tools/local_file_system.py +15 -7
- agno/tools/lumalab.py +41 -26
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +193 -0
- agno/tools/memory.py +419 -0
- agno/tools/mlx_transcribe.py +11 -9
- agno/tools/models/azure_openai.py +190 -0
- agno/tools/models/gemini.py +203 -0
- agno/tools/models/groq.py +158 -0
- agno/tools/models/morph.py +186 -0
- agno/tools/models/nebius.py +124 -0
- agno/tools/models_labs.py +163 -82
- agno/tools/moviepy_video.py +18 -13
- agno/tools/nano_banana.py +151 -0
- agno/tools/neo4j.py +134 -0
- agno/tools/newspaper.py +15 -4
- agno/tools/newspaper4k.py +19 -6
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +181 -17
- agno/tools/openbb.py +27 -20
- agno/tools/opencv.py +321 -0
- agno/tools/openweather.py +233 -0
- agno/tools/oxylabs.py +385 -0
- agno/tools/pandas.py +25 -15
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +238 -185
- agno/tools/pubmed.py +125 -13
- agno/tools/python.py +48 -35
- agno/tools/reasoning.py +283 -0
- agno/tools/reddit.py +207 -29
- agno/tools/redshift.py +406 -0
- agno/tools/replicate.py +69 -26
- agno/tools/resend.py +11 -6
- agno/tools/scrapegraph.py +179 -19
- agno/tools/searxng.py +23 -31
- agno/tools/serpapi.py +15 -10
- agno/tools/serper.py +255 -0
- agno/tools/shell.py +23 -12
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +56 -14
- agno/tools/sleep.py +8 -6
- agno/tools/spider.py +35 -11
- agno/tools/spotify.py +919 -0
- agno/tools/sql.py +34 -19
- agno/tools/tavily.py +158 -8
- agno/tools/telegram.py +18 -8
- agno/tools/todoist.py +218 -0
- agno/tools/toolkit.py +134 -9
- agno/tools/trafilatura.py +388 -0
- agno/tools/trello.py +25 -28
- agno/tools/twilio.py +18 -9
- agno/tools/user_control_flow.py +78 -0
- agno/tools/valyu.py +228 -0
- agno/tools/visualization.py +467 -0
- agno/tools/webbrowser.py +28 -0
- agno/tools/webex.py +76 -0
- agno/tools/website.py +23 -19
- agno/tools/webtools.py +45 -0
- agno/tools/whatsapp.py +286 -0
- agno/tools/wikipedia.py +28 -19
- agno/tools/workflow.py +285 -0
- agno/tools/{twitter.py → x.py} +142 -46
- agno/tools/yfinance.py +41 -39
- agno/tools/youtube.py +34 -17
- agno/tools/zendesk.py +15 -5
- agno/tools/zep.py +454 -0
- agno/tools/zoom.py +86 -37
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/audio.py +37 -1
- agno/utils/certs.py +27 -0
- agno/utils/code_execution.py +11 -0
- agno/utils/common.py +103 -20
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +700 -0
- agno/utils/functions.py +107 -37
- agno/utils/gemini.py +426 -0
- agno/utils/hooks.py +171 -0
- agno/utils/http.py +185 -0
- agno/utils/json_schema.py +159 -37
- agno/utils/knowledge.py +36 -0
- agno/utils/location.py +19 -0
- agno/utils/log.py +221 -8
- agno/utils/mcp.py +214 -0
- agno/utils/media.py +335 -14
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +77 -2
- agno/utils/models/ai_foundry.py +50 -0
- agno/utils/models/claude.py +373 -0
- agno/utils/models/cohere.py +94 -0
- agno/utils/models/llama.py +85 -0
- agno/utils/models/mistral.py +100 -0
- agno/utils/models/openai_responses.py +140 -0
- agno/utils/models/schema_utils.py +153 -0
- agno/utils/models/watsonx.py +41 -0
- agno/utils/openai.py +257 -0
- agno/utils/pickle.py +1 -1
- agno/utils/pprint.py +124 -8
- agno/utils/print_response/agent.py +930 -0
- agno/utils/print_response/team.py +1914 -0
- agno/utils/print_response/workflow.py +1668 -0
- agno/utils/prompts.py +111 -0
- agno/utils/reasoning.py +108 -0
- agno/utils/response.py +163 -0
- agno/utils/serialize.py +32 -0
- agno/utils/shell.py +4 -4
- agno/utils/streamlit.py +487 -0
- agno/utils/string.py +204 -51
- agno/utils/team.py +139 -0
- agno/utils/timer.py +9 -2
- agno/utils/tokens.py +657 -0
- agno/utils/tools.py +19 -1
- agno/utils/whatsapp.py +305 -0
- agno/utils/yaml_io.py +3 -3
- agno/vectordb/__init__.py +2 -0
- agno/vectordb/base.py +87 -9
- agno/vectordb/cassandra/__init__.py +5 -1
- agno/vectordb/cassandra/cassandra.py +383 -27
- agno/vectordb/chroma/__init__.py +4 -0
- agno/vectordb/chroma/chromadb.py +748 -83
- agno/vectordb/clickhouse/__init__.py +7 -1
- agno/vectordb/clickhouse/clickhousedb.py +554 -53
- agno/vectordb/couchbase/__init__.py +3 -0
- agno/vectordb/couchbase/couchbase.py +1446 -0
- agno/vectordb/lancedb/__init__.py +5 -0
- agno/vectordb/lancedb/lance_db.py +730 -98
- agno/vectordb/langchaindb/__init__.py +5 -0
- agno/vectordb/langchaindb/langchaindb.py +163 -0
- agno/vectordb/lightrag/__init__.py +5 -0
- agno/vectordb/lightrag/lightrag.py +388 -0
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +166 -0
- agno/vectordb/milvus/__init__.py +3 -0
- agno/vectordb/milvus/milvus.py +966 -78
- agno/vectordb/mongodb/__init__.py +9 -1
- agno/vectordb/mongodb/mongodb.py +1175 -172
- agno/vectordb/pgvector/__init__.py +8 -0
- agno/vectordb/pgvector/pgvector.py +599 -115
- agno/vectordb/pineconedb/__init__.py +5 -1
- agno/vectordb/pineconedb/pineconedb.py +406 -43
- agno/vectordb/qdrant/__init__.py +4 -0
- agno/vectordb/qdrant/qdrant.py +914 -61
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/__init__.py +8 -1
- agno/vectordb/singlestore/singlestore.py +771 -0
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +663 -0
- agno/vectordb/upstashdb/__init__.py +5 -0
- agno/vectordb/upstashdb/upstashdb.py +718 -0
- agno/vectordb/weaviate/__init__.py +8 -0
- agno/vectordb/weaviate/index.py +15 -0
- agno/vectordb/weaviate/weaviate.py +1009 -0
- agno/workflow/__init__.py +23 -1
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +759 -0
- agno/workflow/loop.py +756 -0
- agno/workflow/parallel.py +853 -0
- agno/workflow/router.py +723 -0
- agno/workflow/step.py +1564 -0
- agno/workflow/steps.py +613 -0
- agno/workflow/types.py +556 -0
- agno/workflow/workflow.py +4327 -514
- agno-2.3.13.dist-info/METADATA +639 -0
- agno-2.3.13.dist-info/RECORD +613 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
- agno-2.3.13.dist-info/licenses/LICENSE +201 -0
- agno/api/playground.py +0 -91
- agno/api/schemas/playground.py +0 -22
- agno/api/schemas/user.py +0 -22
- agno/api/schemas/workspace.py +0 -46
- agno/api/user.py +0 -160
- agno/api/workspace.py +0 -151
- agno/cli/auth_server.py +0 -118
- agno/cli/config.py +0 -275
- agno/cli/console.py +0 -88
- agno/cli/credentials.py +0 -23
- agno/cli/entrypoint.py +0 -571
- agno/cli/operator.py +0 -355
- agno/cli/settings.py +0 -85
- agno/cli/ws/ws_cli.py +0 -817
- agno/constants.py +0 -13
- agno/document/__init__.py +0 -1
- agno/document/chunking/semantic.py +0 -47
- agno/document/chunking/strategy.py +0 -31
- agno/document/reader/__init__.py +0 -1
- agno/document/reader/arxiv_reader.py +0 -41
- agno/document/reader/base.py +0 -22
- agno/document/reader/csv_reader.py +0 -84
- agno/document/reader/docx_reader.py +0 -46
- agno/document/reader/firecrawl_reader.py +0 -99
- agno/document/reader/json_reader.py +0 -43
- agno/document/reader/pdf_reader.py +0 -219
- agno/document/reader/s3/pdf_reader.py +0 -46
- agno/document/reader/s3/text_reader.py +0 -51
- agno/document/reader/text_reader.py +0 -41
- agno/document/reader/website_reader.py +0 -175
- agno/document/reader/youtube_reader.py +0 -50
- agno/embedder/__init__.py +0 -1
- agno/embedder/azure_openai.py +0 -86
- agno/embedder/cohere.py +0 -72
- agno/embedder/fastembed.py +0 -37
- agno/embedder/google.py +0 -73
- agno/embedder/huggingface.py +0 -54
- agno/embedder/mistral.py +0 -80
- agno/embedder/ollama.py +0 -57
- agno/embedder/openai.py +0 -74
- agno/embedder/sentence_transformer.py +0 -38
- agno/embedder/voyageai.py +0 -64
- agno/eval/perf.py +0 -201
- agno/file/__init__.py +0 -1
- agno/file/file.py +0 -16
- agno/file/local/csv.py +0 -32
- agno/file/local/txt.py +0 -19
- agno/infra/app.py +0 -240
- agno/infra/base.py +0 -144
- agno/infra/context.py +0 -20
- agno/infra/db_app.py +0 -52
- agno/infra/resource.py +0 -205
- agno/infra/resources.py +0 -55
- agno/knowledge/agent.py +0 -230
- agno/knowledge/arxiv.py +0 -22
- agno/knowledge/combined.py +0 -22
- agno/knowledge/csv.py +0 -28
- agno/knowledge/csv_url.py +0 -19
- agno/knowledge/document.py +0 -20
- agno/knowledge/docx.py +0 -30
- agno/knowledge/json.py +0 -28
- agno/knowledge/langchain.py +0 -71
- agno/knowledge/llamaindex.py +0 -66
- agno/knowledge/pdf.py +0 -28
- agno/knowledge/pdf_url.py +0 -26
- agno/knowledge/s3/base.py +0 -60
- agno/knowledge/s3/pdf.py +0 -21
- agno/knowledge/s3/text.py +0 -23
- agno/knowledge/text.py +0 -30
- agno/knowledge/website.py +0 -88
- agno/knowledge/wikipedia.py +0 -31
- agno/knowledge/youtube.py +0 -22
- agno/memory/agent.py +0 -392
- agno/memory/classifier.py +0 -104
- agno/memory/db/__init__.py +0 -1
- agno/memory/db/base.py +0 -42
- agno/memory/db/mongodb.py +0 -189
- agno/memory/db/postgres.py +0 -203
- agno/memory/db/sqlite.py +0 -193
- agno/memory/memory.py +0 -15
- agno/memory/row.py +0 -36
- agno/memory/summarizer.py +0 -192
- agno/memory/summary.py +0 -19
- agno/memory/workflow.py +0 -38
- agno/models/google/gemini_openai.py +0 -26
- agno/models/ollama/hermes.py +0 -221
- agno/models/ollama/tools.py +0 -362
- agno/models/vertexai/gemini.py +0 -595
- agno/playground/__init__.py +0 -3
- agno/playground/async_router.py +0 -421
- agno/playground/deploy.py +0 -249
- agno/playground/operator.py +0 -92
- agno/playground/playground.py +0 -91
- agno/playground/schemas.py +0 -76
- agno/playground/serve.py +0 -55
- agno/playground/sync_router.py +0 -405
- agno/reasoning/agent.py +0 -68
- agno/run/response.py +0 -112
- agno/storage/agent/__init__.py +0 -0
- agno/storage/agent/base.py +0 -38
- agno/storage/agent/dynamodb.py +0 -350
- agno/storage/agent/json.py +0 -92
- agno/storage/agent/mongodb.py +0 -228
- agno/storage/agent/postgres.py +0 -367
- agno/storage/agent/session.py +0 -79
- agno/storage/agent/singlestore.py +0 -303
- agno/storage/agent/sqlite.py +0 -357
- agno/storage/agent/yaml.py +0 -93
- agno/storage/workflow/__init__.py +0 -0
- agno/storage/workflow/base.py +0 -40
- agno/storage/workflow/mongodb.py +0 -233
- agno/storage/workflow/postgres.py +0 -366
- agno/storage/workflow/session.py +0 -60
- agno/storage/workflow/sqlite.py +0 -359
- agno/tools/googlesearch.py +0 -88
- agno/utils/defaults.py +0 -57
- agno/utils/filesystem.py +0 -39
- agno/utils/git.py +0 -52
- agno/utils/json_io.py +0 -30
- agno/utils/load_env.py +0 -19
- agno/utils/py_io.py +0 -19
- agno/utils/pyproject.py +0 -18
- agno/utils/resource_filter.py +0 -31
- agno/vectordb/singlestore/s2vectordb.py +0 -390
- agno/vectordb/singlestore/s2vectordb2.py +0 -355
- agno/workspace/__init__.py +0 -0
- agno/workspace/config.py +0 -325
- agno/workspace/enums.py +0 -6
- agno/workspace/helpers.py +0 -48
- agno/workspace/operator.py +0 -758
- agno/workspace/settings.py +0 -63
- agno-0.1.2.dist-info/LICENSE +0 -375
- agno-0.1.2.dist-info/METADATA +0 -502
- agno-0.1.2.dist-info/RECORD +0 -352
- agno-0.1.2.dist-info/entry_points.txt +0 -3
- /agno/{cli → db/migrations}/__init__.py +0 -0
- /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
- /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
- /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
- /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
- /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
- /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
- /agno/{reranker → utils/models}/__init__.py +0 -0
- /agno/{storage → utils/print_response}/__init__.py +0 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/os/utils.py
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request, UploadFile
|
|
6
|
+
from fastapi.routing import APIRoute, APIRouter
|
|
7
|
+
from pydantic import BaseModel, create_model
|
|
8
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
9
|
+
|
|
10
|
+
from agno.agent.agent import Agent
|
|
11
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
12
|
+
from agno.knowledge.knowledge import Knowledge
|
|
13
|
+
from agno.media import Audio, Image, Video
|
|
14
|
+
from agno.media import File as FileMedia
|
|
15
|
+
from agno.models.message import Message
|
|
16
|
+
from agno.os.config import AgentOSConfig
|
|
17
|
+
from agno.run.agent import RunOutputEvent
|
|
18
|
+
from agno.run.team import TeamRunOutputEvent
|
|
19
|
+
from agno.run.workflow import WorkflowRunOutputEvent
|
|
20
|
+
from agno.team.team import Team
|
|
21
|
+
from agno.tools import Toolkit
|
|
22
|
+
from agno.tools.function import Function
|
|
23
|
+
from agno.utils.log import log_warning, logger
|
|
24
|
+
from agno.workflow.workflow import Workflow
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
28
|
+
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
29
|
+
Args:
|
|
30
|
+
request: The FastAPI Request object
|
|
31
|
+
endpoint_func: The function exposing the endpoint that received the request
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary of kwargs
|
|
35
|
+
"""
|
|
36
|
+
import inspect
|
|
37
|
+
|
|
38
|
+
form_data = await request.form()
|
|
39
|
+
sig = inspect.signature(endpoint_func)
|
|
40
|
+
known_fields = set(sig.parameters.keys())
|
|
41
|
+
kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
42
|
+
|
|
43
|
+
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
44
|
+
if session_state := kwargs.get("session_state"):
|
|
45
|
+
try:
|
|
46
|
+
if isinstance(session_state, str):
|
|
47
|
+
session_state_dict = json.loads(session_state) # type: ignore
|
|
48
|
+
kwargs["session_state"] = session_state_dict
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
kwargs.pop("session_state")
|
|
51
|
+
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
52
|
+
|
|
53
|
+
if dependencies := kwargs.get("dependencies"):
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(dependencies, str):
|
|
56
|
+
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
57
|
+
kwargs["dependencies"] = dependencies_dict
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
kwargs.pop("dependencies")
|
|
60
|
+
log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
|
|
61
|
+
|
|
62
|
+
if metadata := kwargs.get("metadata"):
|
|
63
|
+
try:
|
|
64
|
+
if isinstance(metadata, str):
|
|
65
|
+
metadata_dict = json.loads(metadata) # type: ignore
|
|
66
|
+
kwargs["metadata"] = metadata_dict
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
kwargs.pop("metadata")
|
|
69
|
+
log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
|
|
70
|
+
|
|
71
|
+
if knowledge_filters := kwargs.get("knowledge_filters"):
|
|
72
|
+
try:
|
|
73
|
+
if isinstance(knowledge_filters, str):
|
|
74
|
+
knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
|
|
75
|
+
|
|
76
|
+
# Try to deserialize FilterExpr objects
|
|
77
|
+
from agno.filters import from_dict
|
|
78
|
+
|
|
79
|
+
# Check if it's a single FilterExpr dict or a list of FilterExpr dicts
|
|
80
|
+
if isinstance(knowledge_filters_dict, dict) and "op" in knowledge_filters_dict:
|
|
81
|
+
# Single FilterExpr - convert to list format
|
|
82
|
+
kwargs["knowledge_filters"] = [from_dict(knowledge_filters_dict)]
|
|
83
|
+
elif isinstance(knowledge_filters_dict, list):
|
|
84
|
+
# List of FilterExprs or mixed content
|
|
85
|
+
deserialized = []
|
|
86
|
+
for item in knowledge_filters_dict:
|
|
87
|
+
if isinstance(item, dict) and "op" in item:
|
|
88
|
+
deserialized.append(from_dict(item))
|
|
89
|
+
else:
|
|
90
|
+
# Keep non-FilterExpr items as-is
|
|
91
|
+
deserialized.append(item)
|
|
92
|
+
kwargs["knowledge_filters"] = deserialized
|
|
93
|
+
else:
|
|
94
|
+
# Regular dict filter
|
|
95
|
+
kwargs["knowledge_filters"] = knowledge_filters_dict
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
kwargs.pop("knowledge_filters")
|
|
98
|
+
log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
# Filter deserialization failed
|
|
101
|
+
kwargs.pop("knowledge_filters")
|
|
102
|
+
log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
|
|
103
|
+
|
|
104
|
+
# Handle output_schema - convert JSON schema to dynamic Pydantic model
|
|
105
|
+
if output_schema := kwargs.get("output_schema"):
|
|
106
|
+
try:
|
|
107
|
+
if isinstance(output_schema, str):
|
|
108
|
+
from agno.os.utils import json_schema_to_pydantic_model
|
|
109
|
+
|
|
110
|
+
schema_dict = json.loads(output_schema)
|
|
111
|
+
dynamic_model = json_schema_to_pydantic_model(schema_dict)
|
|
112
|
+
kwargs["output_schema"] = dynamic_model
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
kwargs.pop("output_schema")
|
|
115
|
+
log_warning(f"Invalid output_schema JSON: {output_schema}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
kwargs.pop("output_schema")
|
|
118
|
+
log_warning(f"Failed to create output_schema model: {e}")
|
|
119
|
+
|
|
120
|
+
# Parse boolean and null values
|
|
121
|
+
for key, value in kwargs.items():
|
|
122
|
+
if isinstance(value, str) and value.lower() in ["true", "false"]:
|
|
123
|
+
kwargs[key] = value.lower() == "true"
|
|
124
|
+
elif isinstance(value, str) and value.lower() in ["null", "none"]:
|
|
125
|
+
kwargs[key] = None
|
|
126
|
+
|
|
127
|
+
return kwargs
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
|
|
131
|
+
"""Parse JSON data into SSE-compliant format.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
event_dict: Dictionary containing the event data
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
SSE-formatted response:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
event: EventName
|
|
141
|
+
data: { ... }
|
|
142
|
+
|
|
143
|
+
event: AnotherEventName
|
|
144
|
+
data: { ... }
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
# Parse the JSON to extract the event type
|
|
149
|
+
event_type = event.event or "message"
|
|
150
|
+
|
|
151
|
+
# Serialize to valid JSON with double quotes and no newlines
|
|
152
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
153
|
+
|
|
154
|
+
return f"event: {event_type}\ndata: {clean_json}\n\n"
|
|
155
|
+
except json.JSONDecodeError:
|
|
156
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
157
|
+
return f"event: message\ndata: {clean_json}\n\n"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def get_db(
|
|
161
|
+
dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
|
|
162
|
+
) -> Union[BaseDb, AsyncBaseDb]:
|
|
163
|
+
"""Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
|
|
164
|
+
|
|
165
|
+
if table and not db_id:
|
|
166
|
+
raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
|
|
167
|
+
|
|
168
|
+
async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
|
|
169
|
+
"""Check if this database has the specified table (configured and actually exists)."""
|
|
170
|
+
# First check if table name is configured
|
|
171
|
+
is_configured = (
|
|
172
|
+
hasattr(db, "session_table_name")
|
|
173
|
+
and db.session_table_name == table_name
|
|
174
|
+
or hasattr(db, "memory_table_name")
|
|
175
|
+
and db.memory_table_name == table_name
|
|
176
|
+
or hasattr(db, "metrics_table_name")
|
|
177
|
+
and db.metrics_table_name == table_name
|
|
178
|
+
or hasattr(db, "eval_table_name")
|
|
179
|
+
and db.eval_table_name == table_name
|
|
180
|
+
or hasattr(db, "knowledge_table_name")
|
|
181
|
+
and db.knowledge_table_name == table_name
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not is_configured:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
# Then check if table actually exists in the database
|
|
188
|
+
try:
|
|
189
|
+
if isinstance(db, AsyncBaseDb):
|
|
190
|
+
# For async databases, await the check
|
|
191
|
+
return await db.table_exists(table_name)
|
|
192
|
+
else:
|
|
193
|
+
# For sync databases, call directly
|
|
194
|
+
return db.table_exists(table_name)
|
|
195
|
+
except (NotImplementedError, AttributeError):
|
|
196
|
+
# If table_exists not implemented, fall back to configuration check
|
|
197
|
+
return is_configured
|
|
198
|
+
|
|
199
|
+
# If db_id is provided, first find the database with that ID
|
|
200
|
+
if db_id:
|
|
201
|
+
target_db_list = dbs.get(db_id)
|
|
202
|
+
if not target_db_list:
|
|
203
|
+
raise HTTPException(status_code=404, detail=f"No database found with id '{db_id}'")
|
|
204
|
+
|
|
205
|
+
# If table is also specified, search through all databases with this ID to find one with the table
|
|
206
|
+
if table:
|
|
207
|
+
for db in target_db_list:
|
|
208
|
+
if await _has_table(db, table):
|
|
209
|
+
return db
|
|
210
|
+
raise HTTPException(status_code=404, detail=f"No database with id '{db_id}' has table '{table}'")
|
|
211
|
+
|
|
212
|
+
# If no table specified, return the first database with this ID
|
|
213
|
+
return target_db_list[0]
|
|
214
|
+
|
|
215
|
+
# Raise if multiple databases are provided but no db_id is provided
|
|
216
|
+
if len(dbs) > 1:
|
|
217
|
+
raise HTTPException(
|
|
218
|
+
status_code=400, detail="The db_id query parameter is required when using multiple databases"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Return the first (and only) database
|
|
222
|
+
return next(db for dbs in dbs.values() for db in dbs)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge:
|
|
226
|
+
"""Return the knowledge instance with the given ID, or the first knowledge instance if no ID is provided."""
|
|
227
|
+
if not db_id and len(knowledge_instances) == 1:
|
|
228
|
+
return next(iter(knowledge_instances))
|
|
229
|
+
|
|
230
|
+
if not db_id:
|
|
231
|
+
raise HTTPException(
|
|
232
|
+
status_code=400, detail="The db_id query parameter is required when using multiple databases"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
for knowledge in knowledge_instances:
|
|
236
|
+
if knowledge.contents_db and knowledge.contents_db.id == db_id:
|
|
237
|
+
return knowledge
|
|
238
|
+
|
|
239
|
+
raise HTTPException(status_code=404, detail=f"Knowledge instance with id '{db_id}' not found")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> str:
|
|
243
|
+
"""Get the run input from the given run dictionary
|
|
244
|
+
|
|
245
|
+
Uses the RunInput/TeamRunInput object which stores the original user input.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
# For agent or team runs, use the stored input_content
|
|
249
|
+
if not is_workflow_run and run_dict.get("input") is not None:
|
|
250
|
+
input_data = run_dict.get("input")
|
|
251
|
+
if isinstance(input_data, dict) and input_data.get("input_content") is not None:
|
|
252
|
+
return stringify_input_content(input_data["input_content"])
|
|
253
|
+
|
|
254
|
+
if is_workflow_run:
|
|
255
|
+
# Check the input field directly
|
|
256
|
+
if run_dict.get("input") is not None:
|
|
257
|
+
input_value = run_dict.get("input")
|
|
258
|
+
return str(input_value)
|
|
259
|
+
|
|
260
|
+
# Check the step executor runs for fallback
|
|
261
|
+
step_executor_runs = run_dict.get("step_executor_runs", [])
|
|
262
|
+
if step_executor_runs:
|
|
263
|
+
for message in reversed(step_executor_runs[0].get("messages", [])):
|
|
264
|
+
if message.get("role") == "user":
|
|
265
|
+
return message.get("content", "")
|
|
266
|
+
|
|
267
|
+
# Final fallback: scan messages
|
|
268
|
+
if run_dict.get("messages") is not None:
|
|
269
|
+
for message in reversed(run_dict["messages"]):
|
|
270
|
+
if message.get("role") == "user":
|
|
271
|
+
return message.get("content", "")
|
|
272
|
+
|
|
273
|
+
return ""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_session_name(session: Dict[str, Any]) -> str:
|
|
277
|
+
"""Get the session name from the given session dictionary"""
|
|
278
|
+
|
|
279
|
+
# If session_data.session_name is set, return that
|
|
280
|
+
session_data = session.get("session_data")
|
|
281
|
+
if session_data is not None and session_data.get("session_name") is not None:
|
|
282
|
+
return session_data["session_name"]
|
|
283
|
+
|
|
284
|
+
# Otherwise use the original user message
|
|
285
|
+
else:
|
|
286
|
+
runs = session.get("runs", []) or []
|
|
287
|
+
|
|
288
|
+
# For teams, identify the first Team run and avoid using the first member's run
|
|
289
|
+
if session.get("session_type") == "team":
|
|
290
|
+
run = None
|
|
291
|
+
for r in runs:
|
|
292
|
+
# If agent_id is not present, it's a team run
|
|
293
|
+
if not r.get("agent_id"):
|
|
294
|
+
run = r
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# Fallback to first run if no team run found
|
|
298
|
+
if run is None and runs:
|
|
299
|
+
run = runs[0]
|
|
300
|
+
|
|
301
|
+
elif session.get("session_type") == "workflow":
|
|
302
|
+
try:
|
|
303
|
+
workflow_run = runs[0]
|
|
304
|
+
workflow_input = workflow_run.get("input")
|
|
305
|
+
if isinstance(workflow_input, str):
|
|
306
|
+
return workflow_input
|
|
307
|
+
elif isinstance(workflow_input, dict):
|
|
308
|
+
try:
|
|
309
|
+
import json
|
|
310
|
+
|
|
311
|
+
return json.dumps(workflow_input)
|
|
312
|
+
except (TypeError, ValueError):
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
workflow_name = session.get("workflow_data", {}).get("name")
|
|
316
|
+
return f"New {workflow_name} Session" if workflow_name else ""
|
|
317
|
+
except (KeyError, IndexError, TypeError):
|
|
318
|
+
return ""
|
|
319
|
+
|
|
320
|
+
# For agents, use the first run
|
|
321
|
+
else:
|
|
322
|
+
run = runs[0] if runs else None
|
|
323
|
+
|
|
324
|
+
if run is None:
|
|
325
|
+
return ""
|
|
326
|
+
|
|
327
|
+
if not isinstance(run, dict):
|
|
328
|
+
run = run.to_dict()
|
|
329
|
+
|
|
330
|
+
if run and run.get("messages"):
|
|
331
|
+
for message in run["messages"]:
|
|
332
|
+
if message["role"] == "user":
|
|
333
|
+
return message["content"]
|
|
334
|
+
return ""
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
338
|
+
input_media: Dict[str, List[Any]] = {
|
|
339
|
+
"images": [],
|
|
340
|
+
"videos": [],
|
|
341
|
+
"audios": [],
|
|
342
|
+
"files": [],
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
input = run_dict.get("input", {})
|
|
346
|
+
input_media["images"].extend(input.get("images", []))
|
|
347
|
+
input_media["videos"].extend(input.get("videos", []))
|
|
348
|
+
input_media["audios"].extend(input.get("audios", []))
|
|
349
|
+
input_media["files"].extend(input.get("files", []))
|
|
350
|
+
|
|
351
|
+
return input_media
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def process_image(file: UploadFile) -> Image:
|
|
355
|
+
content = file.file.read()
|
|
356
|
+
if not content:
|
|
357
|
+
raise HTTPException(status_code=400, detail="Empty file")
|
|
358
|
+
return Image(content=content, format=extract_format(file), mime_type=file.content_type)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def process_audio(file: UploadFile) -> Audio:
|
|
362
|
+
content = file.file.read()
|
|
363
|
+
if not content:
|
|
364
|
+
raise HTTPException(status_code=400, detail="Empty file")
|
|
365
|
+
return Audio(content=content, format=extract_format(file), mime_type=file.content_type)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def process_video(file: UploadFile) -> Video:
|
|
369
|
+
content = file.file.read()
|
|
370
|
+
if not content:
|
|
371
|
+
raise HTTPException(status_code=400, detail="Empty file")
|
|
372
|
+
return Video(content=content, format=extract_format(file), mime_type=file.content_type)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def process_document(file: UploadFile) -> Optional[FileMedia]:
|
|
376
|
+
try:
|
|
377
|
+
content = file.file.read()
|
|
378
|
+
if not content:
|
|
379
|
+
raise HTTPException(status_code=400, detail="Empty file")
|
|
380
|
+
return FileMedia(
|
|
381
|
+
content=content, filename=file.filename, format=extract_format(file), mime_type=file.content_type
|
|
382
|
+
)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Error processing document {file.filename}: {e}")
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def extract_format(file: UploadFile) -> Optional[str]:
|
|
389
|
+
"""Extract the File format from file name or content_type."""
|
|
390
|
+
# Get the format from the filename
|
|
391
|
+
if file.filename and "." in file.filename:
|
|
392
|
+
return file.filename.split(".")[-1].lower()
|
|
393
|
+
|
|
394
|
+
# Fallback to the file content_type
|
|
395
|
+
if file.content_type:
|
|
396
|
+
return file.content_type.strip().split("/")[-1]
|
|
397
|
+
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
|
|
402
|
+
formatted_tools: List[Dict] = []
|
|
403
|
+
if agent_tools is not None:
|
|
404
|
+
for tool in agent_tools:
|
|
405
|
+
if isinstance(tool, dict):
|
|
406
|
+
formatted_tools.append(tool)
|
|
407
|
+
elif isinstance(tool, Toolkit):
|
|
408
|
+
for _, f in tool.functions.items():
|
|
409
|
+
formatted_tools.append(f.to_dict())
|
|
410
|
+
elif isinstance(tool, Function):
|
|
411
|
+
formatted_tools.append(tool.to_dict())
|
|
412
|
+
elif callable(tool):
|
|
413
|
+
func = Function.from_callable(tool)
|
|
414
|
+
formatted_tools.append(func.to_dict())
|
|
415
|
+
else:
|
|
416
|
+
logger.warning(f"Unknown tool type: {type(tool)}")
|
|
417
|
+
return formatted_tools
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def format_team_tools(team_tools: List[Union[Function, dict]]):
|
|
421
|
+
formatted_tools: List[Dict] = []
|
|
422
|
+
if team_tools is not None:
|
|
423
|
+
for tool in team_tools:
|
|
424
|
+
if isinstance(tool, dict):
|
|
425
|
+
formatted_tools.append(tool)
|
|
426
|
+
elif isinstance(tool, Function):
|
|
427
|
+
formatted_tools.append(tool.to_dict())
|
|
428
|
+
return formatted_tools
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
|
|
432
|
+
if agent_id is None or agents is None:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
for agent in agents:
|
|
436
|
+
if agent.id == agent_id:
|
|
437
|
+
return agent
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_team_by_id(team_id: str, teams: Optional[List[Team]] = None) -> Optional[Team]:
|
|
442
|
+
if team_id is None or teams is None:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
for team in teams:
|
|
446
|
+
if team.id == team_id:
|
|
447
|
+
return team
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = None) -> Optional[Workflow]:
|
|
452
|
+
if workflow_id is None or workflows is None:
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
for workflow in workflows:
|
|
456
|
+
if workflow.id == workflow_id:
|
|
457
|
+
return workflow
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# INPUT SCHEMA VALIDATIONS
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_agent_input_schema_dict(agent: Agent) -> Optional[Dict[str, Any]]:
|
|
465
|
+
"""Get input schema as dictionary for API responses"""
|
|
466
|
+
|
|
467
|
+
if agent.input_schema is not None:
|
|
468
|
+
try:
|
|
469
|
+
return agent.input_schema.model_json_schema()
|
|
470
|
+
except Exception:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_team_input_schema_dict(team: Team) -> Optional[Dict[str, Any]]:
|
|
477
|
+
"""Get input schema as dictionary for API responses"""
|
|
478
|
+
|
|
479
|
+
if team.input_schema is not None:
|
|
480
|
+
try:
|
|
481
|
+
return team.input_schema.model_json_schema()
|
|
482
|
+
except Exception:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def get_workflow_input_schema_dict(workflow: Workflow) -> Optional[Dict[str, Any]]:
|
|
489
|
+
"""Get input schema as dictionary for API responses"""
|
|
490
|
+
|
|
491
|
+
# Priority 1: Explicit input_schema (Pydantic model)
|
|
492
|
+
if workflow.input_schema is not None:
|
|
493
|
+
try:
|
|
494
|
+
return workflow.input_schema.model_json_schema()
|
|
495
|
+
except Exception:
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
# Priority 2: Auto-generate from custom kwargs
|
|
499
|
+
if workflow.steps and callable(workflow.steps):
|
|
500
|
+
custom_params = workflow.run_parameters
|
|
501
|
+
if custom_params and len(custom_params) > 1: # More than just 'message'
|
|
502
|
+
return _generate_schema_from_params(custom_params)
|
|
503
|
+
|
|
504
|
+
# Priority 3: No schema (expects string message)
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
509
|
+
"""Convert function parameters to JSON schema"""
|
|
510
|
+
properties: Dict[str, Any] = {}
|
|
511
|
+
required: List[str] = []
|
|
512
|
+
|
|
513
|
+
for param_name, param_info in params.items():
|
|
514
|
+
# Skip the default 'message' parameter for custom kwargs workflows
|
|
515
|
+
if param_name == "message":
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
# Map Python types to JSON schema types
|
|
519
|
+
param_type = param_info.get("annotation", "str")
|
|
520
|
+
default_value = param_info.get("default")
|
|
521
|
+
is_required = param_info.get("required", False)
|
|
522
|
+
|
|
523
|
+
# Convert Python type annotations to JSON schema types
|
|
524
|
+
if param_type == "str":
|
|
525
|
+
properties[param_name] = {"type": "string"}
|
|
526
|
+
elif param_type == "bool":
|
|
527
|
+
properties[param_name] = {"type": "boolean"}
|
|
528
|
+
elif param_type == "int":
|
|
529
|
+
properties[param_name] = {"type": "integer"}
|
|
530
|
+
elif param_type == "float":
|
|
531
|
+
properties[param_name] = {"type": "number"}
|
|
532
|
+
elif "List" in str(param_type):
|
|
533
|
+
properties[param_name] = {"type": "array", "items": {"type": "string"}}
|
|
534
|
+
else:
|
|
535
|
+
properties[param_name] = {"type": "string"} # fallback
|
|
536
|
+
|
|
537
|
+
# Add default value if present
|
|
538
|
+
if default_value is not None:
|
|
539
|
+
properties[param_name]["default"] = default_value
|
|
540
|
+
|
|
541
|
+
# Add to required if no default value
|
|
542
|
+
if is_required and default_value is None:
|
|
543
|
+
required.append(param_name)
|
|
544
|
+
|
|
545
|
+
schema = {"type": "object", "properties": properties}
|
|
546
|
+
|
|
547
|
+
if required:
|
|
548
|
+
schema["required"] = required
|
|
549
|
+
|
|
550
|
+
return schema
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def resolve_origins(user_origins: Optional[List[str]] = None, default_origins: Optional[List[str]] = None) -> List[str]:
|
|
554
|
+
"""
|
|
555
|
+
Get CORS origins - user-provided origins override defaults.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
user_origins: Optional list of user-provided CORS origins
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
List of allowed CORS origins (user-provided if set, otherwise defaults)
|
|
562
|
+
"""
|
|
563
|
+
# User-provided origins override defaults
|
|
564
|
+
if user_origins:
|
|
565
|
+
return user_origins
|
|
566
|
+
|
|
567
|
+
# Default Agno domains
|
|
568
|
+
return default_origins or [
|
|
569
|
+
"http://localhost:3000",
|
|
570
|
+
"https://agno.com",
|
|
571
|
+
"https://www.agno.com",
|
|
572
|
+
"https://app.agno.com",
|
|
573
|
+
"https://os-stg.agno.com",
|
|
574
|
+
"https://os.agno.com",
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def update_cors_middleware(app: FastAPI, new_origins: list):
|
|
579
|
+
existing_origins: List[str] = []
|
|
580
|
+
|
|
581
|
+
# TODO: Allow more options where CORS is properly merged and user can disable this behaviour
|
|
582
|
+
|
|
583
|
+
# Extract existing origins from current CORS middleware
|
|
584
|
+
for middleware in app.user_middleware:
|
|
585
|
+
if middleware.cls == CORSMiddleware:
|
|
586
|
+
if hasattr(middleware, "kwargs"):
|
|
587
|
+
origins_value = middleware.kwargs.get("allow_origins", [])
|
|
588
|
+
if isinstance(origins_value, list):
|
|
589
|
+
existing_origins = origins_value
|
|
590
|
+
else:
|
|
591
|
+
existing_origins = []
|
|
592
|
+
break
|
|
593
|
+
# Merge origins
|
|
594
|
+
merged_origins = list(set(new_origins + existing_origins))
|
|
595
|
+
final_origins = [origin for origin in merged_origins if origin != "*"]
|
|
596
|
+
|
|
597
|
+
# Remove existing CORS
|
|
598
|
+
app.user_middleware = [m for m in app.user_middleware if m.cls != CORSMiddleware]
|
|
599
|
+
app.middleware_stack = None
|
|
600
|
+
|
|
601
|
+
# Add updated CORS
|
|
602
|
+
app.add_middleware(
|
|
603
|
+
CORSMiddleware, # type: ignore
|
|
604
|
+
allow_origins=final_origins,
|
|
605
|
+
allow_credentials=True,
|
|
606
|
+
allow_methods=["*"],
|
|
607
|
+
allow_headers=["*"],
|
|
608
|
+
expose_headers=["*"],
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def get_existing_route_paths(fastapi_app: FastAPI) -> Dict[str, List[str]]:
|
|
613
|
+
"""Get all existing route paths and methods from the FastAPI app.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
|
|
617
|
+
"""
|
|
618
|
+
existing_paths: Dict[str, Any] = {}
|
|
619
|
+
for route in fastapi_app.routes:
|
|
620
|
+
if isinstance(route, APIRoute):
|
|
621
|
+
path = route.path
|
|
622
|
+
methods = list(route.methods) if route.methods else []
|
|
623
|
+
if path in existing_paths:
|
|
624
|
+
existing_paths[path].extend(methods)
|
|
625
|
+
else:
|
|
626
|
+
existing_paths[path] = methods
|
|
627
|
+
return existing_paths
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def find_conflicting_routes(fastapi_app: FastAPI, router: APIRouter) -> List[Dict[str, Any]]:
|
|
631
|
+
"""Find conflicting routes in the FastAPI app.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
fastapi_app: The FastAPI app with all existing routes
|
|
635
|
+
router: The APIRouter to add
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
List[Dict[str, Any]]: List of conflicting routes
|
|
639
|
+
"""
|
|
640
|
+
existing_paths = get_existing_route_paths(fastapi_app)
|
|
641
|
+
|
|
642
|
+
conflicts = []
|
|
643
|
+
|
|
644
|
+
for route in router.routes:
|
|
645
|
+
if isinstance(route, APIRoute):
|
|
646
|
+
full_path = route.path
|
|
647
|
+
route_methods = list(route.methods) if route.methods else []
|
|
648
|
+
|
|
649
|
+
if full_path in existing_paths:
|
|
650
|
+
conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
|
|
651
|
+
if conflicting_methods:
|
|
652
|
+
conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
|
|
653
|
+
return conflicts
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def load_yaml_config(config_file_path: str) -> AgentOSConfig:
|
|
657
|
+
"""Load a YAML config file and return the configuration as an AgentOSConfig instance."""
|
|
658
|
+
from pathlib import Path
|
|
659
|
+
|
|
660
|
+
import yaml
|
|
661
|
+
|
|
662
|
+
# Validate that the path points to a YAML file
|
|
663
|
+
path = Path(config_file_path)
|
|
664
|
+
if path.suffix.lower() not in [".yaml", ".yml"]:
|
|
665
|
+
raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
|
|
666
|
+
|
|
667
|
+
# Load the YAML file
|
|
668
|
+
with open(config_file_path, "r") as f:
|
|
669
|
+
return AgentOSConfig.model_validate(yaml.safe_load(f))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
|
|
673
|
+
"""Recursively collect MCP tools from a team and its members."""
|
|
674
|
+
# Check the team tools
|
|
675
|
+
if team.tools:
|
|
676
|
+
for tool in team.tools:
|
|
677
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
678
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
679
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
680
|
+
):
|
|
681
|
+
if tool not in mcp_tools:
|
|
682
|
+
mcp_tools.append(tool)
|
|
683
|
+
|
|
684
|
+
# Recursively check team members
|
|
685
|
+
if team.members:
|
|
686
|
+
for member in team.members:
|
|
687
|
+
if isinstance(member, Agent):
|
|
688
|
+
if member.tools:
|
|
689
|
+
for tool in member.tools:
|
|
690
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
691
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
692
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
693
|
+
):
|
|
694
|
+
if tool not in mcp_tools:
|
|
695
|
+
mcp_tools.append(tool)
|
|
696
|
+
|
|
697
|
+
elif isinstance(member, Team):
|
|
698
|
+
# Recursively check nested team
|
|
699
|
+
collect_mcp_tools_from_team(member, mcp_tools)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def collect_mcp_tools_from_workflow(workflow: Workflow, mcp_tools: List[Any]) -> None:
|
|
703
|
+
"""Recursively collect MCP tools from a workflow and its steps."""
|
|
704
|
+
from agno.workflow.steps import Steps
|
|
705
|
+
|
|
706
|
+
# Recursively check workflow steps
|
|
707
|
+
if workflow.steps:
|
|
708
|
+
if isinstance(workflow.steps, list):
|
|
709
|
+
# Handle list of steps
|
|
710
|
+
for step in workflow.steps:
|
|
711
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
712
|
+
|
|
713
|
+
elif isinstance(workflow.steps, Steps):
|
|
714
|
+
# Handle Steps container
|
|
715
|
+
if steps := workflow.steps.steps:
|
|
716
|
+
for step in steps:
|
|
717
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
718
|
+
|
|
719
|
+
elif callable(workflow.steps):
|
|
720
|
+
pass
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> None:
|
|
724
|
+
"""Collect MCP tools from a single workflow step."""
|
|
725
|
+
from agno.workflow.condition import Condition
|
|
726
|
+
from agno.workflow.loop import Loop
|
|
727
|
+
from agno.workflow.parallel import Parallel
|
|
728
|
+
from agno.workflow.router import Router
|
|
729
|
+
from agno.workflow.step import Step
|
|
730
|
+
from agno.workflow.steps import Steps
|
|
731
|
+
|
|
732
|
+
if isinstance(step, Step):
|
|
733
|
+
# Check step's agent
|
|
734
|
+
if step.agent:
|
|
735
|
+
if step.agent.tools:
|
|
736
|
+
for tool in step.agent.tools:
|
|
737
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
738
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
739
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
740
|
+
):
|
|
741
|
+
if tool not in mcp_tools:
|
|
742
|
+
mcp_tools.append(tool)
|
|
743
|
+
# Check step's team
|
|
744
|
+
if step.team:
|
|
745
|
+
collect_mcp_tools_from_team(step.team, mcp_tools)
|
|
746
|
+
|
|
747
|
+
elif isinstance(step, Steps):
|
|
748
|
+
if steps := step.steps:
|
|
749
|
+
for step in steps:
|
|
750
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
751
|
+
|
|
752
|
+
elif isinstance(step, (Parallel, Loop, Condition, Router)):
|
|
753
|
+
# These contain other steps - recursively check them
|
|
754
|
+
if hasattr(step, "steps") and step.steps:
|
|
755
|
+
for sub_step in step.steps:
|
|
756
|
+
collect_mcp_tools_from_workflow_step(sub_step, mcp_tools)
|
|
757
|
+
|
|
758
|
+
elif isinstance(step, Agent):
|
|
759
|
+
# Direct agent in workflow steps
|
|
760
|
+
if step.tools:
|
|
761
|
+
for tool in step.tools:
|
|
762
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
763
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
764
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
765
|
+
):
|
|
766
|
+
if tool not in mcp_tools:
|
|
767
|
+
mcp_tools.append(tool)
|
|
768
|
+
|
|
769
|
+
elif isinstance(step, Team):
|
|
770
|
+
# Direct team in workflow steps
|
|
771
|
+
collect_mcp_tools_from_team(step, mcp_tools)
|
|
772
|
+
|
|
773
|
+
elif isinstance(step, Workflow):
|
|
774
|
+
# Nested workflow
|
|
775
|
+
collect_mcp_tools_from_workflow(step, mcp_tools)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
|
|
779
|
+
"""Convert any given input_content into its string representation.
|
|
780
|
+
|
|
781
|
+
This handles both serialized (dict) and live (object) input_content formats.
|
|
782
|
+
"""
|
|
783
|
+
import json
|
|
784
|
+
|
|
785
|
+
if isinstance(input_content, str):
|
|
786
|
+
return input_content
|
|
787
|
+
elif isinstance(input_content, Message):
|
|
788
|
+
return json.dumps(input_content.to_dict())
|
|
789
|
+
elif isinstance(input_content, dict):
|
|
790
|
+
return json.dumps(input_content, indent=2, default=str)
|
|
791
|
+
elif isinstance(input_content, list):
|
|
792
|
+
if input_content:
|
|
793
|
+
# Handle live Message objects
|
|
794
|
+
if isinstance(input_content[0], Message):
|
|
795
|
+
return json.dumps([m.to_dict() for m in input_content])
|
|
796
|
+
# Handle serialized Message dicts
|
|
797
|
+
elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
|
|
798
|
+
return input_content[0].get("content", str(input_content))
|
|
799
|
+
return str(input_content)
|
|
800
|
+
else:
|
|
801
|
+
return str(input_content)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _get_python_type_from_json_schema(field_schema: Dict[str, Any], field_name: str = "NestedModel") -> Type:
|
|
805
|
+
"""Map JSON schema type to Python type with recursive handling.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
field_schema: JSON schema dictionary for a single field
|
|
809
|
+
field_name: Name of the field (used for nested model naming)
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
Python type corresponding to the JSON schema type
|
|
813
|
+
"""
|
|
814
|
+
if not isinstance(field_schema, dict):
|
|
815
|
+
return Any
|
|
816
|
+
|
|
817
|
+
json_type = field_schema.get("type")
|
|
818
|
+
|
|
819
|
+
# Handle basic types
|
|
820
|
+
if json_type == "string":
|
|
821
|
+
return str
|
|
822
|
+
elif json_type == "integer":
|
|
823
|
+
return int
|
|
824
|
+
elif json_type == "number":
|
|
825
|
+
return float
|
|
826
|
+
elif json_type == "boolean":
|
|
827
|
+
return bool
|
|
828
|
+
elif json_type == "null":
|
|
829
|
+
return type(None)
|
|
830
|
+
elif json_type == "array":
|
|
831
|
+
# Handle arrays with item type specification
|
|
832
|
+
items_schema = field_schema.get("items")
|
|
833
|
+
if items_schema and isinstance(items_schema, dict):
|
|
834
|
+
item_type = _get_python_type_from_json_schema(items_schema, f"{field_name}Item")
|
|
835
|
+
return List[item_type] # type: ignore
|
|
836
|
+
else:
|
|
837
|
+
# No item type specified - use generic list
|
|
838
|
+
return List[Any]
|
|
839
|
+
elif json_type == "object":
|
|
840
|
+
# Recursively create nested Pydantic model
|
|
841
|
+
nested_properties = field_schema.get("properties", {})
|
|
842
|
+
nested_required = field_schema.get("required", [])
|
|
843
|
+
nested_title = field_schema.get("title", field_name)
|
|
844
|
+
|
|
845
|
+
# Build field definitions for nested model
|
|
846
|
+
nested_fields = {}
|
|
847
|
+
for nested_field_name, nested_field_schema in nested_properties.items():
|
|
848
|
+
nested_field_type = _get_python_type_from_json_schema(nested_field_schema, nested_field_name)
|
|
849
|
+
|
|
850
|
+
if nested_field_name in nested_required:
|
|
851
|
+
nested_fields[nested_field_name] = (nested_field_type, ...)
|
|
852
|
+
else:
|
|
853
|
+
nested_fields[nested_field_name] = (Optional[nested_field_type], None) # type: ignore[assignment]
|
|
854
|
+
|
|
855
|
+
# Create nested model if it has fields
|
|
856
|
+
if nested_fields:
|
|
857
|
+
return create_model(nested_title, **nested_fields) # type: ignore
|
|
858
|
+
else:
|
|
859
|
+
# Empty object schema - use generic dict
|
|
860
|
+
return Dict[str, Any]
|
|
861
|
+
else:
|
|
862
|
+
# Unknown or unspecified type - fallback to Any
|
|
863
|
+
if json_type:
|
|
864
|
+
logger.warning(f"Unknown JSON schema type '{json_type}' for field '{field_name}', using Any")
|
|
865
|
+
return Any
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def json_schema_to_pydantic_model(schema: Dict[str, Any]) -> Type[BaseModel]:
|
|
869
|
+
"""Convert a JSON schema dictionary to a Pydantic BaseModel class.
|
|
870
|
+
|
|
871
|
+
This function dynamically creates a Pydantic model from a JSON schema specification,
|
|
872
|
+
handling nested objects, arrays, and optional fields.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
schema: JSON schema dictionary with 'properties', 'required', 'type', etc.
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
Dynamically created Pydantic BaseModel class
|
|
879
|
+
"""
|
|
880
|
+
import copy
|
|
881
|
+
|
|
882
|
+
# Deep copy to avoid modifying the original schema
|
|
883
|
+
schema = copy.deepcopy(schema)
|
|
884
|
+
|
|
885
|
+
# Extract schema components
|
|
886
|
+
model_name = schema.get("title", "DynamicModel")
|
|
887
|
+
properties = schema.get("properties", {})
|
|
888
|
+
required_fields = schema.get("required", [])
|
|
889
|
+
|
|
890
|
+
# Validate schema has properties
|
|
891
|
+
if not properties:
|
|
892
|
+
logger.warning(f"JSON schema '{model_name}' has no properties, creating empty model")
|
|
893
|
+
|
|
894
|
+
# Build field definitions for create_model
|
|
895
|
+
field_definitions = {}
|
|
896
|
+
for field_name, field_schema in properties.items():
|
|
897
|
+
try:
|
|
898
|
+
field_type = _get_python_type_from_json_schema(field_schema, field_name)
|
|
899
|
+
|
|
900
|
+
if field_name in required_fields:
|
|
901
|
+
# Required field: (type, ...)
|
|
902
|
+
field_definitions[field_name] = (field_type, ...)
|
|
903
|
+
else:
|
|
904
|
+
# Optional field: (Optional[type], None)
|
|
905
|
+
field_definitions[field_name] = (Optional[field_type], None) # type: ignore[assignment]
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.warning(f"Failed to process field '{field_name}' in schema '{model_name}': {e}")
|
|
908
|
+
# Skip problematic fields rather than failing entirely
|
|
909
|
+
continue
|
|
910
|
+
|
|
911
|
+
# Create and return the dynamic model
|
|
912
|
+
try:
|
|
913
|
+
return create_model(model_name, **field_definitions) # type: ignore
|
|
914
|
+
except Exception as e:
|
|
915
|
+
logger.error(f"Failed to create dynamic model '{model_name}': {e}")
|
|
916
|
+
# Return a minimal model as fallback
|
|
917
|
+
return create_model(model_name)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def setup_tracing_for_os(db: Union[BaseDb, AsyncBaseDb]) -> None:
|
|
921
|
+
"""Set up OpenTelemetry tracing for this agent/team/workflow."""
|
|
922
|
+
try:
|
|
923
|
+
from agno.tracing import setup_tracing
|
|
924
|
+
|
|
925
|
+
setup_tracing(db=db)
|
|
926
|
+
except ImportError:
|
|
927
|
+
logger.warning(
|
|
928
|
+
"tracing=True but OpenTelemetry packages not installed. "
|
|
929
|
+
"Install with: pip install opentelemetry-api opentelemetry-sdk openinference-instrumentation-agno"
|
|
930
|
+
)
|
|
931
|
+
except Exception as e:
|
|
932
|
+
logger.warning(f"Failed to enable tracing: {e}")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def format_duration_ms(duration_ms: Optional[int]) -> str:
|
|
936
|
+
"""Format a duration in milliseconds to a human-readable string.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
duration_ms: Duration in milliseconds
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
Formatted string like "150ms" or "1.50s"
|
|
943
|
+
"""
|
|
944
|
+
if duration_ms is None or duration_ms < 1000:
|
|
945
|
+
return f"{duration_ms or 0}ms"
|
|
946
|
+
return f"{duration_ms / 1000:.2f}s"
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def parse_datetime_to_utc(datetime_str: str, param_name: str = "datetime") -> "datetime":
|
|
950
|
+
"""Parse an ISO 8601 datetime string and convert to UTC.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
datetime_str: ISO 8601 formatted datetime string (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T15:30:00+05:30')
|
|
954
|
+
param_name: Name of the parameter for error messages
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
datetime object in UTC timezone
|
|
958
|
+
|
|
959
|
+
Raises:
|
|
960
|
+
HTTPException: If the datetime string is invalid
|
|
961
|
+
"""
|
|
962
|
+
try:
|
|
963
|
+
dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00"))
|
|
964
|
+
# Convert to UTC if timezone-aware, otherwise assume UTC
|
|
965
|
+
if dt.tzinfo is not None:
|
|
966
|
+
return dt.astimezone(timezone.utc)
|
|
967
|
+
else:
|
|
968
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
969
|
+
except ValueError as e:
|
|
970
|
+
raise HTTPException(
|
|
971
|
+
status_code=400,
|
|
972
|
+
detail=f"Invalid {param_name} format. Use ISO 8601 format (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T10:00:00+05:30'): {e}",
|
|
973
|
+
)
|