agno 2.0.1__py3-none-any.whl → 2.3.0__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.
Files changed (314) hide show
  1. agno/agent/agent.py +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/os/app.py CHANGED
@@ -1,17 +1,19 @@
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, Optional, Union
4
+ from typing import Any, Dict, List, Literal, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
- from fastapi import FastAPI, HTTPException
7
+ from fastapi import APIRouter, FastAPI, HTTPException
8
8
  from fastapi.responses import JSONResponse
9
+ from fastapi.routing import APIRoute
9
10
  from rich import box
10
11
  from rich.panel import Panel
11
- from starlette.middleware.cors import CORSMiddleware
12
12
  from starlette.requests import Request
13
13
 
14
14
  from agno.agent.agent import Agent
15
+ from agno.db.base import AsyncBaseDb, BaseDb
16
+ from agno.knowledge.knowledge import Knowledge
15
17
  from agno.os.config import (
16
18
  AgentOSConfig,
17
19
  DatabaseConfig,
@@ -27,20 +29,30 @@ from agno.os.config import (
27
29
  SessionDomainConfig,
28
30
  )
29
31
  from agno.os.interfaces.base import BaseInterface
30
- from agno.os.router import get_base_router
32
+ from agno.os.router import get_base_router, get_websocket_router
31
33
  from agno.os.routers.evals import get_eval_router
34
+ from agno.os.routers.health import get_health_router
35
+ from agno.os.routers.home import get_home_router
32
36
  from agno.os.routers.knowledge import get_knowledge_router
33
37
  from agno.os.routers.memory import get_memory_router
34
38
  from agno.os.routers.metrics import get_metrics_router
35
39
  from agno.os.routers.session import get_session_router
36
40
  from agno.os.settings import AgnoAPISettings
37
- from agno.os.utils import generate_id
41
+ from agno.os.utils import (
42
+ collect_mcp_tools_from_team,
43
+ collect_mcp_tools_from_workflow,
44
+ find_conflicting_routes,
45
+ load_yaml_config,
46
+ update_cors_middleware,
47
+ )
38
48
  from agno.team.team import Team
49
+ from agno.utils.log import log_debug, log_error, log_warning
50
+ from agno.utils.string import generate_id, generate_id_from_name
39
51
  from agno.workflow.workflow import Workflow
40
52
 
41
53
 
42
54
  @asynccontextmanager
43
- async def mcp_lifespan(app, mcp_tools):
55
+ async def mcp_lifespan(_, mcp_tools):
44
56
  """Manage MCP connection lifecycle inside a FastAPI app"""
45
57
  # Startup logic: connect to all contextual MCP servers
46
58
  for tool in mcp_tools:
@@ -53,104 +65,223 @@ async def mcp_lifespan(app, mcp_tools):
53
65
  await tool.close()
54
66
 
55
67
 
68
+ def _combine_app_lifespans(lifespans: list) -> Any:
69
+ """Combine multiple FastAPI app lifespan context managers into one."""
70
+ if len(lifespans) == 1:
71
+ return lifespans[0]
72
+
73
+ from contextlib import asynccontextmanager
74
+
75
+ @asynccontextmanager
76
+ async def combined_lifespan(app):
77
+ async def _run_nested(index: int):
78
+ if index >= len(lifespans):
79
+ yield
80
+ return
81
+
82
+ async with lifespans[index](app):
83
+ async for _ in _run_nested(index + 1):
84
+ yield
85
+
86
+ async for _ in _run_nested(0):
87
+ yield
88
+
89
+ return combined_lifespan
90
+
91
+
56
92
  class AgentOS:
57
93
  def __init__(
58
94
  self,
59
- os_id: Optional[str] = None,
95
+ id: Optional[str] = None,
60
96
  name: Optional[str] = None,
61
97
  description: Optional[str] = None,
62
98
  version: Optional[str] = None,
63
99
  agents: Optional[List[Agent]] = None,
64
100
  teams: Optional[List[Team]] = None,
65
101
  workflows: Optional[List[Workflow]] = None,
102
+ knowledge: Optional[List[Knowledge]] = None,
66
103
  interfaces: Optional[List[BaseInterface]] = None,
104
+ a2a_interface: bool = False,
67
105
  config: Optional[Union[str, AgentOSConfig]] = None,
68
106
  settings: Optional[AgnoAPISettings] = None,
69
- fastapi_app: Optional[FastAPI] = None,
70
107
  lifespan: Optional[Any] = None,
71
- enable_mcp: bool = False,
108
+ enable_mcp_server: bool = False,
109
+ base_app: Optional[FastAPI] = None,
110
+ on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
72
111
  telemetry: bool = True,
112
+ auto_provision_dbs: bool = True,
73
113
  ):
74
- if not agents and not workflows and not teams:
75
- raise ValueError("Either agents, teams or workflows must be provided.")
114
+ """Initialize AgentOS.
115
+
116
+ Args:
117
+ id: Unique identifier for this AgentOS instance
118
+ name: Name of the AgentOS instance
119
+ description: Description of the AgentOS instance
120
+ version: Version of the AgentOS instance
121
+ agents: List of agents to include in the OS
122
+ teams: List of teams to include in the OS
123
+ workflows: List of workflows to include in the OS
124
+ knowledge: List of knowledge bases to include in the OS
125
+ interfaces: List of interfaces to include in the OS
126
+ a2a_interface: Whether to expose the OS agents and teams in an A2A server
127
+ config: Configuration file path or AgentOSConfig instance
128
+ settings: API settings for the OS
129
+ lifespan: Optional lifespan context manager for the FastAPI app
130
+ enable_mcp_server: Whether to enable MCP (Model Context Protocol)
131
+ base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
132
+ on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
133
+ telemetry: Whether to enable telemetry
134
+
135
+ """
136
+ if not agents and not workflows and not teams and not knowledge:
137
+ raise ValueError("Either agents, teams, workflows or knowledge bases must be provided.")
76
138
 
77
- self.config = self._load_yaml_config(config) if isinstance(config, str) else config
139
+ self.config = load_yaml_config(config) if isinstance(config, str) else config
78
140
 
79
141
  self.agents: Optional[List[Agent]] = agents
80
142
  self.workflows: Optional[List[Workflow]] = workflows
81
143
  self.teams: Optional[List[Team]] = teams
82
144
  self.interfaces = interfaces or []
83
-
145
+ self.a2a_interface = a2a_interface
146
+ self.knowledge = knowledge
84
147
  self.settings: AgnoAPISettings = settings or AgnoAPISettings()
85
-
148
+ self.auto_provision_dbs = auto_provision_dbs
86
149
  self._app_set = False
87
- self.fastapi_app: Optional[FastAPI] = None
88
- if fastapi_app:
89
- self.fastapi_app = fastapi_app
150
+
151
+ if base_app:
152
+ self.base_app: Optional[FastAPI] = base_app
90
153
  self._app_set = True
154
+ self.on_route_conflict = on_route_conflict
155
+ else:
156
+ self.base_app = None
157
+ self._app_set = False
158
+ self.on_route_conflict = on_route_conflict
91
159
 
92
160
  self.interfaces = interfaces or []
93
161
 
94
- self.os_id: Optional[str] = os_id
95
162
  self.name = name
163
+
164
+ self.id = id
165
+ if not self.id:
166
+ self.id = generate_id(self.name) if self.name else str(uuid4())
167
+
96
168
  self.version = version
97
169
  self.description = description
98
170
 
99
171
  self.telemetry = telemetry
100
172
 
101
- self.enable_mcp = enable_mcp
173
+ self.enable_mcp_server = enable_mcp_server
102
174
  self.lifespan = lifespan
103
175
 
104
176
  # List of all MCP tools used inside the AgentOS
105
- self.mcp_tools = []
106
-
107
- if self.agents:
108
- for agent in self.agents:
109
- # Track all MCP tools to later handle their connection
110
- if agent.tools:
111
- for tool in agent.tools:
112
- # Checking if the tool is a MCPTools or MultiMCPTools instance
113
- type_name = type(tool).__name__
114
- if type_name in ("MCPTools", "MultiMCPTools"):
115
- self.mcp_tools.append(tool)
177
+ self.mcp_tools: List[Any] = []
178
+ self._mcp_app: Optional[Any] = None
116
179
 
117
- agent.initialize_agent()
180
+ self._initialize_agents()
181
+ self._initialize_teams()
182
+ self._initialize_workflows()
118
183
 
119
- # Required for the built-in routes to work
120
- agent.store_events = True
121
-
122
- if self.teams:
123
- for team in self.teams:
124
- # Track all MCP tools to later handle their connection
125
- if team.tools:
126
- for tool in team.tools:
127
- # Checking if the tool is a MCPTools or MultiMCPTools instance
128
- type_name = type(tool).__name__
129
- if type_name in ("MCPTools", "MultiMCPTools"):
130
- self.mcp_tools.append(tool)
184
+ if self.telemetry:
185
+ from agno.api.os import OSLaunch, log_os_telemetry
131
186
 
132
- team.initialize_team()
187
+ log_os_telemetry(launch=OSLaunch(os_id=self.id, data=self._get_telemetry_data()))
133
188
 
134
- # Required for the built-in routes to work
135
- team.store_events = True
189
+ def _add_agent_os_to_lifespan_function(self, lifespan):
190
+ """
191
+ Inspect a lifespan function and wrap it to pass agent_os if it accepts it.
136
192
 
137
- for member in team.members:
138
- if isinstance(member, Agent):
139
- member.team_id = None
140
- member.initialize_agent()
141
- elif isinstance(member, Team):
142
- member.initialize_team()
193
+ Returns:
194
+ A wrapped lifespan that passes agent_os if the lifespan function expects it.
195
+ """
196
+ # Getting the actual function inside the lifespan
197
+ lifespan_function = lifespan
198
+ if hasattr(lifespan, "__wrapped__"):
199
+ lifespan_function = lifespan.__wrapped__
143
200
 
144
- if self.workflows:
145
- for workflow in self.workflows:
146
- # TODO: track MCP tools in workflow members
147
- if not workflow.id:
148
- workflow.id = generate_id(workflow.name)
201
+ try:
202
+ from inspect import signature
149
203
 
150
- if self.telemetry:
151
- from agno.api.os import OSLaunch, log_os_telemetry
204
+ # Inspecting the lifespan function signature to find its parameters
205
+ sig = signature(lifespan_function)
206
+ params = list(sig.parameters.keys())
207
+
208
+ # If the lifespan function expects the 'agent_os' parameter, add it
209
+ if "agent_os" in params:
210
+ return partial(lifespan, agent_os=self)
211
+ else:
212
+ return lifespan
213
+
214
+ except (ValueError, TypeError):
215
+ return lifespan
216
+
217
+ def resync(self, app: FastAPI) -> None:
218
+ """Resync the AgentOS to discover, initialize and configure: agents, teams, workflows, databases and knowledge bases."""
219
+ self._initialize_agents()
220
+ self._initialize_teams()
221
+ self._initialize_workflows()
222
+ self._auto_discover_databases()
223
+ self._auto_discover_knowledge_instances()
224
+
225
+ if self.enable_mcp_server:
226
+ from agno.os.mcp import get_mcp_server
227
+
228
+ self._mcp_app = get_mcp_server(self)
152
229
 
153
- log_os_telemetry(launch=OSLaunch(os_id=self.os_id, data=self._get_telemetry_data()))
230
+ self._reprovision_routers(app=app)
231
+
232
+ def _reprovision_routers(self, app: FastAPI) -> None:
233
+ """Re-provision all routes for the AgentOS."""
234
+ updated_routers = [
235
+ get_session_router(dbs=self.dbs),
236
+ get_metrics_router(dbs=self.dbs),
237
+ get_knowledge_router(knowledge_instances=self.knowledge_instances),
238
+ get_memory_router(dbs=self.dbs),
239
+ get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
240
+ ]
241
+
242
+ # Clear all previously existing routes
243
+ app.router.routes = [
244
+ route
245
+ for route in app.router.routes
246
+ if hasattr(route, "path")
247
+ and route.path in ["/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"]
248
+ or route.path.startswith("/mcp") # type: ignore
249
+ ]
250
+
251
+ # Add the built-in routes
252
+ self._add_built_in_routes(app=app)
253
+
254
+ # Add the updated routes
255
+ for router in updated_routers:
256
+ self._add_router(app, router)
257
+
258
+ # Mount MCP if needed
259
+ if self.enable_mcp_server and self._mcp_app:
260
+ app.mount("/", self._mcp_app)
261
+
262
+ def _add_built_in_routes(self, app: FastAPI) -> None:
263
+ """Add all AgentOSbuilt-in routes to the given app."""
264
+ # Add the home router if MCP server is not enabled
265
+ if not self.enable_mcp_server:
266
+ self._add_router(app, get_home_router(self))
267
+
268
+ self._add_router(app, get_health_router(health_endpoint="/health"))
269
+ self._add_router(app, get_base_router(self, settings=self.settings))
270
+ self._add_router(app, get_websocket_router(self, settings=self.settings))
271
+
272
+ # Add A2A interface if relevant
273
+ has_a2a_interface = False
274
+ for interface in self.interfaces:
275
+ if not has_a2a_interface and interface.__class__.__name__ == "A2A":
276
+ has_a2a_interface = True
277
+ interface_router = interface.get_router()
278
+ self._add_router(app, interface_router)
279
+ if self.a2a_interface and not has_a2a_interface:
280
+ from agno.os.interfaces.a2a import A2A
281
+
282
+ a2a_interface = A2A(agents=self.agents, teams=self.teams, workflows=self.workflows)
283
+ self.interfaces.append(a2a_interface)
284
+ self._add_router(app, a2a_interface.get_router())
154
285
 
155
286
  def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
156
287
  # Adjust the FastAPI app lifespan to handle MCP connections if relevant
@@ -167,7 +298,7 @@ class AgentOS:
167
298
  async with mcp_tools_lifespan(app): # type: ignore
168
299
  yield
169
300
 
170
- app_lifespan = combined_lifespan # type: ignore
301
+ app_lifespan = combined_lifespan
171
302
  else:
172
303
  app_lifespan = mcp_tools_lifespan
173
304
 
@@ -181,77 +312,175 @@ class AgentOS:
181
312
  lifespan=app_lifespan,
182
313
  )
