agno 2.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/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- 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/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- 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 +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- 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 +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -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 +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -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 +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- 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/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- 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 +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- 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 +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- 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/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/os/app.py
CHANGED
|
@@ -12,9 +12,11 @@ from rich.panel import Panel
|
|
|
12
12
|
from starlette.requests import Request
|
|
13
13
|
|
|
14
14
|
from agno.agent.agent import Agent
|
|
15
|
-
from agno.db.base import BaseDb
|
|
15
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
16
|
+
from agno.knowledge.knowledge import Knowledge
|
|
16
17
|
from agno.os.config import (
|
|
17
18
|
AgentOSConfig,
|
|
19
|
+
AuthorizationConfig,
|
|
18
20
|
DatabaseConfig,
|
|
19
21
|
EvalsConfig,
|
|
20
22
|
EvalsDomainConfig,
|
|
@@ -26,9 +28,12 @@ from agno.os.config import (
|
|
|
26
28
|
MetricsDomainConfig,
|
|
27
29
|
SessionConfig,
|
|
28
30
|
SessionDomainConfig,
|
|
31
|
+
TracesConfig,
|
|
32
|
+
TracesDomainConfig,
|
|
29
33
|
)
|
|
30
34
|
from agno.os.interfaces.base import BaseInterface
|
|
31
|
-
from agno.os.router import get_base_router
|
|
35
|
+
from agno.os.router import get_base_router
|
|
36
|
+
from agno.os.routers.agents import get_agent_router
|
|
32
37
|
from agno.os.routers.evals import get_eval_router
|
|
33
38
|
from agno.os.routers.health import get_health_router
|
|
34
39
|
from agno.os.routers.home import get_home_router
|
|
@@ -36,16 +41,21 @@ from agno.os.routers.knowledge import get_knowledge_router
|
|
|
36
41
|
from agno.os.routers.memory import get_memory_router
|
|
37
42
|
from agno.os.routers.metrics import get_metrics_router
|
|
38
43
|
from agno.os.routers.session import get_session_router
|
|
44
|
+
from agno.os.routers.teams import get_team_router
|
|
45
|
+
from agno.os.routers.traces import get_traces_router
|
|
46
|
+
from agno.os.routers.workflows import get_websocket_router, get_workflow_router
|
|
39
47
|
from agno.os.settings import AgnoAPISettings
|
|
40
48
|
from agno.os.utils import (
|
|
41
49
|
collect_mcp_tools_from_team,
|
|
42
50
|
collect_mcp_tools_from_workflow,
|
|
43
51
|
find_conflicting_routes,
|
|
44
52
|
load_yaml_config,
|
|
53
|
+
resolve_origins,
|
|
54
|
+
setup_tracing_for_os,
|
|
45
55
|
update_cors_middleware,
|
|
46
56
|
)
|
|
47
57
|
from agno.team.team import Team
|
|
48
|
-
from agno.utils.log import
|
|
58
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
49
59
|
from agno.utils.string import generate_id, generate_id_from_name
|
|
50
60
|
from agno.workflow.workflow import Workflow
|
|
51
61
|
|
|
@@ -64,6 +74,39 @@ async def mcp_lifespan(_, mcp_tools):
|
|
|
64
74
|
await tool.close()
|
|
65
75
|
|
|
66
76
|
|
|
77
|
+
@asynccontextmanager
|
|
78
|
+
async def db_lifespan(app: FastAPI, agent_os: "AgentOS"):
|
|
79
|
+
"""Initializes databases in the event loop"""
|
|
80
|
+
if agent_os.auto_provision_dbs:
|
|
81
|
+
agent_os._initialize_sync_databases()
|
|
82
|
+
await agent_os._initialize_async_databases()
|
|
83
|
+
yield
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _combine_app_lifespans(lifespans: list) -> Any:
|
|
87
|
+
"""Combine multiple FastAPI app lifespan context managers into one."""
|
|
88
|
+
if len(lifespans) == 1:
|
|
89
|
+
return lifespans[0]
|
|
90
|
+
|
|
91
|
+
from contextlib import asynccontextmanager
|
|
92
|
+
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def combined_lifespan(app):
|
|
95
|
+
async def _run_nested(index: int):
|
|
96
|
+
if index >= len(lifespans):
|
|
97
|
+
yield
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
async with lifespans[index](app):
|
|
101
|
+
async for _ in _run_nested(index + 1):
|
|
102
|
+
yield
|
|
103
|
+
|
|
104
|
+
async for _ in _run_nested(0):
|
|
105
|
+
yield
|
|
106
|
+
|
|
107
|
+
return combined_lifespan
|
|
108
|
+
|
|
109
|
+
|
|
67
110
|
class AgentOS:
|
|
68
111
|
def __init__(
|
|
69
112
|
self,
|
|
@@ -74,19 +117,23 @@ class AgentOS:
|
|
|
74
117
|
agents: Optional[List[Agent]] = None,
|
|
75
118
|
teams: Optional[List[Team]] = None,
|
|
76
119
|
workflows: Optional[List[Workflow]] = None,
|
|
120
|
+
knowledge: Optional[List[Knowledge]] = None,
|
|
77
121
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
78
122
|
a2a_interface: bool = False,
|
|
123
|
+
authorization: bool = False,
|
|
124
|
+
authorization_config: Optional[AuthorizationConfig] = None,
|
|
125
|
+
cors_allowed_origins: Optional[List[str]] = None,
|
|
79
126
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
80
127
|
settings: Optional[AgnoAPISettings] = None,
|
|
81
128
|
lifespan: Optional[Any] = None,
|
|
82
129
|
enable_mcp_server: bool = False,
|
|
83
130
|
base_app: Optional[FastAPI] = None,
|
|
84
131
|
on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
|
|
132
|
+
tracing: bool = False,
|
|
133
|
+
tracing_db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
134
|
+
auto_provision_dbs: bool = True,
|
|
135
|
+
run_hooks_in_background: bool = False,
|
|
85
136
|
telemetry: bool = True,
|
|
86
|
-
os_id: Optional[str] = None, # Deprecated
|
|
87
|
-
enable_mcp: bool = False, # Deprecated
|
|
88
|
-
fastapi_app: Optional[FastAPI] = None, # Deprecated
|
|
89
|
-
replace_routes: Optional[bool] = None, # Deprecated
|
|
90
137
|
):
|
|
91
138
|
"""Initialize AgentOS.
|
|
92
139
|
|
|
@@ -98,6 +145,7 @@ class AgentOS:
|
|
|
98
145
|
agents: List of agents to include in the OS
|
|
99
146
|
teams: List of teams to include in the OS
|
|
100
147
|
workflows: List of workflows to include in the OS
|
|
148
|
+
knowledge: List of knowledge bases to include in the OS
|
|
101
149
|
interfaces: List of interfaces to include in the OS
|
|
102
150
|
a2a_interface: Whether to expose the OS agents and teams in an A2A server
|
|
103
151
|
config: Configuration file path or AgentOSConfig instance
|
|
@@ -106,11 +154,19 @@ class AgentOS:
|
|
|
106
154
|
enable_mcp_server: Whether to enable MCP (Model Context Protocol)
|
|
107
155
|
base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
|
|
108
156
|
on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
|
|
157
|
+
auto_provision_dbs: Whether to automatically provision databases
|
|
158
|
+
authorization: Whether to enable authorization
|
|
159
|
+
authorization_config: Configuration for the authorization middleware
|
|
160
|
+
cors_allowed_origins: List of allowed CORS origins (will be merged with default Agno domains)
|
|
161
|
+
tracing: If True, enables OpenTelemetry tracing for all agents and teams in the OS
|
|
162
|
+
tracing_db: Dedicated database for storing and reading traces. Recommended for multi-db setups.
|
|
163
|
+
If not provided and tracing=True, the first available db from agents/teams/workflows is used.
|
|
164
|
+
run_hooks_in_background: If True, run agent/team pre/post hooks as FastAPI background tasks (non-blocking)
|
|
109
165
|
telemetry: Whether to enable telemetry
|
|
110
166
|
|
|
111
167
|
"""
|
|
112
|
-
if not agents and not workflows and not teams:
|
|
113
|
-
raise ValueError("Either agents, teams or
|
|
168
|
+
if not agents and not workflows and not teams and not knowledge:
|
|
169
|
+
raise ValueError("Either agents, teams, workflows or knowledge bases must be provided.")
|
|
114
170
|
|
|
115
171
|
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
116
172
|
|
|
@@ -119,22 +175,15 @@ class AgentOS:
|
|
|
119
175
|
self.teams: Optional[List[Team]] = teams
|
|
120
176
|
self.interfaces = interfaces or []
|
|
121
177
|
self.a2a_interface = a2a_interface
|
|
122
|
-
|
|
178
|
+
self.knowledge = knowledge
|
|
123
179
|
self.settings: AgnoAPISettings = settings or AgnoAPISettings()
|
|
124
|
-
|
|
180
|
+
self.auto_provision_dbs = auto_provision_dbs
|
|
125
181
|
self._app_set = False
|
|
126
182
|
|
|
127
183
|
if base_app:
|
|
128
184
|
self.base_app: Optional[FastAPI] = base_app
|
|
129
185
|
self._app_set = True
|
|
130
186
|
self.on_route_conflict = on_route_conflict
|
|
131
|
-
elif fastapi_app:
|
|
132
|
-
self.base_app = fastapi_app
|
|
133
|
-
self._app_set = True
|
|
134
|
-
if replace_routes is not None:
|
|
135
|
-
self.on_route_conflict = "preserve_agentos" if replace_routes else "preserve_base_app"
|
|
136
|
-
else:
|
|
137
|
-
self.on_route_conflict = on_route_conflict
|
|
138
187
|
else:
|
|
139
188
|
self.base_app = None
|
|
140
189
|
self._app_set = False
|
|
@@ -144,7 +193,7 @@ class AgentOS:
|
|
|
144
193
|
|
|
145
194
|
self.name = name
|
|
146
195
|
|
|
147
|
-
self.id = id
|
|
196
|
+
self.id = id
|
|
148
197
|
if not self.id:
|
|
149
198
|
self.id = generate_id(self.name) if self.name else str(uuid4())
|
|
150
199
|
|
|
@@ -152,58 +201,138 @@ class AgentOS:
|
|
|
152
201
|
self.description = description
|
|
153
202
|
|
|
154
203
|
self.telemetry = telemetry
|
|
204
|
+
self.tracing = tracing
|
|
205
|
+
self.tracing_db = tracing_db
|
|
155
206
|
|
|
156
|
-
self.enable_mcp_server =
|
|
207
|
+
self.enable_mcp_server = enable_mcp_server
|
|
157
208
|
self.lifespan = lifespan
|
|
158
209
|
|
|
210
|
+
# RBAC
|
|
211
|
+
self.authorization = authorization
|
|
212
|
+
self.authorization_config = authorization_config
|
|
213
|
+
|
|
214
|
+
# CORS configuration - merge user-provided origins with defaults from settings
|
|
215
|
+
self.cors_allowed_origins = resolve_origins(cors_allowed_origins, self.settings.cors_origin_list)
|
|
216
|
+
|
|
217
|
+
# If True, run agent/team hooks as FastAPI background tasks
|
|
218
|
+
self.run_hooks_in_background = run_hooks_in_background
|
|
219
|
+
|
|
159
220
|
# List of all MCP tools used inside the AgentOS
|
|
160
221
|
self.mcp_tools: List[Any] = []
|
|
161
222
|
self._mcp_app: Optional[Any] = None
|
|
162
223
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if agent.tools:
|
|
167
|
-
for tool in agent.tools:
|
|
168
|
-
# Checking if the tool is a MCPTools or MultiMCPTools instance
|
|
169
|
-
type_name = type(tool).__name__
|
|
170
|
-
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
171
|
-
if tool not in self.mcp_tools:
|
|
172
|
-
self.mcp_tools.append(tool)
|
|
224
|
+
self._initialize_agents()
|
|
225
|
+
self._initialize_teams()
|
|
226
|
+
self._initialize_workflows()
|
|
173
227
|
|
|
174
|
-
|
|
228
|
+
if self.tracing:
|
|
229
|
+
self._setup_tracing()
|
|
175
230
|
|
|
176
|
-
|
|
177
|
-
|
|
231
|
+
if self.telemetry:
|
|
232
|
+
from agno.api.os import OSLaunch, log_os_telemetry
|
|
178
233
|
|
|
179
|
-
|
|
180
|
-
for team in self.teams:
|
|
181
|
-
# Track all MCP tools recursively
|
|
182
|
-
collect_mcp_tools_from_team(team, self.mcp_tools)
|
|
234
|
+
log_os_telemetry(launch=OSLaunch(os_id=self.id, data=self._get_telemetry_data()))
|
|
183
235
|
|
|
184
|
-
|
|
236
|
+
def _add_agent_os_to_lifespan_function(self, lifespan):
|
|
237
|
+
"""
|
|
238
|
+
Inspect a lifespan function and wrap it to pass agent_os if it accepts it.
|
|
185
239
|
|
|
186
|
-
|
|
187
|
-
|
|
240
|
+
Returns:
|
|
241
|
+
A wrapped lifespan that passes agent_os if the lifespan function expects it.
|
|
242
|
+
"""
|
|
243
|
+
# Getting the actual function inside the lifespan
|
|
244
|
+
lifespan_function = lifespan
|
|
245
|
+
if hasattr(lifespan, "__wrapped__"):
|
|
246
|
+
lifespan_function = lifespan.__wrapped__
|
|
188
247
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
member.team_id = None
|
|
192
|
-
member.initialize_agent()
|
|
193
|
-
elif isinstance(member, Team):
|
|
194
|
-
member.initialize_team()
|
|
248
|
+
try:
|
|
249
|
+
from inspect import signature
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
200
|
-
if not workflow.id:
|
|
201
|
-
workflow.id = generate_id_from_name(workflow.name)
|
|
251
|
+
# Inspecting the lifespan function signature to find its parameters
|
|
252
|
+
sig = signature(lifespan_function)
|
|
253
|
+
params = list(sig.parameters.keys())
|
|
202
254
|
|
|
203
|
-
|
|
204
|
-
|
|
255
|
+
# If the lifespan function expects the 'agent_os' parameter, add it
|
|
256
|
+
if "agent_os" in params:
|
|
257
|
+
return partial(lifespan, agent_os=self)
|
|
258
|
+
else:
|
|
259
|
+
return lifespan
|
|
205
260
|
|
|
206
|
-
|
|
261
|
+
except (ValueError, TypeError):
|
|
262
|
+
return lifespan
|
|
263
|
+
|
|
264
|
+
def resync(self, app: FastAPI) -> None:
|
|
265
|
+
"""Resync the AgentOS to discover, initialize and configure: agents, teams, workflows, databases and knowledge bases."""
|
|
266
|
+
self._initialize_agents()
|
|
267
|
+
self._initialize_teams()
|
|
268
|
+
self._initialize_workflows()
|
|
269
|
+
self._auto_discover_databases()
|
|
270
|
+
self._auto_discover_knowledge_instances()
|
|
271
|
+
|
|
272
|
+
if self.enable_mcp_server:
|
|
273
|
+
from agno.os.mcp import get_mcp_server
|
|
274
|
+
|
|
275
|
+
self._mcp_app = get_mcp_server(self)
|
|
276
|
+
|
|
277
|
+
self._reprovision_routers(app=app)
|
|
278
|
+
|
|
279
|
+
def _reprovision_routers(self, app: FastAPI) -> None:
|
|
280
|
+
"""Re-provision all routes for the AgentOS."""
|
|
281
|
+
updated_routers = [
|
|
282
|
+
get_session_router(dbs=self.dbs),
|
|
283
|
+
get_metrics_router(dbs=self.dbs),
|
|
284
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
285
|
+
get_traces_router(dbs=self.dbs),
|
|
286
|
+
get_memory_router(dbs=self.dbs),
|
|
287
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# Clear all previously existing routes
|
|
291
|
+
app.router.routes = [
|
|
292
|
+
route
|
|
293
|
+
for route in app.router.routes
|
|
294
|
+
if hasattr(route, "path")
|
|
295
|
+
and route.path in ["/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"]
|
|
296
|
+
or route.path.startswith("/mcp") # type: ignore
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
# Add the built-in routes
|
|
300
|
+
self._add_built_in_routes(app=app)
|
|
301
|
+
|
|
302
|
+
# Add the updated routes
|
|
303
|
+
for router in updated_routers:
|
|
304
|
+
self._add_router(app, router)
|
|
305
|
+
|
|
306
|
+
# Mount MCP if needed
|
|
307
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
308
|
+
app.mount("/", self._mcp_app)
|
|
309
|
+
|
|
310
|
+
def _add_built_in_routes(self, app: FastAPI) -> None:
|
|
311
|
+
"""Add all AgentOSbuilt-in routes to the given app."""
|
|
312
|
+
# Add the home router if MCP server is not enabled
|
|
313
|
+
if not self.enable_mcp_server:
|
|
314
|
+
self._add_router(app, get_home_router(self))
|
|
315
|
+
|
|
316
|
+
self._add_router(app, get_health_router(health_endpoint="/health"))
|
|
317
|
+
self._add_router(app, get_base_router(self, settings=self.settings))
|
|
318
|
+
self._add_router(app, get_agent_router(self, settings=self.settings))
|
|
319
|
+
self._add_router(app, get_team_router(self, settings=self.settings))
|
|
320
|
+
self._add_router(app, get_workflow_router(self, settings=self.settings))
|
|
321
|
+
self._add_router(app, get_websocket_router(self, settings=self.settings))
|
|
322
|
+
|
|
323
|
+
# Add A2A interface if relevant
|
|
324
|
+
has_a2a_interface = False
|
|
325
|
+
for interface in self.interfaces:
|
|
326
|
+
if not has_a2a_interface and interface.__class__.__name__ == "A2A":
|
|
327
|
+
has_a2a_interface = True
|
|
328
|
+
interface_router = interface.get_router()
|
|
329
|
+
self._add_router(app, interface_router)
|
|
330
|
+
if self.a2a_interface and not has_a2a_interface:
|
|
331
|
+
from agno.os.interfaces.a2a import A2A
|
|
332
|
+
|
|
333
|
+
a2a_interface = A2A(agents=self.agents, teams=self.teams, workflows=self.workflows)
|
|
334
|
+
self.interfaces.append(a2a_interface)
|
|
335
|
+
self._add_router(app, a2a_interface.get_router())
|
|
207
336
|
|
|
208
337
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
209
338
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
@@ -220,7 +349,7 @@ class AgentOS:
|
|
|
220
349
|
async with mcp_tools_lifespan(app): # type: ignore
|
|
221
350
|
yield
|
|
222
351
|
|
|
223
|
-
app_lifespan = combined_lifespan
|
|
352
|
+
app_lifespan = combined_lifespan
|
|
224
353
|
else:
|
|
225
354
|
app_lifespan = mcp_tools_lifespan
|
|
226
355
|
|
|
@@ -234,53 +363,175 @@ class AgentOS:
|
|
|
234
363
|
lifespan=app_lifespan,
|
|
235
364
|
)
|
|
236
365
|
|
|
366
|
+
def _initialize_agents(self) -> None:
|
|
367
|
+
"""Initialize and configure all agents for AgentOS usage."""
|
|
368
|
+
if not self.agents:
|
|
369
|
+
return
|
|
370
|
+
for agent in self.agents:
|
|
371
|
+
# Track all MCP tools to later handle their connection
|
|
372
|
+
if agent.tools:
|
|
373
|
+
for tool in agent.tools:
|
|
374
|
+
# Checking if the tool is an instance of MCPTools, MultiMCPTools, or a subclass of those
|
|
375
|
+
if hasattr(type(tool), "__mro__"):
|
|
376
|
+
mro_names = {cls.__name__ for cls in type(tool).__mro__}
|
|
377
|
+
if mro_names & {"MCPTools", "MultiMCPTools"}:
|
|
378
|
+
if tool not in self.mcp_tools:
|
|
379
|
+
self.mcp_tools.append(tool)
|
|
380
|
+
|
|
381
|
+
agent.initialize_agent()
|
|
382
|
+
|
|
383
|
+
# Required for the built-in routes to work
|
|
384
|
+
agent.store_events = True
|
|
385
|
+
|
|
386
|
+
# Propagate run_hooks_in_background setting from AgentOS to agents
|
|
387
|
+
agent._run_hooks_in_background = self.run_hooks_in_background
|
|
388
|
+
|
|
389
|
+
def _initialize_teams(self) -> None:
|
|
390
|
+
"""Initialize and configure all teams for AgentOS usage."""
|
|
391
|
+
if not self.teams:
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
for team in self.teams:
|
|
395
|
+
# Track all MCP tools recursively
|
|
396
|
+
collect_mcp_tools_from_team(team, self.mcp_tools)
|
|
397
|
+
|
|
398
|
+
team.initialize_team()
|
|
399
|
+
|
|
400
|
+
for member in team.members:
|
|
401
|
+
if isinstance(member, Agent):
|
|
402
|
+
member.team_id = None
|
|
403
|
+
member.initialize_agent()
|
|
404
|
+
elif isinstance(member, Team):
|
|
405
|
+
member.initialize_team()
|
|
406
|
+
|
|
407
|
+
# Required for the built-in routes to work
|
|
408
|
+
team.store_events = True
|
|
409
|
+
|
|
410
|
+
# Propagate run_hooks_in_background setting to team and all nested members
|
|
411
|
+
team.propagate_run_hooks_in_background(self.run_hooks_in_background)
|
|
412
|
+
|
|
413
|
+
def _initialize_workflows(self) -> None:
|
|
414
|
+
"""Initialize and configure all workflows for AgentOS usage."""
|
|
415
|
+
if not self.workflows:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
if self.workflows:
|
|
419
|
+
for workflow in self.workflows:
|
|
420
|
+
# Track MCP tools recursively in workflow members
|
|
421
|
+
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
422
|
+
|
|
423
|
+
if not workflow.id:
|
|
424
|
+
workflow.id = generate_id_from_name(workflow.name)
|
|
425
|
+
|
|
426
|
+
# Required for the built-in routes to work
|
|
427
|
+
workflow.store_events = True
|
|
428
|
+
|
|
429
|
+
# Propagate run_hooks_in_background setting to workflow and all its step agents/teams
|
|
430
|
+
workflow.propagate_run_hooks_in_background(self.run_hooks_in_background)
|
|
431
|
+
|
|
432
|
+
def _setup_tracing(self) -> None:
|
|
433
|
+
"""Set up OpenTelemetry tracing for this AgentOS.
|
|
434
|
+
|
|
435
|
+
Uses tracing_db if provided, otherwise falls back to the first available
|
|
436
|
+
database from agents/teams/workflows.
|
|
437
|
+
"""
|
|
438
|
+
# Use tracing_db if explicitly provided
|
|
439
|
+
if self.tracing_db is not None:
|
|
440
|
+
setup_tracing_for_os(db=self.tracing_db)
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
# Fall back to finding the first available database
|
|
444
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None
|
|
445
|
+
|
|
446
|
+
for agent in self.agents or []:
|
|
447
|
+
if agent.db:
|
|
448
|
+
db = agent.db
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
if db is None:
|
|
452
|
+
for team in self.teams or []:
|
|
453
|
+
if team.db:
|
|
454
|
+
db = team.db
|
|
455
|
+
break
|
|
456
|
+
|
|
457
|
+
if db is None:
|
|
458
|
+
for workflow in self.workflows or []:
|
|
459
|
+
if workflow.db:
|
|
460
|
+
db = workflow.db
|
|
461
|
+
break
|
|
462
|
+
|
|
463
|
+
if db is None:
|
|
464
|
+
log_warning(
|
|
465
|
+
"tracing=True but no database found. "
|
|
466
|
+
"Provide 'tracing_db' parameter or 'db' parameter to at least one agent/team/workflow."
|
|
467
|
+
)
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
setup_tracing_for_os(db=db)
|
|
471
|
+
|
|
237
472
|
def get_app(self) -> FastAPI:
|
|
238
473
|
if self.base_app:
|
|
239
474
|
fastapi_app = self.base_app
|
|
240
|
-
else:
|
|
241
|
-
if self.enable_mcp_server:
|
|
242
|
-
from contextlib import asynccontextmanager
|
|
243
475
|
|
|
476
|
+
# Initialize MCP server if enabled
|
|
477
|
+
if self.enable_mcp_server:
|
|
244
478
|
from agno.os.mcp import get_mcp_server
|
|
245
479
|
|
|
246
480
|
self._mcp_app = get_mcp_server(self)
|
|
247
481
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
# Combine both lifespans
|
|
251
|
-
@asynccontextmanager
|
|
252
|
-
async def combined_lifespan(app: FastAPI):
|
|
253
|
-
# Run both lifespans
|
|
254
|
-
async with self.lifespan(app): # type: ignore
|
|
255
|
-
async with self._mcp_app.lifespan(app): # type: ignore
|
|
256
|
-
yield
|
|
482
|
+
# Collect all lifespans that need to be combined
|
|
483
|
+
lifespans = []
|
|
257
484
|
|
|
258
|
-
|
|
485
|
+
# The user provided lifespan
|
|
486
|
+
if self.lifespan:
|
|
487
|
+
# Wrap the user lifespan with agent_os parameter
|
|
488
|
+
wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
489
|
+
lifespans.append(wrapped_lifespan)
|
|
259
490
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
491
|
+
# The provided app's existing lifespan
|
|
492
|
+
if fastapi_app.router.lifespan_context:
|
|
493
|
+
lifespans.append(fastapi_app.router.lifespan_context)
|
|
263
494
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self._add_router(fastapi_app, get_health_router())
|
|
268
|
-
self._add_router(fastapi_app, get_home_router(self))
|
|
495
|
+
# The MCP tools lifespan
|
|
496
|
+
if self.mcp_tools:
|
|
497
|
+
lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
|
|
269
498
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
has_a2a_interface = True
|
|
274
|
-
interface_router = interface.get_router()
|
|
275
|
-
self._add_router(fastapi_app, interface_router)
|
|
499
|
+
# The /mcp server lifespan
|
|
500
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
501
|
+
lifespans.append(self._mcp_app.lifespan)
|
|
276
502
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
from agno.os.interfaces.a2a import A2A
|
|
503
|
+
# The async database lifespan
|
|
504
|
+
lifespans.append(partial(db_lifespan, agent_os=self))
|
|
280
505
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
506
|
+
# Combine lifespans and set them in the app
|
|
507
|
+
if lifespans:
|
|
508
|
+
fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
|
|
509
|
+
|
|
510
|
+
else:
|
|
511
|
+
lifespans = []
|
|
512
|
+
|
|
513
|
+
# User provided lifespan
|
|
514
|
+
if self.lifespan:
|
|
515
|
+
lifespans.append(self._add_agent_os_to_lifespan_function(self.lifespan))
|
|
516
|
+
|
|
517
|
+
# MCP tools lifespan
|
|
518
|
+
if self.mcp_tools:
|
|
519
|
+
lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
|
|
520
|
+
|
|
521
|
+
# MCP server lifespan
|
|
522
|
+
if self.enable_mcp_server:
|
|
523
|
+
from agno.os.mcp import get_mcp_server
|
|
524
|
+
|
|
525
|
+
self._mcp_app = get_mcp_server(self)
|
|
526
|
+
lifespans.append(self._mcp_app.lifespan)
|
|
527
|
+
|
|
528
|
+
# Async database initialization lifespan
|
|
529
|
+
lifespans.append(partial(db_lifespan, agent_os=self)) # type: ignore
|
|
530
|
+
|
|
531
|
+
final_lifespan = _combine_app_lifespans(lifespans) if lifespans else None
|
|
532
|
+
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
533
|
+
|
|
534
|
+
self._add_built_in_routes(app=fastapi_app)
|
|
284
535
|
|
|
285
536
|
self._auto_discover_databases()
|
|
286
537
|
self._auto_discover_knowledge_instances()
|
|
@@ -291,6 +542,7 @@ class AgentOS:
|
|
|
291
542
|
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
292
543
|
get_metrics_router(dbs=self.dbs),
|
|
293
544
|
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
545
|
+
get_traces_router(dbs=self.dbs),
|
|
294
546
|
]
|
|
295
547
|
|
|
296
548
|
for router in routers:
|
|
@@ -299,35 +551,76 @@ class AgentOS:
|
|
|
299
551
|
# Mount MCP if needed
|
|
300
552
|
if self.enable_mcp_server and self._mcp_app:
|
|
301
553
|
fastapi_app.mount("/", self._mcp_app)
|
|
302
|
-
else:
|
|
303
|
-
# Add the home router
|
|
304
|
-
self._add_router(fastapi_app, get_home_router(self))
|
|
305
554
|
|
|
306
555
|
if not self._app_set:
|
|
307
556
|
|
|
308
557
|
@fastapi_app.exception_handler(HTTPException)
|
|
309
558
|
async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
|
|
559
|
+
log_error(f"HTTP exception: {exc.status_code} {exc.detail}")
|
|
310
560
|
return JSONResponse(
|
|
311
561
|
status_code=exc.status_code,
|
|
312
562
|
content={"detail": str(exc.detail)},
|
|
313
563
|
)
|
|
314
564
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
except Exception as e:
|
|
319
|
-
return JSONResponse(
|
|
320
|
-
status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
|
|
321
|
-
content={"detail": str(e)},
|
|
322
|
-
)
|
|
565
|
+
@fastapi_app.exception_handler(Exception)
|
|
566
|
+
async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
567
|
+
import traceback
|
|
323
568
|
|
|
324
|
-
|
|
569
|
+
log_error(f"Unhandled exception:\n{traceback.format_exc(limit=5)}")
|
|
570
|
+
|
|
571
|
+
return JSONResponse(
|
|
572
|
+
status_code=getattr(exc, "status_code", 500),
|
|
573
|
+
content={"detail": str(exc)},
|
|
574
|
+
)
|
|
325
575
|
|
|
326
576
|
# Update CORS middleware
|
|
327
|
-
update_cors_middleware(fastapi_app, self.
|
|
577
|
+
update_cors_middleware(fastapi_app, self.cors_allowed_origins) # type: ignore
|
|
578
|
+
|
|
579
|
+
# Set agent_os_id and cors_allowed_origins on app state
|
|
580
|
+
# This allows middleware (like JWT) to access these values
|
|
581
|
+
fastapi_app.state.agent_os_id = self.id
|
|
582
|
+
fastapi_app.state.cors_allowed_origins = self.cors_allowed_origins
|
|
583
|
+
|
|
584
|
+
# Add JWT middleware if authorization is enabled
|
|
585
|
+
if self.authorization:
|
|
586
|
+
self._add_jwt_middleware(fastapi_app)
|
|
328
587
|
|
|
329
588
|
return fastapi_app
|
|
330
589
|
|
|
590
|
+
def _add_jwt_middleware(self, fastapi_app: FastAPI) -> None:
|
|
591
|
+
from agno.os.middleware.jwt import JWTMiddleware, JWTValidator
|
|
592
|
+
|
|
593
|
+
verify_audience = False
|
|
594
|
+
jwks_file = None
|
|
595
|
+
verification_keys = None
|
|
596
|
+
algorithm = "RS256"
|
|
597
|
+
|
|
598
|
+
if self.authorization_config:
|
|
599
|
+
algorithm = self.authorization_config.algorithm or "RS256"
|
|
600
|
+
verification_keys = self.authorization_config.verification_keys
|
|
601
|
+
jwks_file = self.authorization_config.jwks_file
|
|
602
|
+
verify_audience = self.authorization_config.verify_audience or False
|
|
603
|
+
|
|
604
|
+
log_info(f"Adding JWT middleware for authorization (algorithm: {algorithm})")
|
|
605
|
+
|
|
606
|
+
# Create validator and store on app.state for WebSocket access
|
|
607
|
+
jwt_validator = JWTValidator(
|
|
608
|
+
verification_keys=verification_keys,
|
|
609
|
+
jwks_file=jwks_file,
|
|
610
|
+
algorithm=algorithm,
|
|
611
|
+
)
|
|
612
|
+
fastapi_app.state.jwt_validator = jwt_validator
|
|
613
|
+
|
|
614
|
+
# Add middleware to stack
|
|
615
|
+
fastapi_app.add_middleware(
|
|
616
|
+
JWTMiddleware,
|
|
617
|
+
verification_keys=verification_keys,
|
|
618
|
+
jwks_file=jwks_file,
|
|
619
|
+
algorithm=algorithm,
|
|
620
|
+
authorization=self.authorization,
|
|
621
|
+
verify_audience=verify_audience,
|
|
622
|
+
)
|
|
623
|
+
|
|
331
624
|
def get_routes(self) -> List[Any]:
|
|
332
625
|
"""Retrieve all routes from the FastAPI app.
|
|
333
626
|
|
|
@@ -353,7 +646,7 @@ class AgentOS:
|
|
|
353
646
|
# Skip conflicting AgentOS routes, prefer user's existing routes
|
|
354
647
|
for conflict in conflicts:
|
|
355
648
|
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
356
|
-
|
|
649
|
+
log_debug(
|
|
357
650
|
f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
|
|
358
651
|
f"Using existing custom route instead"
|
|
359
652
|
)
|
|
@@ -372,7 +665,7 @@ class AgentOS:
|
|
|
372
665
|
# Log warnings but still add all routes (AgentOS routes will override)
|
|
373
666
|
for conflict in conflicts:
|
|
374
667
|
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
375
|
-
|
|
668
|
+
log_warning(
|
|
376
669
|
f"Route conflict detected: {methods_str} {conflict['path']} - "
|
|
377
670
|
f"AgentOS route will override existing custom route"
|
|
378
671
|
)
|
|
@@ -404,11 +697,12 @@ class AgentOS:
|
|
|
404
697
|
}
|
|
405
698
|
|
|
406
699
|
def _auto_discover_databases(self) -> None:
|
|
407
|
-
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
408
|
-
from agno.db.base import BaseDb
|
|
700
|
+
"""Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
|
|
409
701
|
|
|
410
|
-
dbs: Dict[str, BaseDb] = {}
|
|
411
|
-
knowledge_dbs: Dict[
|
|
702
|
+
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
|
|
703
|
+
knowledge_dbs: Dict[
|
|
704
|
+
str, List[Union[BaseDb, AsyncBaseDb]]
|
|
705
|
+
] = {} # Track databases specifically used for knowledge
|
|
412
706
|
|
|
413
707
|
for agent in self.agents or []:
|
|
414
708
|
if agent.db:
|
|
@@ -426,68 +720,120 @@ class AgentOS:
|
|
|
426
720
|
if workflow.db:
|
|
427
721
|
self._register_db_with_validation(dbs, workflow.db)
|
|
428
722
|
|
|
723
|
+
for knowledge_base in self.knowledge or []:
|
|
724
|
+
if knowledge_base.contents_db:
|
|
725
|
+
self._register_db_with_validation(knowledge_dbs, knowledge_base.contents_db)
|
|
726
|
+
|
|
429
727
|
for interface in self.interfaces or []:
|
|
430
728
|
if interface.agent and interface.agent.db:
|
|
431
729
|
self._register_db_with_validation(dbs, interface.agent.db)
|
|
432
730
|
elif interface.team and interface.team.db:
|
|
433
731
|
self._register_db_with_validation(dbs, interface.team.db)
|
|
434
732
|
|
|
733
|
+
# Register tracing_db if provided (for traces reading)
|
|
734
|
+
if self.tracing_db is not None:
|
|
735
|
+
self._register_db_with_validation(dbs, self.tracing_db)
|
|
736
|
+
|
|
435
737
|
self.dbs = dbs
|
|
436
738
|
self.knowledge_dbs = knowledge_dbs
|
|
437
739
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
740
|
+
# Initialize all discovered databases
|
|
741
|
+
if self.auto_provision_dbs:
|
|
742
|
+
self._pending_async_db_init = True
|
|
743
|
+
|
|
744
|
+
def _initialize_sync_databases(self) -> None:
|
|
745
|
+
"""Initialize sync databases."""
|
|
746
|
+
from itertools import chain
|
|
747
|
+
|
|
748
|
+
unique_dbs = list(
|
|
749
|
+
{
|
|
750
|
+
id(db): db
|
|
751
|
+
for db in chain(
|
|
752
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
446
753
|
)
|
|
447
|
-
|
|
754
|
+
}.values()
|
|
755
|
+
)
|
|
448
756
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Two database objects are compatible if they point to the same database with identical configuration.
|
|
453
|
-
"""
|
|
454
|
-
# If they're the same object reference, they're compatible
|
|
455
|
-
if db1 is db2:
|
|
456
|
-
return True
|
|
757
|
+
for db in unique_dbs:
|
|
758
|
+
if isinstance(db, AsyncBaseDb):
|
|
759
|
+
continue # Skip async dbs
|
|
457
760
|
|
|
458
|
-
|
|
459
|
-
|
|
761
|
+
try:
|
|
762
|
+
if hasattr(db, "_create_all_tables") and callable(db._create_all_tables):
|
|
763
|
+
db._create_all_tables()
|
|
764
|
+
except Exception as e:
|
|
765
|
+
log_warning(f"Failed to initialize {db.__class__.__name__} (id: {db.id}): {e}")
|
|
460
766
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return False
|
|
767
|
+
async def _initialize_async_databases(self) -> None:
|
|
768
|
+
"""Initialize async databases."""
|
|
464
769
|
|
|
465
|
-
|
|
466
|
-
if db1.db_file != db2.db_file: # type: ignore
|
|
467
|
-
return False
|
|
770
|
+
from itertools import chain
|
|
468
771
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
return False
|
|
772
|
+
unique_dbs = list(
|
|
773
|
+
{
|
|
774
|
+
id(db): db
|
|
775
|
+
for db in chain(
|
|
776
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
777
|
+
)
|
|
778
|
+
}.values()
|
|
779
|
+
)
|
|
478
780
|
|
|
479
|
-
|
|
781
|
+
for db in unique_dbs:
|
|
782
|
+
if not isinstance(db, AsyncBaseDb):
|
|
783
|
+
continue # Skip sync dbs
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
if hasattr(db, "_create_all_tables") and callable(db._create_all_tables):
|
|
787
|
+
await db._create_all_tables()
|
|
788
|
+
except Exception as e:
|
|
789
|
+
log_warning(f"Failed to initialize async {db.__class__.__name__} (id: {db.id}): {e}")
|
|
790
|
+
|
|
791
|
+
def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
|
|
792
|
+
"""Get the table names for a database"""
|
|
793
|
+
table_names = {
|
|
794
|
+
"session_table_name": db.session_table_name,
|
|
795
|
+
"culture_table_name": db.culture_table_name,
|
|
796
|
+
"memory_table_name": db.memory_table_name,
|
|
797
|
+
"metrics_table_name": db.metrics_table_name,
|
|
798
|
+
"evals_table_name": db.eval_table_name,
|
|
799
|
+
"knowledge_table_name": db.knowledge_table_name,
|
|
800
|
+
}
|
|
801
|
+
return {k: v for k, v in table_names.items() if v is not None}
|
|
802
|
+
|
|
803
|
+
def _register_db_with_validation(
|
|
804
|
+
self, registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]], db: Union[BaseDb, AsyncBaseDb]
|
|
805
|
+
) -> None:
|
|
806
|
+
"""Register a database in the contextual OS after validating it is not conflicting with registered databases"""
|
|
807
|
+
if db.id in registered_dbs:
|
|
808
|
+
registered_dbs[db.id].append(db)
|
|
809
|
+
else:
|
|
810
|
+
registered_dbs[db.id] = [db]
|
|
480
811
|
|
|
481
812
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
482
813
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
483
|
-
|
|
814
|
+
seen_ids = set()
|
|
815
|
+
knowledge_instances: List[Knowledge] = []
|
|
816
|
+
|
|
817
|
+
def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
|
|
818
|
+
"""Add knowledge instance if it's not already in the list (by object identity or db_id)."""
|
|
819
|
+
# Use database ID if available, otherwise use object ID as fallback
|
|
820
|
+
if not knowledge.contents_db:
|
|
821
|
+
return
|
|
822
|
+
if knowledge.contents_db.id in seen_ids:
|
|
823
|
+
return
|
|
824
|
+
seen_ids.add(knowledge.contents_db.id)
|
|
825
|
+
knowledge_instances.append(knowledge)
|
|
826
|
+
|
|
484
827
|
for agent in self.agents or []:
|
|
485
828
|
if agent.knowledge:
|
|
486
|
-
|
|
829
|
+
_add_knowledge_if_not_duplicate(agent.knowledge)
|
|
487
830
|
|
|
488
831
|
for team in self.teams or []:
|
|
489
832
|
if team.knowledge:
|
|
490
|
-
|
|
833
|
+
_add_knowledge_if_not_duplicate(team.knowledge)
|
|
834
|
+
|
|
835
|
+
for knowledge_base in self.knowledge or []:
|
|
836
|
+
_add_knowledge_if_not_duplicate(knowledge_base)
|
|
491
837
|
|
|
492
838
|
self.knowledge_instances = knowledge_instances
|
|
493
839
|
|
|
@@ -498,13 +844,15 @@ class AgentOS:
|
|
|
498
844
|
session_config.dbs = []
|
|
499
845
|
|
|
500
846
|
dbs_with_specific_config = [db.db_id for db in session_config.dbs]
|
|
501
|
-
|
|
502
|
-
for db_id in self.dbs.keys():
|
|
847
|
+
for db_id, dbs in self.dbs.items():
|
|
503
848
|
if db_id not in dbs_with_specific_config:
|
|
849
|
+
# Collect unique table names from all databases with the same id
|
|
850
|
+
unique_tables = list(set(db.session_table_name for db in dbs))
|
|
504
851
|
session_config.dbs.append(
|
|
505
852
|
DatabaseConfig(
|
|
506
853
|
db_id=db_id,
|
|
507
854
|
domain_config=SessionDomainConfig(display_name=db_id),
|
|
855
|
+
tables=unique_tables,
|
|
508
856
|
)
|
|
509
857
|
)
|
|
510
858
|
|
|
@@ -518,12 +866,15 @@ class AgentOS:
|
|
|
518
866
|
|
|
519
867
|
dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
|
|
520
868
|
|
|
521
|
-
for db_id in self.dbs.
|
|
869
|
+
for db_id, dbs in self.dbs.items():
|
|
522
870
|
if db_id not in dbs_with_specific_config:
|
|
871
|
+
# Collect unique table names from all databases with the same id
|
|
872
|
+
unique_tables = list(set(db.memory_table_name for db in dbs))
|
|
523
873
|
memory_config.dbs.append(
|
|
524
874
|
DatabaseConfig(
|
|
525
875
|
db_id=db_id,
|
|
526
876
|
domain_config=MemoryDomainConfig(display_name=db_id),
|
|
877
|
+
tables=unique_tables,
|
|
527
878
|
)
|
|
528
879
|
)
|
|
529
880
|
|
|
@@ -557,12 +908,15 @@ class AgentOS:
|
|
|
557
908
|
|
|
558
909
|
dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
|
|
559
910
|
|
|
560
|
-
for db_id in self.dbs.
|
|
911
|
+
for db_id, dbs in self.dbs.items():
|
|
561
912
|
if db_id not in dbs_with_specific_config:
|
|
913
|
+
# Collect unique table names from all databases with the same id
|
|
914
|
+
unique_tables = list(set(db.metrics_table_name for db in dbs))
|
|
562
915
|
metrics_config.dbs.append(
|
|
563
916
|
DatabaseConfig(
|
|
564
917
|
db_id=db_id,
|
|
565
918
|
domain_config=MetricsDomainConfig(display_name=db_id),
|
|
919
|
+
tables=unique_tables,
|
|
566
920
|
)
|
|
567
921
|
)
|
|
568
922
|
|
|
@@ -576,17 +930,50 @@ class AgentOS:
|
|
|
576
930
|
|
|
577
931
|
dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
|
|
578
932
|
|
|
579
|
-
for db_id in self.dbs.
|
|
933
|
+
for db_id, dbs in self.dbs.items():
|
|
580
934
|
if db_id not in dbs_with_specific_config:
|
|
935
|
+
# Collect unique table names from all databases with the same id
|
|
936
|
+
unique_tables = list(set(db.eval_table_name for db in dbs))
|
|
581
937
|
evals_config.dbs.append(
|
|
582
938
|
DatabaseConfig(
|
|
583
939
|
db_id=db_id,
|
|
584
940
|
domain_config=EvalsDomainConfig(display_name=db_id),
|
|
941
|
+
tables=unique_tables,
|
|
585
942
|
)
|
|
586
943
|
)
|
|
587
944
|
|
|
588
945
|
return evals_config
|
|
589
946
|
|
|
947
|
+
def _get_traces_config(self) -> TracesConfig:
|
|
948
|
+
traces_config = self.config.traces if self.config and self.config.traces else TracesConfig()
|
|
949
|
+
|
|
950
|
+
if traces_config.dbs is None:
|
|
951
|
+
traces_config.dbs = []
|
|
952
|
+
|
|
953
|
+
dbs_with_specific_config = [db.db_id for db in traces_config.dbs]
|
|
954
|
+
|
|
955
|
+
# If tracing_db is explicitly set, only use that database for traces
|
|
956
|
+
if self.tracing_db is not None:
|
|
957
|
+
if self.tracing_db.id not in dbs_with_specific_config:
|
|
958
|
+
traces_config.dbs.append(
|
|
959
|
+
DatabaseConfig(
|
|
960
|
+
db_id=self.tracing_db.id,
|
|
961
|
+
domain_config=TracesDomainConfig(display_name=self.tracing_db.id),
|
|
962
|
+
)
|
|
963
|
+
)
|
|
964
|
+
else:
|
|
965
|
+
# Fall back to all discovered databases
|
|
966
|
+
for db_id in self.dbs.keys():
|
|
967
|
+
if db_id not in dbs_with_specific_config:
|
|
968
|
+
traces_config.dbs.append(
|
|
969
|
+
DatabaseConfig(
|
|
970
|
+
db_id=db_id,
|
|
971
|
+
domain_config=TracesDomainConfig(display_name=db_id),
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
return traces_config
|
|
976
|
+
|
|
590
977
|
def serve(
|
|
591
978
|
self,
|
|
592
979
|
app: Union[str, FastAPI],
|
|
@@ -595,6 +982,7 @@ class AgentOS:
|
|
|
595
982
|
port: int = 7777,
|
|
596
983
|
reload: bool = False,
|
|
597
984
|
workers: Optional[int] = None,
|
|
985
|
+
access_log: bool = False,
|
|
598
986
|
**kwargs,
|
|
599
987
|
):
|
|
600
988
|
import uvicorn
|
|
@@ -627,4 +1015,13 @@ class AgentOS:
|
|
|
627
1015
|
)
|
|
628
1016
|
)
|
|
629
1017
|
|
|
630
|
-
uvicorn.run(
|
|
1018
|
+
uvicorn.run(
|
|
1019
|
+
app=app,
|
|
1020
|
+
host=host,
|
|
1021
|
+
port=port,
|
|
1022
|
+
reload=reload,
|
|
1023
|
+
workers=workers,
|
|
1024
|
+
access_log=access_log,
|
|
1025
|
+
lifespan="on",
|
|
1026
|
+
**kwargs,
|
|
1027
|
+
)
|