agno 2.2.13__py3-none-any.whl → 2.4.3__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/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- 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/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/os/app.py
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
2
|
from functools import partial
|
|
3
3
|
from os import getenv
|
|
4
|
-
from typing import Any, Dict, List, Literal, Optional,
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, FastAPI, HTTPException
|
|
8
|
+
from fastapi.exceptions import RequestValidationError
|
|
8
9
|
from fastapi.responses import JSONResponse
|
|
9
10
|
from fastapi.routing import APIRoute
|
|
11
|
+
from httpx import HTTPStatusError
|
|
10
12
|
from rich import box
|
|
11
13
|
from rich.panel import Panel
|
|
12
14
|
from starlette.requests import Request
|
|
13
15
|
|
|
14
|
-
from agno.agent
|
|
16
|
+
from agno.agent import Agent, RemoteAgent
|
|
15
17
|
from agno.db.base import AsyncBaseDb, BaseDb
|
|
16
18
|
from agno.knowledge.knowledge import Knowledge
|
|
17
19
|
from agno.os.config import (
|
|
18
20
|
AgentOSConfig,
|
|
21
|
+
AuthorizationConfig,
|
|
19
22
|
DatabaseConfig,
|
|
20
23
|
EvalsConfig,
|
|
21
24
|
EvalsDomainConfig,
|
|
@@ -27,44 +30,77 @@ from agno.os.config import (
|
|
|
27
30
|
MetricsDomainConfig,
|
|
28
31
|
SessionConfig,
|
|
29
32
|
SessionDomainConfig,
|
|
33
|
+
TracesConfig,
|
|
34
|
+
TracesDomainConfig,
|
|
30
35
|
)
|
|
31
36
|
from agno.os.interfaces.base import BaseInterface
|
|
32
37
|
from agno.os.router import get_base_router, get_websocket_router
|
|
38
|
+
from agno.os.routers.agents import get_agent_router
|
|
39
|
+
from agno.os.routers.components import get_components_router
|
|
40
|
+
from agno.os.routers.database import get_database_router
|
|
33
41
|
from agno.os.routers.evals import get_eval_router
|
|
34
42
|
from agno.os.routers.health import get_health_router
|
|
35
43
|
from agno.os.routers.home import get_home_router
|
|
36
44
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
37
45
|
from agno.os.routers.memory import get_memory_router
|
|
38
46
|
from agno.os.routers.metrics import get_metrics_router
|
|
47
|
+
from agno.os.routers.registry import get_registry_router
|
|
39
48
|
from agno.os.routers.session import get_session_router
|
|
49
|
+
from agno.os.routers.teams import get_team_router
|
|
50
|
+
from agno.os.routers.traces import get_traces_router
|
|
51
|
+
from agno.os.routers.workflows import get_workflow_router
|
|
40
52
|
from agno.os.settings import AgnoAPISettings
|
|
41
53
|
from agno.os.utils import (
|
|
42
54
|
collect_mcp_tools_from_team,
|
|
43
55
|
collect_mcp_tools_from_workflow,
|
|
44
56
|
find_conflicting_routes,
|
|
45
57
|
load_yaml_config,
|
|
58
|
+
resolve_origins,
|
|
59
|
+
setup_tracing_for_os,
|
|
46
60
|
update_cors_middleware,
|
|
47
61
|
)
|
|
48
|
-
from agno.
|
|
49
|
-
from agno.
|
|
62
|
+
from agno.registry import Registry
|
|
63
|
+
from agno.remote.base import RemoteDb, RemoteKnowledge
|
|
64
|
+
from agno.team import RemoteTeam, Team
|
|
65
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
50
66
|
from agno.utils.string import generate_id, generate_id_from_name
|
|
51
|
-
from agno.workflow
|
|
67
|
+
from agno.workflow import RemoteWorkflow, Workflow
|
|
52
68
|
|
|
53
69
|
|
|
54
70
|
@asynccontextmanager
|
|
55
71
|
async def mcp_lifespan(_, mcp_tools):
|
|
56
72
|
"""Manage MCP connection lifecycle inside a FastAPI app"""
|
|
57
|
-
# Startup logic: connect to all contextual MCP servers
|
|
58
73
|
for tool in mcp_tools:
|
|
59
74
|
await tool.connect()
|
|
60
75
|
|
|
61
76
|
yield
|
|
62
77
|
|
|
63
|
-
# Shutdown logic: Close all contextual MCP connections
|
|
64
78
|
for tool in mcp_tools:
|
|
65
79
|
await tool.close()
|
|
66
80
|
|
|
67
81
|
|
|
82
|
+
@asynccontextmanager
|
|
83
|
+
async def http_client_lifespan(_):
|
|
84
|
+
"""Manage httpx client lifecycle for proper connection pool cleanup."""
|
|
85
|
+
from agno.utils.http import aclose_default_clients
|
|
86
|
+
|
|
87
|
+
yield
|
|
88
|
+
|
|
89
|
+
await aclose_default_clients()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@asynccontextmanager
|
|
93
|
+
async def db_lifespan(app: FastAPI, agent_os: "AgentOS"):
|
|
94
|
+
"""Initializes databases in the event loop and closes them on shutdown."""
|
|
95
|
+
if agent_os.auto_provision_dbs:
|
|
96
|
+
agent_os._initialize_sync_databases()
|
|
97
|
+
await agent_os._initialize_async_databases()
|
|
98
|
+
|
|
99
|
+
yield
|
|
100
|
+
|
|
101
|
+
await agent_os._close_databases()
|
|
102
|
+
|
|
103
|
+
|
|
68
104
|
def _combine_app_lifespans(lifespans: list) -> Any:
|
|
69
105
|
"""Combine multiple FastAPI app lifespan context managers into one."""
|
|
70
106
|
if len(lifespans) == 1:
|
|
@@ -96,24 +132,27 @@ class AgentOS:
|
|
|
96
132
|
name: Optional[str] = None,
|
|
97
133
|
description: Optional[str] = None,
|
|
98
134
|
version: Optional[str] = None,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
136
|
+
agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
|
|
137
|
+
teams: Optional[List[Union[Team, RemoteTeam]]] = None,
|
|
138
|
+
workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
|
|
102
139
|
knowledge: Optional[List[Knowledge]] = None,
|
|
103
140
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
104
141
|
a2a_interface: bool = False,
|
|
142
|
+
authorization: bool = False,
|
|
143
|
+
authorization_config: Optional[AuthorizationConfig] = None,
|
|
144
|
+
cors_allowed_origins: Optional[List[str]] = None,
|
|
105
145
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
106
146
|
settings: Optional[AgnoAPISettings] = None,
|
|
107
147
|
lifespan: Optional[Any] = None,
|
|
108
148
|
enable_mcp_server: bool = False,
|
|
109
149
|
base_app: Optional[FastAPI] = None,
|
|
110
150
|
on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
|
|
111
|
-
|
|
151
|
+
tracing: bool = False,
|
|
112
152
|
auto_provision_dbs: bool = True,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
replace_routes: Optional[bool] = None, # Deprecated
|
|
153
|
+
run_hooks_in_background: bool = False,
|
|
154
|
+
telemetry: bool = True,
|
|
155
|
+
registry: Optional[Registry] = None,
|
|
117
156
|
):
|
|
118
157
|
"""Initialize AgentOS.
|
|
119
158
|
|
|
@@ -122,6 +161,7 @@ class AgentOS:
|
|
|
122
161
|
name: Name of the AgentOS instance
|
|
123
162
|
description: Description of the AgentOS instance
|
|
124
163
|
version: Version of the AgentOS instance
|
|
164
|
+
db: Default database for the AgentOS instance. Agents, teams and workflows with no db will use this one.
|
|
125
165
|
agents: List of agents to include in the OS
|
|
126
166
|
teams: List of teams to include in the OS
|
|
127
167
|
workflows: List of workflows to include in the OS
|
|
@@ -134,17 +174,24 @@ class AgentOS:
|
|
|
134
174
|
enable_mcp_server: Whether to enable MCP (Model Context Protocol)
|
|
135
175
|
base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
|
|
136
176
|
on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
|
|
177
|
+
auto_provision_dbs: Whether to automatically provision databases
|
|
178
|
+
authorization: Whether to enable authorization
|
|
179
|
+
authorization_config: Configuration for the authorization middleware
|
|
180
|
+
cors_allowed_origins: List of allowed CORS origins (will be merged with default Agno domains)
|
|
181
|
+
tracing: If True, enables OpenTelemetry tracing for all agents and teams in the OS
|
|
182
|
+
run_hooks_in_background: If True, run agent/team pre/post hooks as FastAPI background tasks (non-blocking)
|
|
137
183
|
telemetry: Whether to enable telemetry
|
|
184
|
+
registry: Optional registry to use for the AgentOS
|
|
138
185
|
|
|
139
186
|
"""
|
|
140
|
-
if not agents and not workflows and not teams and not knowledge:
|
|
141
|
-
raise ValueError("Either agents, teams, workflows
|
|
187
|
+
if not agents and not workflows and not teams and not knowledge and not db:
|
|
188
|
+
raise ValueError("Either agents, teams, workflows, knowledge bases or a database must be provided.")
|
|
142
189
|
|
|
143
190
|
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
144
191
|
|
|
145
|
-
self.agents: Optional[List[Agent]] = agents
|
|
146
|
-
self.workflows: Optional[List[Workflow]] = workflows
|
|
147
|
-
self.teams: Optional[List[Team]] = teams
|
|
192
|
+
self.agents: Optional[List[Union[Agent, RemoteAgent]]] = agents
|
|
193
|
+
self.workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = workflows
|
|
194
|
+
self.teams: Optional[List[Union[Team, RemoteTeam]]] = teams
|
|
148
195
|
self.interfaces = interfaces or []
|
|
149
196
|
self.a2a_interface = a2a_interface
|
|
150
197
|
self.knowledge = knowledge
|
|
@@ -156,13 +203,6 @@ class AgentOS:
|
|
|
156
203
|
self.base_app: Optional[FastAPI] = base_app
|
|
157
204
|
self._app_set = True
|
|
158
205
|
self.on_route_conflict = on_route_conflict
|
|
159
|
-
elif fastapi_app:
|
|
160
|
-
self.base_app = fastapi_app
|
|
161
|
-
self._app_set = True
|
|
162
|
-
if replace_routes is not None:
|
|
163
|
-
self.on_route_conflict = "preserve_agentos" if replace_routes else "preserve_base_app"
|
|
164
|
-
else:
|
|
165
|
-
self.on_route_conflict = on_route_conflict
|
|
166
206
|
else:
|
|
167
207
|
self.base_app = None
|
|
168
208
|
self._app_set = False
|
|
@@ -172,18 +212,32 @@ class AgentOS:
|
|
|
172
212
|
|
|
173
213
|
self.name = name
|
|
174
214
|
|
|
175
|
-
self.id = id
|
|
215
|
+
self.id = id
|
|
176
216
|
if not self.id:
|
|
177
217
|
self.id = generate_id(self.name) if self.name else str(uuid4())
|
|
178
218
|
|
|
179
219
|
self.version = version
|
|
180
220
|
self.description = description
|
|
221
|
+
self.db = db
|
|
181
222
|
|
|
182
223
|
self.telemetry = telemetry
|
|
224
|
+
self.tracing = tracing
|
|
183
225
|
|
|
184
|
-
self.enable_mcp_server =
|
|
226
|
+
self.enable_mcp_server = enable_mcp_server
|
|
185
227
|
self.lifespan = lifespan
|
|
186
228
|
|
|
229
|
+
self.registry = registry
|
|
230
|
+
|
|
231
|
+
# RBAC
|
|
232
|
+
self.authorization = authorization
|
|
233
|
+
self.authorization_config = authorization_config
|
|
234
|
+
|
|
235
|
+
# CORS configuration - merge user-provided origins with defaults from settings
|
|
236
|
+
self.cors_allowed_origins = resolve_origins(cors_allowed_origins, self.settings.cors_origin_list)
|
|
237
|
+
|
|
238
|
+
# If True, run agent/team hooks as FastAPI background tasks
|
|
239
|
+
self.run_hooks_in_background = run_hooks_in_background
|
|
240
|
+
|
|
187
241
|
# List of all MCP tools used inside the AgentOS
|
|
188
242
|
self.mcp_tools: List[Any] = []
|
|
189
243
|
self._mcp_app: Optional[Any] = None
|
|
@@ -192,6 +246,12 @@ class AgentOS:
|
|
|
192
246
|
self._initialize_teams()
|
|
193
247
|
self._initialize_workflows()
|
|
194
248
|
|
|
249
|
+
# Check for duplicate IDs
|
|
250
|
+
self._raise_if_duplicate_ids()
|
|
251
|
+
|
|
252
|
+
if self.tracing:
|
|
253
|
+
self._setup_tracing()
|
|
254
|
+
|
|
195
255
|
if self.telemetry:
|
|
196
256
|
from agno.api.os import OSLaunch, log_os_telemetry
|
|
197
257
|
|
|
@@ -230,6 +290,9 @@ class AgentOS:
|
|
|
230
290
|
self._initialize_agents()
|
|
231
291
|
self._initialize_teams()
|
|
232
292
|
self._initialize_workflows()
|
|
293
|
+
|
|
294
|
+
# Check for duplicate IDs
|
|
295
|
+
self._raise_if_duplicate_ids()
|
|
233
296
|
self._auto_discover_databases()
|
|
234
297
|
self._auto_discover_knowledge_instances()
|
|
235
298
|
|
|
@@ -243,12 +306,21 @@ class AgentOS:
|
|
|
243
306
|
def _reprovision_routers(self, app: FastAPI) -> None:
|
|
244
307
|
"""Re-provision all routes for the AgentOS."""
|
|
245
308
|
updated_routers = [
|
|
309
|
+
get_home_router(self),
|
|
246
310
|
get_session_router(dbs=self.dbs),
|
|
247
|
-
get_metrics_router(dbs=self.dbs),
|
|
248
|
-
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
249
311
|
get_memory_router(dbs=self.dbs),
|
|
250
312
|
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
313
|
+
get_metrics_router(dbs=self.dbs),
|
|
314
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
315
|
+
get_traces_router(dbs=self.dbs),
|
|
316
|
+
get_database_router(self, settings=self.settings),
|
|
251
317
|
]
|
|
318
|
+
# Add component and registry routers only if a sync db (BaseDb) is available
|
|
319
|
+
# Component routes require sync database operations
|
|
320
|
+
if self.db is not None and isinstance(self.db, BaseDb):
|
|
321
|
+
updated_routers.append(get_components_router(os_db=self.db, registry=self.registry))
|
|
322
|
+
if self.registry is not None:
|
|
323
|
+
updated_routers.append(get_registry_router(registry=self.registry))
|
|
252
324
|
|
|
253
325
|
# Clear all previously existing routes
|
|
254
326
|
app.router.routes = [
|
|
@@ -278,6 +350,9 @@ class AgentOS:
|
|
|
278
350
|
|
|
279
351
|
self._add_router(app, get_health_router(health_endpoint="/health"))
|
|
280
352
|
self._add_router(app, get_base_router(self, settings=self.settings))
|
|
353
|
+
self._add_router(app, get_agent_router(self, settings=self.settings, registry=self.registry))
|
|
354
|
+
self._add_router(app, get_team_router(self, settings=self.settings, registry=self.registry))
|
|
355
|
+
self._add_router(app, get_workflow_router(self, settings=self.settings))
|
|
281
356
|
self._add_router(app, get_websocket_router(self, settings=self.settings))
|
|
282
357
|
|
|
283
358
|
# Add A2A interface if relevant
|
|
@@ -294,6 +369,31 @@ class AgentOS:
|
|
|
294
369
|
self.interfaces.append(a2a_interface)
|
|
295
370
|
self._add_router(app, a2a_interface.get_router())
|
|
296
371
|
|
|
372
|
+
def _raise_if_duplicate_ids(self) -> None:
|
|
373
|
+
"""Check for duplicate IDs within each entity type.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: If duplicate IDs are found within the same entity type
|
|
377
|
+
"""
|
|
378
|
+
duplicate_ids: List[str] = []
|
|
379
|
+
|
|
380
|
+
for entities in [self.agents, self.teams, self.workflows]:
|
|
381
|
+
if not entities:
|
|
382
|
+
continue
|
|
383
|
+
seen_ids: set[str] = set()
|
|
384
|
+
for entity in entities:
|
|
385
|
+
entity_id = entity.id
|
|
386
|
+
if entity_id is None:
|
|
387
|
+
continue
|
|
388
|
+
if entity_id in seen_ids:
|
|
389
|
+
if entity_id not in duplicate_ids:
|
|
390
|
+
duplicate_ids.append(entity_id)
|
|
391
|
+
else:
|
|
392
|
+
seen_ids.add(entity_id)
|
|
393
|
+
|
|
394
|
+
if duplicate_ids:
|
|
395
|
+
raise ValueError(f"Duplicate IDs found in AgentOS: {', '.join(repr(id_) for id_ in duplicate_ids)}")
|
|
396
|
+
|
|
297
397
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
298
398
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
299
399
|
app_lifespan = lifespan
|
|
@@ -327,28 +427,43 @@ class AgentOS:
|
|
|
327
427
|
"""Initialize and configure all agents for AgentOS usage."""
|
|
328
428
|
if not self.agents:
|
|
329
429
|
return
|
|
330
|
-
|
|
331
430
|
for agent in self.agents:
|
|
431
|
+
if isinstance(agent, RemoteAgent):
|
|
432
|
+
continue
|
|
433
|
+
# Set the default db to agents without their own
|
|
434
|
+
if self.db is not None and agent.db is None:
|
|
435
|
+
agent.db = self.db
|
|
332
436
|
# Track all MCP tools to later handle their connection
|
|
333
437
|
if agent.tools:
|
|
334
438
|
for tool in agent.tools:
|
|
335
|
-
# Checking if the tool is
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if
|
|
339
|
-
self.mcp_tools
|
|
439
|
+
# Checking if the tool is an instance of MCPTools, MultiMCPTools, or a subclass of those
|
|
440
|
+
if hasattr(type(tool), "__mro__"):
|
|
441
|
+
mro_names = {cls.__name__ for cls in type(tool).__mro__}
|
|
442
|
+
if mro_names & {"MCPTools", "MultiMCPTools"}:
|
|
443
|
+
if tool not in self.mcp_tools:
|
|
444
|
+
self.mcp_tools.append(tool)
|
|
340
445
|
|
|
341
446
|
agent.initialize_agent()
|
|
342
447
|
|
|
343
448
|
# Required for the built-in routes to work
|
|
344
449
|
agent.store_events = True
|
|
345
450
|
|
|
451
|
+
# Propagate run_hooks_in_background setting from AgentOS to agents
|
|
452
|
+
agent._run_hooks_in_background = self.run_hooks_in_background
|
|
453
|
+
|
|
346
454
|
def _initialize_teams(self) -> None:
|
|
347
455
|
"""Initialize and configure all teams for AgentOS usage."""
|
|
348
456
|
if not self.teams:
|
|
349
457
|
return
|
|
350
458
|
|
|
351
459
|
for team in self.teams:
|
|
460
|
+
if isinstance(team, RemoteTeam):
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
# Set the default db to teams without their own
|
|
464
|
+
if self.db is not None and team.db is None:
|
|
465
|
+
team.db = self.db
|
|
466
|
+
|
|
352
467
|
# Track all MCP tools recursively
|
|
353
468
|
collect_mcp_tools_from_team(team, self.mcp_tools)
|
|
354
469
|
|
|
@@ -364,21 +479,72 @@ class AgentOS:
|
|
|
364
479
|
# Required for the built-in routes to work
|
|
365
480
|
team.store_events = True
|
|
366
481
|
|
|
482
|
+
# Propagate run_hooks_in_background setting to team and all nested members
|
|
483
|
+
team.propagate_run_hooks_in_background(self.run_hooks_in_background)
|
|
484
|
+
|
|
367
485
|
def _initialize_workflows(self) -> None:
|
|
368
486
|
"""Initialize and configure all workflows for AgentOS usage."""
|
|
369
487
|
if not self.workflows:
|
|
370
488
|
return
|
|
371
489
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
490
|
+
for workflow in self.workflows:
|
|
491
|
+
if isinstance(workflow, RemoteWorkflow):
|
|
492
|
+
continue
|
|
493
|
+
# Set the default db to workflows without their own
|
|
494
|
+
if self.db is not None and workflow.db is None:
|
|
495
|
+
workflow.db = self.db
|
|
496
|
+
|
|
497
|
+
# Track MCP tools recursively in workflow members
|
|
498
|
+
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
499
|
+
|
|
500
|
+
if not workflow.id:
|
|
501
|
+
workflow.id = generate_id_from_name(workflow.name)
|
|
502
|
+
|
|
503
|
+
# Required for the built-in routes to work
|
|
504
|
+
workflow.store_events = True
|
|
505
|
+
|
|
506
|
+
# Propagate run_hooks_in_background setting to workflow and all its step agents/teams
|
|
507
|
+
workflow.propagate_run_hooks_in_background(self.run_hooks_in_background)
|
|
508
|
+
|
|
509
|
+
def _setup_tracing(self) -> None:
|
|
510
|
+
"""Set up OpenTelemetry tracing for this AgentOS.
|
|
376
511
|
|
|
377
|
-
|
|
378
|
-
|
|
512
|
+
Uses the AgentOS db if provided, otherwise falls back to the first available
|
|
513
|
+
database from agents/teams/workflows.
|
|
514
|
+
"""
|
|
515
|
+
# Use AgentOS db if explicitly provided
|
|
516
|
+
if self.db is not None:
|
|
517
|
+
setup_tracing_for_os(db=self.db)
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
# Fall back to finding the first available database
|
|
521
|
+
db: Optional[Union[BaseDb, AsyncBaseDb, RemoteDb]] = None
|
|
522
|
+
|
|
523
|
+
for agent in self.agents or []:
|
|
524
|
+
if agent.db:
|
|
525
|
+
db = agent.db
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
if db is None:
|
|
529
|
+
for team in self.teams or []:
|
|
530
|
+
if team.db:
|
|
531
|
+
db = team.db
|
|
532
|
+
break
|
|
533
|
+
|
|
534
|
+
if db is None:
|
|
535
|
+
for workflow in self.workflows or []:
|
|
536
|
+
if workflow.db:
|
|
537
|
+
db = workflow.db
|
|
538
|
+
break
|
|
539
|
+
|
|
540
|
+
if db is None:
|
|
541
|
+
log_warning(
|
|
542
|
+
"tracing=True but no database found. "
|
|
543
|
+
"Provide 'db' parameter to AgentOS or to at least one agent/team/workflow."
|
|
544
|
+
)
|
|
545
|
+
return
|
|
379
546
|
|
|
380
|
-
|
|
381
|
-
workflow.store_events = True
|
|
547
|
+
setup_tracing_for_os(db=db)
|
|
382
548
|
|
|
383
549
|
def get_app(self) -> FastAPI:
|
|
384
550
|
if self.base_app:
|
|
@@ -411,41 +577,42 @@ class AgentOS:
|
|
|
411
577
|
if self.enable_mcp_server and self._mcp_app:
|
|
412
578
|
lifespans.append(self._mcp_app.lifespan)
|
|
413
579
|
|
|
580
|
+
# The async database lifespan
|
|
581
|
+
lifespans.append(partial(db_lifespan, agent_os=self))
|
|
582
|
+
|
|
583
|
+
# The httpx client cleanup lifespan (should be last to close after other lifespans)
|
|
584
|
+
lifespans.append(http_client_lifespan)
|
|
585
|
+
|
|
414
586
|
# Combine lifespans and set them in the app
|
|
415
587
|
if lifespans:
|
|
416
588
|
fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
|
|
417
589
|
|
|
418
590
|
else:
|
|
419
|
-
|
|
420
|
-
from contextlib import asynccontextmanager
|
|
591
|
+
lifespans = []
|
|
421
592
|
|
|
422
|
-
|
|
593
|
+
# User provided lifespan
|
|
594
|
+
if self.lifespan:
|
|
595
|
+
lifespans.append(self._add_agent_os_to_lifespan_function(self.lifespan))
|
|
423
596
|
|
|
424
|
-
|
|
597
|
+
# MCP tools lifespan
|
|
598
|
+
if self.mcp_tools:
|
|
599
|
+
lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
|
|
425
600
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
601
|
+
# MCP server lifespan
|
|
602
|
+
if self.enable_mcp_server:
|
|
603
|
+
from agno.os.mcp import get_mcp_server
|
|
430
604
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
async def combined_lifespan(app: FastAPI):
|
|
434
|
-
# Run both lifespans
|
|
435
|
-
async with wrapped_lifespan(app): # type: ignore
|
|
436
|
-
async with self._mcp_app.lifespan(app): # type: ignore
|
|
437
|
-
yield
|
|
605
|
+
self._mcp_app = get_mcp_server(self)
|
|
606
|
+
lifespans.append(self._mcp_app.lifespan)
|
|
438
607
|
|
|
439
|
-
|
|
608
|
+
# Async database initialization lifespan
|
|
609
|
+
lifespans.append(partial(db_lifespan, agent_os=self)) # type: ignore
|
|
440
610
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
# Wrap the user lifespan with agent_os parameter
|
|
444
|
-
wrapped_user_lifespan = None
|
|
445
|
-
if self.lifespan is not None:
|
|
446
|
-
wrapped_user_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
611
|
+
# The httpx client cleanup lifespan (should be last to close after other lifespans)
|
|
612
|
+
lifespans.append(http_client_lifespan)
|
|
447
613
|
|
|
448
|
-
|
|
614
|
+
final_lifespan = _combine_app_lifespans(lifespans) if lifespans else None
|
|
615
|
+
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
449
616
|
|
|
450
617
|
self._add_built_in_routes(app=fastapi_app)
|
|
451
618
|
|
|
@@ -458,7 +625,15 @@ class AgentOS:
|
|
|
458
625
|
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
459
626
|
get_metrics_router(dbs=self.dbs),
|
|
460
627
|
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
628
|
+
get_traces_router(dbs=self.dbs),
|
|
629
|
+
get_database_router(self, settings=self.settings),
|
|
461
630
|
]
|
|
631
|
+
# Add component and registry routers only if a sync db (BaseDb) is available
|
|
632
|
+
# Component routes require sync database operations
|
|
633
|
+
if self.db is not None and isinstance(self.db, BaseDb):
|
|
634
|
+
routers.append(get_components_router(os_db=self.db, registry=self.registry))
|
|
635
|
+
if self.registry is not None:
|
|
636
|
+
routers.append(get_registry_router(registry=self.registry))
|
|
462
637
|
|
|
463
638
|
for router in routers:
|
|
464
639
|
self._add_router(fastapi_app, router)
|
|
@@ -469,6 +644,14 @@ class AgentOS:
|
|
|
469
644
|
|
|
470
645
|
if not self._app_set:
|
|
471
646
|
|
|
647
|
+
@fastapi_app.exception_handler(RequestValidationError)
|
|
648
|
+
async def validation_exception_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
|
|
649
|
+
log_error(f"Validation error (422): {exc.errors()}")
|
|
650
|
+
return JSONResponse(
|
|
651
|
+
status_code=422,
|
|
652
|
+
content={"detail": exc.errors()},
|
|
653
|
+
)
|
|
654
|
+
|
|
472
655
|
@fastapi_app.exception_handler(HTTPException)
|
|
473
656
|
async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
|
|
474
657
|
log_error(f"HTTP exception: {exc.status_code} {exc.detail}")
|
|
@@ -477,6 +660,16 @@ class AgentOS:
|
|
|
477
660
|
content={"detail": str(exc.detail)},
|
|
478
661
|
)
|
|
479
662
|
|
|
663
|
+
@fastapi_app.exception_handler(HTTPStatusError)
|
|
664
|
+
async def http_status_error_handler(_: Request, exc: HTTPStatusError) -> JSONResponse:
|
|
665
|
+
status_code = exc.response.status_code
|
|
666
|
+
detail = exc.response.text
|
|
667
|
+
log_error(f"Downstream server returned HTTP status error: {status_code} {detail}")
|
|
668
|
+
return JSONResponse(
|
|
669
|
+
status_code=status_code,
|
|
670
|
+
content={"detail": detail},
|
|
671
|
+
)
|
|
672
|
+
|
|
480
673
|
@fastapi_app.exception_handler(Exception)
|
|
481
674
|
async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
482
675
|
import traceback
|
|
@@ -489,10 +682,70 @@ class AgentOS:
|
|
|
489
682
|
)
|
|
490
683
|
|
|
491
684
|
# Update CORS middleware
|
|
492
|
-
update_cors_middleware(fastapi_app, self.
|
|
685
|
+
update_cors_middleware(fastapi_app, self.cors_allowed_origins) # type: ignore
|
|
686
|
+
|
|
687
|
+
# Set agent_os_id and cors_allowed_origins on app state
|
|
688
|
+
# This allows middleware (like JWT) to access these values
|
|
689
|
+
fastapi_app.state.agent_os_id = self.id
|
|
690
|
+
fastapi_app.state.cors_allowed_origins = self.cors_allowed_origins
|
|
691
|
+
|
|
692
|
+
# Add JWT middleware if authorization is enabled
|
|
693
|
+
if self.authorization:
|
|
694
|
+
# Set authorization_enabled flag on settings so security key validation is skipped
|
|
695
|
+
self.settings.authorization_enabled = True
|
|
696
|
+
|
|
697
|
+
jwt_configured = bool(getenv("JWT_VERIFICATION_KEY") or getenv("JWT_JWKS_FILE"))
|
|
698
|
+
security_key_set = bool(self.settings.os_security_key)
|
|
699
|
+
if jwt_configured and security_key_set:
|
|
700
|
+
log_warning(
|
|
701
|
+
"Both JWT configuration (JWT_VERIFICATION_KEY or JWT_JWKS_FILE) and OS_SECURITY_KEY are set. "
|
|
702
|
+
"With authorization=True, only JWT authorization will be used. "
|
|
703
|
+
"Consider removing OS_SECURITY_KEY from your environment."
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
self._add_jwt_middleware(fastapi_app)
|
|
707
|
+
|
|
708
|
+
# Add trailing slash normalization middleware
|
|
709
|
+
from agno.os.middleware.trailing_slash import TrailingSlashMiddleware
|
|
710
|
+
|
|
711
|
+
fastapi_app.add_middleware(TrailingSlashMiddleware)
|
|
493
712
|
|
|
494
713
|
return fastapi_app
|
|
495
714
|
|
|
715
|
+
def _add_jwt_middleware(self, fastapi_app: FastAPI) -> None:
|
|
716
|
+
from agno.os.middleware.jwt import JWTMiddleware, JWTValidator
|
|
717
|
+
|
|
718
|
+
verify_audience = False
|
|
719
|
+
jwks_file = None
|
|
720
|
+
verification_keys = None
|
|
721
|
+
algorithm = "RS256"
|
|
722
|
+
|
|
723
|
+
if self.authorization_config:
|
|
724
|
+
algorithm = self.authorization_config.algorithm or "RS256"
|
|
725
|
+
verification_keys = self.authorization_config.verification_keys
|
|
726
|
+
jwks_file = self.authorization_config.jwks_file
|
|
727
|
+
verify_audience = self.authorization_config.verify_audience or False
|
|
728
|
+
|
|
729
|
+
log_info(f"Adding JWT middleware for authorization (algorithm: {algorithm})")
|
|
730
|
+
|
|
731
|
+
# Create validator and store on app.state for WebSocket access
|
|
732
|
+
jwt_validator = JWTValidator(
|
|
733
|
+
verification_keys=verification_keys,
|
|
734
|
+
jwks_file=jwks_file,
|
|
735
|
+
algorithm=algorithm,
|
|
736
|
+
)
|
|
737
|
+
fastapi_app.state.jwt_validator = jwt_validator
|
|
738
|
+
|
|
739
|
+
# Add middleware to stack
|
|
740
|
+
fastapi_app.add_middleware(
|
|
741
|
+
JWTMiddleware,
|
|
742
|
+
verification_keys=verification_keys,
|
|
743
|
+
jwks_file=jwks_file,
|
|
744
|
+
algorithm=algorithm,
|
|
745
|
+
authorization=self.authorization,
|
|
746
|
+
verify_audience=verify_audience,
|
|
747
|
+
)
|
|
748
|
+
|
|
496
749
|
def get_routes(self) -> List[Any]:
|
|
497
750
|
"""Retrieve all routes from the FastAPI app.
|
|
498
751
|
|
|
@@ -561,32 +814,43 @@ class AgentOS:
|
|
|
561
814
|
|
|
562
815
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
563
816
|
"""Get the telemetry data for the OS"""
|
|
817
|
+
agent_ids = []
|
|
818
|
+
team_ids = []
|
|
819
|
+
workflow_ids = []
|
|
820
|
+
for agent in self.agents or []:
|
|
821
|
+
agent_ids.append(agent.id)
|
|
822
|
+
for team in self.teams or []:
|
|
823
|
+
team_ids.append(team.id)
|
|
824
|
+
for workflow in self.workflows or []:
|
|
825
|
+
workflow_ids.append(workflow.id)
|
|
564
826
|
return {
|
|
565
|
-
"agents":
|
|
566
|
-
"teams":
|
|
567
|
-
"workflows":
|
|
827
|
+
"agents": agent_ids,
|
|
828
|
+
"teams": team_ids,
|
|
829
|
+
"workflows": workflow_ids,
|
|
568
830
|
"interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
|
|
569
831
|
}
|
|
570
832
|
|
|
571
833
|
def _auto_discover_databases(self) -> None:
|
|
572
834
|
"""Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
|
|
573
835
|
|
|
574
|
-
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
|
|
836
|
+
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]] = {}
|
|
575
837
|
knowledge_dbs: Dict[
|
|
576
|
-
str, List[Union[BaseDb, AsyncBaseDb]]
|
|
838
|
+
str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]
|
|
577
839
|
] = {} # Track databases specifically used for knowledge
|
|
578
840
|
|
|
579
841
|
for agent in self.agents or []:
|
|
580
842
|
if agent.db:
|
|
581
843
|
self._register_db_with_validation(dbs, agent.db)
|
|
582
|
-
|
|
583
|
-
|
|
844
|
+
agent_contents_db = getattr(agent.knowledge, "contents_db", None) if agent.knowledge else None
|
|
845
|
+
if agent_contents_db:
|
|
846
|
+
self._register_db_with_validation(knowledge_dbs, agent_contents_db)
|
|
584
847
|
|
|
585
848
|
for team in self.teams or []:
|
|
586
849
|
if team.db:
|
|
587
850
|
self._register_db_with_validation(dbs, team.db)
|
|
588
|
-
|
|
589
|
-
|
|
851
|
+
team_contents_db = getattr(team.knowledge, "contents_db", None) if team.knowledge else None
|
|
852
|
+
if team_contents_db:
|
|
853
|
+
self._register_db_with_validation(knowledge_dbs, team_contents_db)
|
|
590
854
|
|
|
591
855
|
for workflow in self.workflows or []:
|
|
592
856
|
if workflow.db:
|
|
@@ -602,39 +866,21 @@ class AgentOS:
|
|
|
602
866
|
elif interface.team and interface.team.db:
|
|
603
867
|
self._register_db_with_validation(dbs, interface.team.db)
|
|
604
868
|
|
|
869
|
+
# Register AgentOS db if provided
|
|
870
|
+
if self.db is not None:
|
|
871
|
+
self._register_db_with_validation(dbs, self.db)
|
|
872
|
+
|
|
605
873
|
self.dbs = dbs
|
|
606
874
|
self.knowledge_dbs = knowledge_dbs
|
|
607
875
|
|
|
608
|
-
# Initialize
|
|
876
|
+
# Initialize all discovered databases
|
|
609
877
|
if self.auto_provision_dbs:
|
|
610
|
-
|
|
611
|
-
import concurrent.futures
|
|
878
|
+
self._pending_async_db_init = True
|
|
612
879
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
asyncio.get_running_loop()
|
|
616
|
-
|
|
617
|
-
def run_in_new_loop():
|
|
618
|
-
new_loop = asyncio.new_event_loop()
|
|
619
|
-
asyncio.set_event_loop(new_loop)
|
|
620
|
-
try:
|
|
621
|
-
return new_loop.run_until_complete(self._initialize_databases())
|
|
622
|
-
finally:
|
|
623
|
-
new_loop.close()
|
|
624
|
-
|
|
625
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
626
|
-
future = executor.submit(run_in_new_loop)
|
|
627
|
-
future.result() # Wait for completion
|
|
628
|
-
|
|
629
|
-
except RuntimeError:
|
|
630
|
-
# No event loop running, use asyncio.run
|
|
631
|
-
asyncio.run(self._initialize_databases())
|
|
632
|
-
|
|
633
|
-
async def _initialize_databases(self) -> None:
|
|
634
|
-
"""Initialize all discovered databases and create all Agno tables that don't exist yet."""
|
|
880
|
+
def _initialize_sync_databases(self) -> None:
|
|
881
|
+
"""Initialize sync databases."""
|
|
635
882
|
from itertools import chain
|
|
636
883
|
|
|
637
|
-
# Collect all database instances and remove duplicates by identity
|
|
638
884
|
unique_dbs = list(
|
|
639
885
|
{
|
|
640
886
|
id(db): db
|
|
@@ -644,37 +890,65 @@ class AgentOS:
|
|
|
644
890
|
}.values()
|
|
645
891
|
)
|
|
646
892
|
|
|
647
|
-
# Separate sync and async databases
|
|
648
|
-
sync_dbs: List[Tuple[str, BaseDb]] = []
|
|
649
|
-
async_dbs: List[Tuple[str, AsyncBaseDb]] = []
|
|
650
|
-
|
|
651
893
|
for db in unique_dbs:
|
|
652
|
-
|
|
653
|
-
|
|
894
|
+
if isinstance(db, AsyncBaseDb):
|
|
895
|
+
continue # Skip async dbs
|
|
654
896
|
|
|
655
|
-
# Initialize sync databases
|
|
656
|
-
for db_id, db in sync_dbs:
|
|
657
897
|
try:
|
|
658
|
-
if hasattr(db, "_create_all_tables") and callable(
|
|
898
|
+
if hasattr(db, "_create_all_tables") and callable(db._create_all_tables):
|
|
659
899
|
db._create_all_tables()
|
|
660
|
-
else:
|
|
661
|
-
log_debug(f"No table initialization needed for {db.__class__.__name__}")
|
|
662
|
-
|
|
663
900
|
except Exception as e:
|
|
664
|
-
log_warning(f"Failed to initialize {db.__class__.__name__} (id: {
|
|
901
|
+
log_warning(f"Failed to initialize {db.__class__.__name__} (id: {db.id}): {e}")
|
|
665
902
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
903
|
+
async def _initialize_async_databases(self) -> None:
|
|
904
|
+
"""Initialize async databases."""
|
|
905
|
+
|
|
906
|
+
from itertools import chain
|
|
907
|
+
|
|
908
|
+
unique_dbs = list(
|
|
909
|
+
{
|
|
910
|
+
id(db): db
|
|
911
|
+
for db in chain(
|
|
912
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
913
|
+
)
|
|
914
|
+
}.values()
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
for db in unique_dbs:
|
|
918
|
+
if not isinstance(db, AsyncBaseDb):
|
|
919
|
+
continue # Skip sync dbs
|
|
670
920
|
|
|
671
|
-
|
|
921
|
+
try:
|
|
922
|
+
if hasattr(db, "_create_all_tables") and callable(db._create_all_tables):
|
|
672
923
|
await db._create_all_tables()
|
|
673
|
-
|
|
674
|
-
|
|
924
|
+
except Exception as e:
|
|
925
|
+
log_warning(f"Failed to initialize async {db.__class__.__name__} (id: {db.id}): {e}")
|
|
926
|
+
|
|
927
|
+
async def _close_databases(self) -> None:
|
|
928
|
+
"""Close all database connections and release connection pools."""
|
|
929
|
+
from itertools import chain
|
|
930
|
+
|
|
931
|
+
if not hasattr(self, "dbs") or not hasattr(self, "knowledge_dbs"):
|
|
932
|
+
return
|
|
933
|
+
|
|
934
|
+
unique_dbs = list(
|
|
935
|
+
{
|
|
936
|
+
id(db): db
|
|
937
|
+
for db in chain(
|
|
938
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
939
|
+
)
|
|
940
|
+
}.values()
|
|
941
|
+
)
|
|
675
942
|
|
|
943
|
+
for db in unique_dbs:
|
|
944
|
+
try:
|
|
945
|
+
if hasattr(db, "close") and callable(db.close):
|
|
946
|
+
if isinstance(db, AsyncBaseDb):
|
|
947
|
+
await db.close()
|
|
948
|
+
else:
|
|
949
|
+
db.close()
|
|
676
950
|
except Exception as e:
|
|
677
|
-
log_warning(f"Failed to
|
|
951
|
+
log_warning(f"Failed to close {db.__class__.__name__} (id: {db.id}): {e}")
|
|
678
952
|
|
|
679
953
|
def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
|
|
680
954
|
"""Get the table names for a database"""
|
|
@@ -689,7 +963,9 @@ class AgentOS:
|
|
|
689
963
|
return {k: v for k, v in table_names.items() if v is not None}
|
|
690
964
|
|
|
691
965
|
def _register_db_with_validation(
|
|
692
|
-
self,
|
|
966
|
+
self,
|
|
967
|
+
registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]],
|
|
968
|
+
db: Union[BaseDb, AsyncBaseDb, RemoteDb],
|
|
693
969
|
) -> None:
|
|
694
970
|
"""Register a database in the contextual OS after validating it is not conflicting with registered databases"""
|
|
695
971
|
if db.id in registered_dbs:
|
|
@@ -699,18 +975,21 @@ class AgentOS:
|
|
|
699
975
|
|
|
700
976
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
701
977
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
702
|
-
seen_ids = set()
|
|
703
|
-
knowledge_instances: List[Knowledge] = []
|
|
978
|
+
seen_ids: set[str] = set()
|
|
979
|
+
knowledge_instances: List[Union[Knowledge, RemoteKnowledge]] = []
|
|
704
980
|
|
|
705
|
-
def _add_knowledge_if_not_duplicate(knowledge:
|
|
981
|
+
def _add_knowledge_if_not_duplicate(knowledge: Any) -> None:
|
|
706
982
|
"""Add knowledge instance if it's not already in the list (by object identity or db_id)."""
|
|
707
|
-
#
|
|
708
|
-
|
|
983
|
+
# Only handle Knowledge and RemoteKnowledge instances that have contents_db
|
|
984
|
+
contents_db = getattr(knowledge, "contents_db", None)
|
|
985
|
+
if not contents_db:
|
|
709
986
|
return
|
|
710
|
-
if
|
|
987
|
+
if contents_db.id in seen_ids:
|
|
711
988
|
return
|
|
712
|
-
seen_ids.add(
|
|
713
|
-
|
|
989
|
+
seen_ids.add(contents_db.id)
|
|
990
|
+
# Only append if it's a Knowledge or RemoteKnowledge instance
|
|
991
|
+
if isinstance(knowledge, (Knowledge, RemoteKnowledge)):
|
|
992
|
+
knowledge_instances.append(knowledge)
|
|
714
993
|
|
|
715
994
|
for agent in self.agents or []:
|
|
716
995
|
if agent.knowledge:
|
|
@@ -832,6 +1111,36 @@ class AgentOS:
|
|
|
832
1111
|
|
|
833
1112
|
return evals_config
|
|
834
1113
|
|
|
1114
|
+
def _get_traces_config(self) -> TracesConfig:
|
|
1115
|
+
traces_config = self.config.traces if self.config and self.config.traces else TracesConfig()
|
|
1116
|
+
|
|
1117
|
+
if traces_config.dbs is None:
|
|
1118
|
+
traces_config.dbs = []
|
|
1119
|
+
|
|
1120
|
+
dbs_with_specific_config = [db.db_id for db in traces_config.dbs]
|
|
1121
|
+
|
|
1122
|
+
# If AgentOS db is explicitly set, only use that database for traces
|
|
1123
|
+
if self.db is not None:
|
|
1124
|
+
if self.db.id not in dbs_with_specific_config:
|
|
1125
|
+
traces_config.dbs.append(
|
|
1126
|
+
DatabaseConfig(
|
|
1127
|
+
db_id=self.db.id,
|
|
1128
|
+
domain_config=TracesDomainConfig(display_name=self.db.id),
|
|
1129
|
+
)
|
|
1130
|
+
)
|
|
1131
|
+
else:
|
|
1132
|
+
# Fall back to all discovered databases
|
|
1133
|
+
for db_id in self.dbs.keys():
|
|
1134
|
+
if db_id not in dbs_with_specific_config:
|
|
1135
|
+
traces_config.dbs.append(
|
|
1136
|
+
DatabaseConfig(
|
|
1137
|
+
db_id=db_id,
|
|
1138
|
+
domain_config=TracesDomainConfig(display_name=db_id),
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
return traces_config
|
|
1143
|
+
|
|
835
1144
|
def serve(
|
|
836
1145
|
self,
|
|
837
1146
|
app: Union[str, FastAPI],
|
|
@@ -839,6 +1148,8 @@ class AgentOS:
|
|
|
839
1148
|
host: str = "localhost",
|
|
840
1149
|
port: int = 7777,
|
|
841
1150
|
reload: bool = False,
|
|
1151
|
+
reload_includes: Optional[List[str]] = None,
|
|
1152
|
+
reload_excludes: Optional[List[str]] = None,
|
|
842
1153
|
workers: Optional[int] = None,
|
|
843
1154
|
access_log: bool = False,
|
|
844
1155
|
**kwargs,
|
|
@@ -858,8 +1169,12 @@ class AgentOS:
|
|
|
858
1169
|
Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"),
|
|
859
1170
|
Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}"),
|
|
860
1171
|
]
|
|
861
|
-
if
|
|
862
|
-
panel_group.append(
|
|
1172
|
+
if self.authorization:
|
|
1173
|
+
panel_group.append(
|
|
1174
|
+
Align.center("\n\n[bold chartreuse3]:lock: JWT Authorization Enabled[/bold chartreuse3]")
|
|
1175
|
+
)
|
|
1176
|
+
elif bool(self.settings.os_security_key):
|
|
1177
|
+
panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Key Enabled[/bold chartreuse3]"))
|
|
863
1178
|
|
|
864
1179
|
console = Console()
|
|
865
1180
|
console.print(
|
|
@@ -873,4 +1188,19 @@ class AgentOS:
|
|
|
873
1188
|
)
|
|
874
1189
|
)
|
|
875
1190
|
|
|
876
|
-
|
|
1191
|
+
# Adding *.yaml to reload_includes to reload the app when the yaml config file changes.
|
|
1192
|
+
if reload and reload_includes is not None:
|
|
1193
|
+
reload_includes = ["*.yaml", "*.yml"]
|
|
1194
|
+
|
|
1195
|
+
uvicorn.run(
|
|
1196
|
+
app=app,
|
|
1197
|
+
host=host,
|
|
1198
|
+
port=port,
|
|
1199
|
+
reload=reload,
|
|
1200
|
+
reload_includes=reload_includes,
|
|
1201
|
+
reload_excludes=reload_excludes,
|
|
1202
|
+
workers=workers,
|
|
1203
|
+
access_log=access_log,
|
|
1204
|
+
lifespan="on",
|
|
1205
|
+
**kwargs,
|
|
1206
|
+
)
|