183
314
 
315
+ def _initialize_agents(self) -> None:
316
+ """Initialize and configure all agents for AgentOS usage."""
317
+ if not self.agents:
318
+ return
319
+
320
+ for agent in self.agents:
321
+ # Track all MCP tools to later handle their connection
322
+ if agent.tools:
323
+ for tool in agent.tools:
324
+ # Checking if the tool is a MCPTools or MultiMCPTools instance
325
+ type_name = type(tool).__name__
326
+ if type_name in ("MCPTools", "MultiMCPTools"):
327
+ if tool not in self.mcp_tools:
328
+ self.mcp_tools.append(tool)
329
+
330
+ agent.initialize_agent()
331
+
332
+ # Required for the built-in routes to work
333
+ agent.store_events = True
334
+
335
+ def _initialize_teams(self) -> None:
336
+ """Initialize and configure all teams for AgentOS usage."""
337
+ if not self.teams:
338
+ return
339
+
340
+ for team in self.teams:
341
+ # Track all MCP tools recursively
342
+ collect_mcp_tools_from_team(team, self.mcp_tools)
343
+
344
+ team.initialize_team()
345
+
346
+ for member in team.members:
347
+ if isinstance(member, Agent):
348
+ member.team_id = None
349
+ member.initialize_agent()
350
+ elif isinstance(member, Team):
351
+ member.initialize_team()
352
+
353
+ # Required for the built-in routes to work
354
+ team.store_events = True
355
+
356
+ def _initialize_workflows(self) -> None:
357
+ """Initialize and configure all workflows for AgentOS usage."""
358
+ if not self.workflows:
359
+ return
360
+
361
+ if self.workflows:
362
+ for workflow in self.workflows:
363
+ # Track MCP tools recursively in workflow members
364
+ collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
365
+
366
+ if not workflow.id:
367
+ workflow.id = generate_id_from_name(workflow.name)
368
+
369
+ # Required for the built-in routes to work
370
+ workflow.store_events = True
371
+
184
372
  def get_app(self) -> FastAPI:
185
- if not self.fastapi_app:
186
- if self.enable_mcp:
373
+ if self.base_app:
374
+ fastapi_app = self.base_app
375
+
376
+ # Initialize MCP server if enabled
377
+ if self.enable_mcp_server:
378
+ from agno.os.mcp import get_mcp_server
379
+
380
+ self._mcp_app = get_mcp_server(self)
381
+
382
+ # Collect all lifespans that need to be combined
383
+ lifespans = []
384
+
385
+ # The user provided lifespan
386
+ if self.lifespan:
387
+ # Wrap the user lifespan with agent_os parameter
388
+ wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
389
+ lifespans.append(wrapped_lifespan)
390
+
391
+ # The provided app's existing lifespan
392
+ if fastapi_app.router.lifespan_context:
393
+ lifespans.append(fastapi_app.router.lifespan_context)
394
+
395
+ # The MCP tools lifespan
396
+ if self.mcp_tools:
397
+ lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
398
+
399
+ # The /mcp server lifespan
400
+ if self.enable_mcp_server and self._mcp_app:
401
+ lifespans.append(self._mcp_app.lifespan)
402
+
403
+ # Combine lifespans and set them in the app
404
+ if lifespans:
405
+ fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
406
+
407
+ else:
408
+ if self.enable_mcp_server:
187
409
  from contextlib import asynccontextmanager
188
410
 
189
411
  from agno.os.mcp import get_mcp_server
190
412
 
191
- self.mcp_app = get_mcp_server(self)
413
+ self._mcp_app = get_mcp_server(self)
192
414
 
193
- final_lifespan = self.mcp_app.lifespan
415
+ final_lifespan = self._mcp_app.lifespan # type: ignore
194
416
  if self.lifespan is not None:
417
+ # Wrap the user lifespan with agent_os parameter
418
+ wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
419
+
195
420
  # Combine both lifespans
196
421
  @asynccontextmanager
197
422
  async def combined_lifespan(app: FastAPI):
198
423
  # Run both lifespans
199
- async with self.lifespan(app): # type: ignore
200
- async with self.mcp_app.lifespan(app): # type: ignore
424
+ async with wrapped_lifespan(app): # type: ignore
425
+ async with self._mcp_app.lifespan(app): # type: ignore
201
426
  yield
202
427
 
203
428
  final_lifespan = combined_lifespan # type: ignore
204
429
 
205
- self.fastapi_app = self._make_app(lifespan=final_lifespan)
430
+ fastapi_app = self._make_app(lifespan=final_lifespan)
206
431
  else:
207
- self.fastapi_app = self._make_app(lifespan=self.lifespan)
432
+ # Wrap the user lifespan with agent_os parameter
433
+ wrapped_user_lifespan = None
434
+ if self.lifespan is not None:
435
+ wrapped_user_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
208
436
 
209
- # Add routes
210
- self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
437
+ fastapi_app = self._make_app(lifespan=wrapped_user_lifespan)
211
438
 
212
- for interface in self.interfaces:
213
- interface_router = interface.get_router()
214
- self.fastapi_app.include_router(interface_router)
439
+ self._add_built_in_routes(app=fastapi_app)
215
440
 
216
441
  self._auto_discover_databases()
217
442
  self._auto_discover_knowledge_instances()
218
- self._setup_routers()
443
+
444
+ routers = [
445
+ get_session_router(dbs=self.dbs),
446
+ get_memory_router(dbs=self.dbs),
447
+ get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
448
+ get_metrics_router(dbs=self.dbs),
449
+ get_knowledge_router(knowledge_instances=self.knowledge_instances),
450
+ ]
451
+
452
+ for router in routers:
453
+ self._add_router(fastapi_app, router)
219
454
 
220
455
  # Mount MCP if needed
221
- if self.enable_mcp and self.mcp_app:
222
- self.fastapi_app.mount("/", self.mcp_app)
456
+ if self.enable_mcp_server and self._mcp_app:
457
+ fastapi_app.mount("/", self._mcp_app)
223
458
 
224
- # Add middleware (only if app is not set)
225
459
  if not self._app_set:
226
460
 
227
- @self.fastapi_app.exception_handler(HTTPException)
228
- async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
461
+ @fastapi_app.exception_handler(HTTPException)
462
+ async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
463
+ log_error(f"HTTP exception: {exc.status_code} {exc.detail}")
229
464
  return JSONResponse(
230
465
  status_code=exc.status_code,
231
466
  content={"detail": str(exc.detail)},
232
467
  )
233
468
 
234
- async def general_exception_handler(request: Request, call_next):
235
- try:
236
- return await call_next(request)
237
- except Exception as e:
238
- return JSONResponse(
239
- status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
240
- content={"detail": str(e)},
241
- )
469
+ @fastapi_app.exception_handler(Exception)
470
+ async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
471
+ import traceback
242
472
 
243
- self.fastapi_app.middleware("http")(general_exception_handler)
473
+ log_error(f"Unhandled exception:\n{traceback.format_exc(limit=5)}")
244
474
 
245
- self.fastapi_app.add_middleware(
246
- CORSMiddleware,
247
- allow_origins=self.settings.cors_origin_list, # type: ignore
248
- allow_credentials=True,
249
- allow_methods=["*"],
250
- allow_headers=["*"],
251
- expose_headers=["*"],
252
- )
475
+ return JSONResponse(
476
+ status_code=getattr(exc, "status_code", 500),
477
+ content={"detail": str(exc)},
478
+ )
479
+
480
+ # Update CORS middleware
481
+ update_cors_middleware(fastapi_app, self.settings.cors_origin_list) # type: ignore
253
482
 
254
- return self.fastapi_app
483
+ return fastapi_app
255
484
 
256
485
  def get_routes(self) -> List[Any]:
257
486
  """Retrieve all routes from the FastAPI app.
@@ -263,6 +492,62 @@ class AgentOS:
263
492
 
264
493
  return app.routes
265
494
 
495
+ def _add_router(self, fastapi_app: FastAPI, router: APIRouter) -> None:
496
+ """Add a router to the FastAPI app, avoiding route conflicts.
497
+
498
+ Args:
499
+ router: The APIRouter to add
500
+ """
501
+
502
+ conflicts = find_conflicting_routes(fastapi_app, router)
503
+ conflicting_routes = [conflict["route"] for conflict in conflicts]
504
+
505
+ if conflicts and self._app_set:
506
+ if self.on_route_conflict == "preserve_base_app":
507
+ # Skip conflicting AgentOS routes, prefer user's existing routes
508
+ for conflict in conflicts:
509
+ methods_str = ", ".join(conflict["methods"]) # type: ignore
510
+ log_debug(
511
+ f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
512
+ f"Using existing custom route instead"
513
+ )
514
+
515
+ # Create a new router without the conflicting routes
516
+ filtered_router = APIRouter()
517
+ for route in router.routes:
518
+ if route not in conflicting_routes:
519
+ filtered_router.routes.append(route)
520
+
521
+ # Use the filtered router if it has any routes left
522
+ if filtered_router.routes:
523
+ fastapi_app.include_router(filtered_router)
524
+
525
+ elif self.on_route_conflict == "preserve_agentos":
526
+ # Log warnings but still add all routes (AgentOS routes will override)
527
+ for conflict in conflicts:
528
+ methods_str = ", ".join(conflict["methods"]) # type: ignore
529
+ log_warning(
530
+ f"Route conflict detected: {methods_str} {conflict['path']} - "
531
+ f"AgentOS route will override existing custom route"
532
+ )
533
+
534
+ # Remove conflicting routes
535
+ for route in fastapi_app.routes:
536
+ for conflict in conflicts:
537
+ if isinstance(route, APIRoute):
538
+ if route.path == conflict["path"] and list(route.methods) == list(conflict["methods"]): # type: ignore
539
+ fastapi_app.routes.pop(fastapi_app.routes.index(route))
540
+
541
+ fastapi_app.include_router(router)
542
+
543
+ elif self.on_route_conflict == "error":
544
+ conflicting_paths = [conflict["path"] for conflict in conflicts]
545
+ raise ValueError(f"Route conflict detected: {conflicting_paths}")
546
+
547
+ else:
548
+ # No conflicts, add router normally
549
+ fastapi_app.include_router(router)
550
+
266
551
  def _get_telemetry_data(self) -> Dict[str, Any]:
267
552
  """Get the telemetry data for the OS"""
268
553
  return {
@@ -272,59 +557,160 @@ class AgentOS:
272
557
  "interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
273
558
  }
274
559
 
275
- def _load_yaml_config(self, config_file_path: str) -> AgentOSConfig:
276
- """Load a YAML config file and return the configuration as an AgentOSConfig instance."""
277
- from pathlib import Path
278
-
279
- import yaml
280
-
281
- # Validate that the path points to a YAML file
282
- path = Path(config_file_path)
283
- if path.suffix.lower() not in [".yaml", ".yml"]:
284
- raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
285
-
286
- # Load the YAML file
287
- with open(config_file_path, "r") as f:
288
- return AgentOSConfig.model_validate(yaml.safe_load(f))
289
-
290
560
  def _auto_discover_databases(self) -> None:
291
- """Auto-discover the databases used by all contextual agents, teams and workflows."""
292
- dbs = {}
561
+ """Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
562
+
563
+ dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
564
+ knowledge_dbs: Dict[
565
+ str, List[Union[BaseDb, AsyncBaseDb]]
566
+ ] = {} # Track databases specifically used for knowledge
293
567
 
294
568
  for agent in self.agents or []:
295
569
  if agent.db:
296
- dbs[agent.db.id] = agent.db
570
+ self._register_db_with_validation(dbs, agent.db)
297
571
  if agent.knowledge and agent.knowledge.contents_db:
298
- dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
572
+ self._register_db_with_validation(knowledge_dbs, agent.knowledge.contents_db)
299
573
 
300
574
  for team in self.teams or []:
301
575
  if team.db:
302
- dbs[team.db.id] = team.db
576
+ self._register_db_with_validation(dbs, team.db)
303
577
  if team.knowledge and team.knowledge.contents_db:
304
- dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
578
+ self._register_db_with_validation(knowledge_dbs, team.knowledge.contents_db)
305
579
 
306
580
  for workflow in self.workflows or []:
307
581
  if workflow.db:
308
- dbs[workflow.db.id] = workflow.db
582
+ self._register_db_with_validation(dbs, workflow.db)
583
+
584
+ for knowledge_base in self.knowledge or []:
585
+ if knowledge_base.contents_db:
586
+ self._register_db_with_validation(knowledge_dbs, knowledge_base.contents_db)
309
587
 
310
588
  for interface in self.interfaces or []:
311
589
  if interface.agent and interface.agent.db:
312
- dbs[interface.agent.db.id] = interface.agent.db
590
+ self._register_db_with_validation(dbs, interface.agent.db)
313
591
  elif interface.team and interface.team.db:
314
- dbs[interface.team.db.id] = interface.team.db
592
+ self._register_db_with_validation(dbs, interface.team.db)
315
593
 
316
594
  self.dbs = dbs
595
+ self.knowledge_dbs = knowledge_dbs
596
+
597
+ # Initialize/scaffold all discovered databases
598
+ if self.auto_provision_dbs:
599
+ import asyncio
600
+ import concurrent.futures
601
+
602
+ try:
603
+ # If we're already in an event loop, run in a separate thread
604
+ asyncio.get_running_loop()
605
+
606
+ def run_in_new_loop():
607
+ new_loop = asyncio.new_event_loop()
608
+ asyncio.set_event_loop(new_loop)
609
+ try:
610
+ return new_loop.run_until_complete(self._initialize_databases())
611
+ finally:
612
+ new_loop.close()
613
+
614
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
615
+ future = executor.submit(run_in_new_loop)
616
+ future.result() # Wait for completion
617
+
618
+ except RuntimeError:
619
+ # No event loop running, use asyncio.run
620
+ asyncio.run(self._initialize_databases())
621
+
622
+ async def _initialize_databases(self) -> None:
623
+ """Initialize all discovered databases and create all Agno tables that don't exist yet."""
624
+ from itertools import chain
625
+
626
+ # Collect all database instances and remove duplicates by identity
627
+ unique_dbs = list(
628
+ {
629
+ id(db): db
630
+ for db in chain(
631
+ chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
632
+ )
633
+ }.values()
634
+ )
635
+
636
+ # Separate sync and async databases
637
+ sync_dbs: List[Tuple[str, BaseDb]] = []
638
+ async_dbs: List[Tuple[str, AsyncBaseDb]] = []
639
+
640
+ for db in unique_dbs:
641
+ target = async_dbs if isinstance(db, AsyncBaseDb) else sync_dbs
642
+ target.append((db.id, db)) # type: ignore
643
+
644
+ # Initialize sync databases
645
+ for db_id, db in sync_dbs:
646
+ try:
647
+ if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
648
+ db._create_all_tables()
649
+ else:
650
+ log_debug(f"No table initialization needed for {db.__class__.__name__}")
651
+
652
+ except Exception as e:
653
+ log_warning(f"Failed to initialize {db.__class__.__name__} (id: {db_id}): {e}")
654
+
655
+ # Initialize async databases
656
+ for db_id, db in async_dbs:
657
+ try:
658
+ log_debug(f"Initializing async {db.__class__.__name__} (id: {db_id})")
659
+
660
+ if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
661
+ await db._create_all_tables()
662
+ else:
663
+ log_debug(f"No table initialization needed for async {db.__class__.__name__}")
664
+
665
+ except Exception as e:
666
+ log_warning(f"Failed to initialize async database {db.__class__.__name__} (id: {db_id}): {e}")
667
+
668
+ def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
669
+ """Get the table names for a database"""
670
+ table_names = {
671
+ "session_table_name": db.session_table_name,
672
+ "culture_table_name": db.culture_table_name,
673
+ "memory_table_name": db.memory_table_name,
674
+ "metrics_table_name": db.metrics_table_name,
675
+ "evals_table_name": db.eval_table_name,
676
+ "knowledge_table_name": db.knowledge_table_name,
677
+ }
678
+ return {k: v for k, v in table_names.items() if v is not None}
679
+
680
+ def _register_db_with_validation(
681
+ self, registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]], db: Union[BaseDb, AsyncBaseDb]
682
+ ) -> None:
683
+ """Register a database in the contextual OS after validating it is not conflicting with registered databases"""
684
+ if db.id in registered_dbs:
685
+ registered_dbs[db.id].append(db)
686
+ else:
687
+ registered_dbs[db.id] = [db]
317
688
 
318
689
  def _auto_discover_knowledge_instances(self) -> None:
319
690
  """Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
320
- knowledge_instances = []
691
+ seen_ids = set()
692
+ knowledge_instances: List[Knowledge] = []
693
+
694
+ def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
695
+ """Add knowledge instance if it's not already in the list (by object identity or db_id)."""
696
+ # Use database ID if available, otherwise use object ID as fallback
697
+ if not knowledge.contents_db:
698
+ return
699
+ if knowledge.contents_db.id in seen_ids:
700
+ return
701
+ seen_ids.add(knowledge.contents_db.id)
702
+ knowledge_instances.append(knowledge)
703
+
321
704
  for agent in self.agents or []:
322
705
  if agent.knowledge:
323
- knowledge_instances.append(agent.knowledge)
706
+ _add_knowledge_if_not_duplicate(agent.knowledge)
324
707
 
325
708
  for team in self.teams or []:
326
709
  if team.knowledge:
327
- knowledge_instances.append(team.knowledge)
710
+ _add_knowledge_if_not_duplicate(team.knowledge)
711
+
712
+ for knowledge_base in self.knowledge or []:
713
+ _add_knowledge_if_not_duplicate(knowledge_base)
328
714
 
329
715
  self.knowledge_instances = knowledge_instances
330
716
 
@@ -334,17 +720,16 @@ class AgentOS:
334
720
  if session_config.dbs is None:
335
721
  session_config.dbs = []
336
722
 
337
- multiple_dbs: bool = len(self.dbs.keys()) > 1
338
723
  dbs_with_specific_config = [db.db_id for db in session_config.dbs]
339
-
340
- for db_id in self.dbs.keys():
724
+ for db_id, dbs in self.dbs.items():
341
725
  if db_id not in dbs_with_specific_config:
726
+ # Collect unique table names from all databases with the same id
727
+ unique_tables = list(set(db.session_table_name for db in dbs))
342
728
  session_config.dbs.append(
343
729
  DatabaseConfig(
344
730
  db_id=db_id,
345
- domain_config=SessionDomainConfig(
346
- display_name="Sessions" if not multiple_dbs else "Sessions in database '" + db_id + "'"
347
- ),
731
+ domain_config=SessionDomainConfig(display_name=db_id),
732
+ tables=unique_tables,
348
733
  )
349
734
  )
350
735
 
@@ -356,17 +741,17 @@ class AgentOS:
356
741
  if memory_config.dbs is None:
357
742
  memory_config.dbs = []
358
743
 
359
- multiple_dbs: bool = len(self.dbs.keys()) > 1
360
744
  dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
361
745
 
362
- for db_id in self.dbs.keys():
746
+ for db_id, dbs in self.dbs.items():
363
747
  if db_id not in dbs_with_specific_config:
748
+ # Collect unique table names from all databases with the same id
749
+ unique_tables = list(set(db.memory_table_name for db in dbs))
364
750
  memory_config.dbs.append(
365
751
  DatabaseConfig(
366
752
  db_id=db_id,
367
- domain_config=MemoryDomainConfig(
368
- display_name="Memory" if not multiple_dbs else "Memory in database '" + db_id + "'"
369
- ),
753
+ domain_config=MemoryDomainConfig(display_name=db_id),
754
+ tables=unique_tables,
370
755
  )
371
756
  )
372
757
 
@@ -378,17 +763,15 @@ class AgentOS:
378
763
  if knowledge_config.dbs is None:
379
764
  knowledge_config.dbs = []
380
765
 
381
- multiple_dbs: bool = len(self.dbs.keys()) > 1
382
766
  dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
383
767
 
384
- for db_id in self.dbs.keys():
768
+ # Only add databases that are actually used for knowledge contents
769
+ for db_id in self.knowledge_dbs.keys():
385
770
  if db_id not in dbs_with_specific_config:
386
771
  knowledge_config.dbs.append(
387
772
  DatabaseConfig(
388
773
  db_id=db_id,
389
- domain_config=KnowledgeDomainConfig(
390
- display_name="Knowledge" if not multiple_dbs else "Knowledge in database " + db_id
391
- ),
774
+ domain_config=KnowledgeDomainConfig(display_name=db_id),
392
775
  )
393
776
  )
394
777
 
@@ -400,17 +783,17 @@ class AgentOS:
400
783
  if metrics_config.dbs is None:
401
784
  metrics_config.dbs = []
402
785
 
403
- multiple_dbs: bool = len(self.dbs.keys()) > 1
404
786
  dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
405
787
 
406
- for db_id in self.dbs.keys():
788
+ for db_id, dbs in self.dbs.items():
407
789
  if db_id not in dbs_with_specific_config:
790
+ # Collect unique table names from all databases with the same id
791
+ unique_tables = list(set(db.metrics_table_name for db in dbs))
408
792
  metrics_config.dbs.append(
409
793
  DatabaseConfig(
410
794
  db_id=db_id,
411
- domain_config=MetricsDomainConfig(
412
- display_name="Metrics" if not multiple_dbs else "Metrics in database '" + db_id + "'"
413
- ),
795
+ domain_config=MetricsDomainConfig(display_name=db_id),
796
+ tables=unique_tables,
414
797
  )
415
798
  )
416
799
 
@@ -422,45 +805,22 @@ class AgentOS:
422
805
  if evals_config.dbs is None:
423
806
  evals_config.dbs = []
424
807
 
425
- multiple_dbs: bool = len(self.dbs.keys()) > 1
426
808
  dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
427
809
 
428
- for db_id in self.dbs.keys():
810
+ for db_id, dbs in self.dbs.items():
429
811
  if db_id not in dbs_with_specific_config:
812
+ # Collect unique table names from all databases with the same id
813
+ unique_tables = list(set(db.eval_table_name for db in dbs))
430
814
  evals_config.dbs.append(
431
815
  DatabaseConfig(
432
816
  db_id=db_id,
433
- domain_config=EvalsDomainConfig(
434
- display_name="Evals" if not multiple_dbs else "Evals in database '" + db_id + "'"
435
- ),
817
+ domain_config=EvalsDomainConfig(display_name=db_id),
818
+ tables=unique_tables,
436
819
  )
437
820
  )
438
821
 
439
822
  return evals_config
440
823
 
441
- def _setup_routers(self) -> None:
442
- """Add all routers to the FastAPI app."""
443
- if not self.dbs or not self.fastapi_app:
444
- return
445
-
446
- routers = [
447
- get_session_router(dbs=self.dbs),
448
- get_memory_router(dbs=self.dbs),
449
- get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
450
- get_metrics_router(dbs=self.dbs),
451
- get_knowledge_router(knowledge_instances=self.knowledge_instances),
452
- ]
453
-
454
- for router in routers:
455
- self.fastapi_app.include_router(router)
456
-
457
- def set_os_id(self) -> str:
458
- # If os_id is already set, keep it instead of overriding with UUID
459
- if self.os_id is None:
460
- self.os_id = str(uuid4())
461
-
462
- return self.os_id
463
-
464
824
  def serve(
465
825
  self,
466
826
  app: Union[str, FastAPI],
@@ -469,6 +829,7 @@ class AgentOS:
469
829
  port: int = 7777,
470
830
  reload: bool = False,
471
831
  workers: Optional[int] = None,
832
+ access_log: bool = False,
472
833
  **kwargs,
473
834
  ):
474
835
  import uvicorn
@@ -482,13 +843,17 @@ class AgentOS:
482
843
  from rich.align import Align
483
844
  from rich.console import Console, Group
484
845
 
485
- aligned_endpoint = Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]")
486
- connection_endpoint = f"\n\n[bold dark_orange]Running on:[/bold dark_orange] http://{host}:{port}"
846
+ panel_group = [
847
+ Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"),
848
+ Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}"),
849
+ ]
850
+ if bool(self.settings.os_security_key):
851
+ panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
487
852
 
488
853
  console = Console()
489
854
  console.print(
490
855
  Panel(
491
- Group(aligned_endpoint, connection_endpoint),
856
+ Group(*panel_group),
492
857
  title="AgentOS",
493
858
  expand=False,
494
859
  border_style="dark_orange",
@@ -497,4 +862,4 @@ class AgentOS:
497
862
  )
498
863
  )
499
864
 
500
- uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, **kwargs)
865
+ uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, access_log=access_log, **kwargs)