agno 2.0.10__py3-none-any.whl → 2.1.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 (85) hide show
  1. agno/agent/agent.py +608 -175
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/postgres/postgres.py +6 -4
  4. agno/exceptions.py +62 -1
  5. agno/guardrails/__init__.py +6 -0
  6. agno/guardrails/base.py +19 -0
  7. agno/guardrails/openai.py +144 -0
  8. agno/guardrails/pii.py +94 -0
  9. agno/guardrails/prompt_injection.py +51 -0
  10. agno/knowledge/embedder/aws_bedrock.py +9 -4
  11. agno/knowledge/embedder/azure_openai.py +54 -0
  12. agno/knowledge/embedder/base.py +2 -0
  13. agno/knowledge/embedder/cohere.py +184 -5
  14. agno/knowledge/embedder/google.py +79 -1
  15. agno/knowledge/embedder/huggingface.py +9 -4
  16. agno/knowledge/embedder/jina.py +63 -0
  17. agno/knowledge/embedder/mistral.py +78 -11
  18. agno/knowledge/embedder/ollama.py +5 -0
  19. agno/knowledge/embedder/openai.py +18 -54
  20. agno/knowledge/embedder/voyageai.py +69 -16
  21. agno/knowledge/knowledge.py +5 -4
  22. agno/knowledge/reader/pdf_reader.py +4 -3
  23. agno/knowledge/reader/website_reader.py +3 -2
  24. agno/models/base.py +125 -32
  25. agno/models/cerebras/cerebras.py +1 -0
  26. agno/models/cerebras/cerebras_openai.py +1 -0
  27. agno/models/dashscope/dashscope.py +1 -0
  28. agno/models/google/gemini.py +27 -5
  29. agno/models/litellm/chat.py +17 -0
  30. agno/models/openai/chat.py +13 -4
  31. agno/models/perplexity/perplexity.py +2 -3
  32. agno/models/requesty/__init__.py +5 -0
  33. agno/models/requesty/requesty.py +49 -0
  34. agno/models/vllm/vllm.py +1 -0
  35. agno/models/xai/xai.py +1 -0
  36. agno/os/app.py +167 -148
  37. agno/os/interfaces/whatsapp/router.py +2 -0
  38. agno/os/mcp.py +1 -1
  39. agno/os/middleware/__init__.py +7 -0
  40. agno/os/middleware/jwt.py +233 -0
  41. agno/os/router.py +181 -45
  42. agno/os/routers/home.py +2 -2
  43. agno/os/routers/memory/memory.py +23 -1
  44. agno/os/routers/memory/schemas.py +1 -1
  45. agno/os/routers/session/session.py +20 -3
  46. agno/os/utils.py +172 -8
  47. agno/run/agent.py +120 -77
  48. agno/run/team.py +115 -72
  49. agno/run/workflow.py +5 -15
  50. agno/session/summary.py +9 -10
  51. agno/session/team.py +2 -1
  52. agno/team/team.py +720 -168
  53. agno/tools/firecrawl.py +4 -4
  54. agno/tools/function.py +42 -2
  55. agno/tools/knowledge.py +3 -3
  56. agno/tools/searxng.py +2 -2
  57. agno/tools/serper.py +2 -2
  58. agno/tools/spider.py +2 -2
  59. agno/tools/workflow.py +4 -5
  60. agno/utils/events.py +66 -1
  61. agno/utils/hooks.py +57 -0
  62. agno/utils/media.py +11 -9
  63. agno/utils/print_response/agent.py +43 -5
  64. agno/utils/print_response/team.py +48 -12
  65. agno/vectordb/cassandra/cassandra.py +44 -4
  66. agno/vectordb/chroma/chromadb.py +79 -8
  67. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  68. agno/vectordb/couchbase/couchbase.py +76 -5
  69. agno/vectordb/lancedb/lance_db.py +38 -3
  70. agno/vectordb/llamaindex/__init__.py +3 -0
  71. agno/vectordb/milvus/milvus.py +76 -4
  72. agno/vectordb/mongodb/mongodb.py +76 -4
  73. agno/vectordb/pgvector/pgvector.py +50 -6
  74. agno/vectordb/pineconedb/pineconedb.py +39 -2
  75. agno/vectordb/qdrant/qdrant.py +76 -26
  76. agno/vectordb/singlestore/singlestore.py +77 -4
  77. agno/vectordb/upstashdb/upstashdb.py +42 -2
  78. agno/vectordb/weaviate/weaviate.py +39 -3
  79. agno/workflow/types.py +1 -0
  80. agno/workflow/workflow.py +58 -2
  81. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/METADATA +4 -3
  82. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/RECORD +85 -75
  83. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/WHEEL +0 -0
  84. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/licenses/LICENSE +0 -0
  85. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/top_level.txt +0 -0
agno/os/app.py CHANGED
@@ -1,7 +1,7 @@
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, Set, Union
4
+ from typing import Any, Dict, List, Literal, Optional, Union
5
5
  from uuid import uuid4
6
6
 
7
7
  from fastapi import APIRouter, FastAPI, HTTPException
@@ -12,6 +12,7 @@ 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
16
  from agno.os.config import (
16
17
  AgentOSConfig,
17
18
  DatabaseConfig,
@@ -36,7 +37,13 @@ from agno.os.routers.memory import get_memory_router
36
37
  from agno.os.routers.metrics import get_metrics_router
37
38
  from agno.os.routers.session import get_session_router
38
39
  from agno.os.settings import AgnoAPISettings
39
- from agno.os.utils import update_cors_middleware
40
+ from agno.os.utils import (
41
+ collect_mcp_tools_from_team,
42
+ collect_mcp_tools_from_workflow,
43
+ find_conflicting_routes,
44
+ load_yaml_config,
45
+ update_cors_middleware,
46
+ )
40
47
  from agno.team.team import Team
41
48
  from agno.utils.log import logger
42
49
  from agno.utils.string import generate_id, generate_id_from_name
@@ -44,7 +51,7 @@ from agno.workflow.workflow import Workflow
44
51
 
45
52
 
46
53
  @asynccontextmanager
47
- async def mcp_lifespan(app, mcp_tools):
54
+ async def mcp_lifespan(_, mcp_tools):
48
55
  """Manage MCP connection lifecycle inside a FastAPI app"""
49
56
  # Startup logic: connect to all contextual MCP servers
50
57
  for tool in mcp_tools:
@@ -60,7 +67,8 @@ async def mcp_lifespan(app, mcp_tools):
60
67
  class AgentOS:
61
68
  def __init__(
62
69
  self,
63
- os_id: Optional[str] = None,
70
+ id: Optional[str] = None,
71
+ os_id: Optional[str] = None, # Deprecated
64
72
  name: Optional[str] = None,
65
73
  description: Optional[str] = None,
66
74
  version: Optional[str] = None,
@@ -70,16 +78,19 @@ class AgentOS:
70
78
  interfaces: Optional[List[BaseInterface]] = None,
71
79
  config: Optional[Union[str, AgentOSConfig]] = None,
72
80
  settings: Optional[AgnoAPISettings] = None,
73
- fastapi_app: Optional[FastAPI] = None,
74
81
  lifespan: Optional[Any] = None,
75
- enable_mcp: bool = False,
76
- replace_routes: bool = True,
82
+ enable_mcp: bool = False, # Deprecated
83
+ enable_mcp_server: bool = False,
84
+ fastapi_app: Optional[FastAPI] = None, # Deprecated
85
+ base_app: Optional[FastAPI] = None,
86
+ replace_routes: Optional[bool] = None, # Deprecated
87
+ on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
77
88
  telemetry: bool = True,
78
89
  ):
79
90
  """Initialize AgentOS.
80
91
 
81
92
  Args:
82
- os_id: Unique identifier for this AgentOS instance
93
+ id: Unique identifier for this AgentOS instance
83
94
  name: Name of the AgentOS instance
84
95
  description: Description of the AgentOS instance
85
96
  version: Version of the AgentOS instance
@@ -89,18 +100,16 @@ class AgentOS:
89
100
  interfaces: List of interfaces to include in the OS
90
101
  config: Configuration file path or AgentOSConfig instance
91
102
  settings: API settings for the OS
92
- fastapi_app: Optional custom FastAPI app to use instead of creating a new one
93
103
  lifespan: Optional lifespan context manager for the FastAPI app
94
- enable_mcp: Whether to enable MCP (Model Context Protocol)
95
- replace_routes: If False and using a custom fastapi_app, skip AgentOS routes that
96
- conflict with existing routes, preferring the user's custom routes.
97
- If True (default), AgentOS routes will override conflicting custom routes.
104
+ enable_mcp_server: Whether to enable MCP (Model Context Protocol)
105
+ base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
106
+ on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
98
107
  telemetry: Whether to enable telemetry
99
108
  """
100
109
  if not agents and not workflows and not teams:
101
110
  raise ValueError("Either agents, teams or workflows must be provided.")
102
111
 
103
- self.config = self._load_yaml_config(config) if isinstance(config, str) else config
112
+ self.config = load_yaml_config(config) if isinstance(config, str) else config
104
113
 
105
114
  self.agents: Optional[List[Agent]] = agents
106
115
  self.workflows: Optional[List[Workflow]] = workflows
@@ -110,27 +119,42 @@ class AgentOS:
110
119
  self.settings: AgnoAPISettings = settings or AgnoAPISettings()
111
120
 
112
121
  self._app_set = False
113
- self.fastapi_app: Optional[FastAPI] = None
114
- if fastapi_app:
115
- self.fastapi_app = fastapi_app
122
+
123
+ if base_app:
124
+ self.base_app: Optional[FastAPI] = base_app
125
+ self._app_set = True
126
+ self.on_route_conflict = on_route_conflict
127
+ elif fastapi_app:
128
+ self.base_app = fastapi_app
116
129
  self._app_set = True
130
+ if replace_routes is not None:
131
+ self.on_route_conflict = "preserve_agentos" if replace_routes else "preserve_base_app"
132
+ else:
133
+ self.on_route_conflict = on_route_conflict
134
+ else:
135
+ self.base_app = None
136
+ self._app_set = False
137
+ self.on_route_conflict = on_route_conflict
117
138
 
118
139
  self.interfaces = interfaces or []
119
140
 
120
- self.os_id = os_id
121
141
  self.name = name
142
+
143
+ self.id = id or os_id
144
+ if not self.id:
145
+ self.id = generate_id(self.name) if self.name else str(uuid4())
146
+
122
147
  self.version = version
123
148
  self.description = description
124
149
 
125
- self.replace_routes = replace_routes
126
-
127
150
  self.telemetry = telemetry
128
151
 
129
- self.enable_mcp = enable_mcp
152
+ self.enable_mcp_server = enable_mcp or enable_mcp_server
130
153
  self.lifespan = lifespan
131
154
 
132
155
  # List of all MCP tools used inside the AgentOS
133
- self.mcp_tools = []
156
+ self.mcp_tools: List[Any] = []
157
+ self._mcp_app: Optional[Any] = None
134
158
 
135
159
  if self.agents:
136
160
  for agent in self.agents:
@@ -140,7 +164,8 @@ class AgentOS:
140
164
  # Checking if the tool is a MCPTools or MultiMCPTools instance
141
165
  type_name = type(tool).__name__
142
166
  if type_name in ("MCPTools", "MultiMCPTools"):
143
- self.mcp_tools.append(tool)
167
+ if tool not in self.mcp_tools:
168
+ self.mcp_tools.append(tool)
144
169
 
145
170
  agent.initialize_agent()
146
171
 
@@ -149,13 +174,8 @@ class AgentOS:
149
174
 
150
175
  if self.teams:
151
176
  for team in self.teams:
152
- # Track all MCP tools to later handle their connection
153
- if team.tools:
154
- for tool in team.tools:
155
- # Checking if the tool is a MCPTools or MultiMCPTools instance
156
- type_name = type(tool).__name__
157
- if type_name in ("MCPTools", "MultiMCPTools"):
158
- self.mcp_tools.append(tool)
177
+ # Track all MCP tools recursively
178
+ collect_mcp_tools_from_team(team, self.mcp_tools)
159
179
 
160
180
  team.initialize_team()
161
181
 
@@ -171,17 +191,15 @@ class AgentOS:
171
191
 
172
192
  if self.workflows:
173
193
  for workflow in self.workflows:
174
- # TODO: track MCP tools in workflow members
194
+ # Track MCP tools recursively in workflow members
195
+ collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
175
196
  if not workflow.id:
176
197
  workflow.id = generate_id_from_name(workflow.name)
177
198
 
178
- if not self.os_id:
179
- self.os_id = generate_id(self.name) if self.name else str(uuid4())
180
-
181
199
  if self.telemetry:
182
200
  from agno.api.os import OSLaunch, log_os_telemetry
183
201
 
184
- log_os_telemetry(launch=OSLaunch(os_id=self.os_id, data=self._get_telemetry_data()))
202
+ log_os_telemetry(launch=OSLaunch(os_id=self.id, data=self._get_telemetry_data()))
185
203
 
186
204
  def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
187
205
  # Adjust the FastAPI app lifespan to handle MCP connections if relevant
@@ -213,39 +231,41 @@ class AgentOS:
213
231
  )
214
232
 
215
233
  def get_app(self) -> FastAPI:
216
- if not self.fastapi_app:
217
- if self.enable_mcp:
234
+ if self.base_app:
235
+ fastapi_app = self.base_app
236
+ else:
237
+ if self.enable_mcp_server:
218
238
  from contextlib import asynccontextmanager
219
239
 
220
240
  from agno.os.mcp import get_mcp_server
221
241
 
222
- self.mcp_app = get_mcp_server(self)
242
+ self._mcp_app = get_mcp_server(self)
223
243
 
224
- final_lifespan = self.mcp_app.lifespan
244
+ final_lifespan = self._mcp_app.lifespan # type: ignore
225
245
  if self.lifespan is not None:
226
246
  # Combine both lifespans
227
247
  @asynccontextmanager
228
248
  async def combined_lifespan(app: FastAPI):
229
249
  # Run both lifespans
230
250
  async with self.lifespan(app): # type: ignore
231
- async with self.mcp_app.lifespan(app): # type: ignore
251
+ async with self._mcp_app.lifespan(app): # type: ignore
232
252
  yield
233
253
 
234
254
  final_lifespan = combined_lifespan # type: ignore
235
255
 
236
- self.fastapi_app = self._make_app(lifespan=final_lifespan)
256
+ fastapi_app = self._make_app(lifespan=final_lifespan)
237
257
  else:
238
- self.fastapi_app = self._make_app(lifespan=self.lifespan)
258
+ fastapi_app = self._make_app(lifespan=self.lifespan)
239
259
 
240
- # Add routes with conflict detection
241
- self._add_router(get_base_router(self, settings=self.settings))
242
- self._add_router(get_websocket_router(self, settings=self.settings))
243
- self._add_router(get_health_router())
244
- self._add_router(get_home_router(self))
260
+ # Add routes
261
+ self._add_router(fastapi_app, get_base_router(self, settings=self.settings))
262
+ self._add_router(fastapi_app, get_websocket_router(self, settings=self.settings))
263
+ self._add_router(fastapi_app, get_health_router())
264
+ self._add_router(fastapi_app, get_home_router(self))
245
265
 
246
266
  for interface in self.interfaces:
247
267
  interface_router = interface.get_router()
248
- self._add_router(interface_router)
268
+ self._add_router(fastapi_app, interface_router)
249
269
 
250
270
  self._auto_discover_databases()
251
271
  self._auto_discover_knowledge_instances()
@@ -259,17 +279,19 @@ class AgentOS:
259
279
  ]
260
280
 
261
281
  for router in routers:
262
- self._add_router(router)
282
+ self._add_router(fastapi_app, router)
263
283
 
264
284
  # Mount MCP if needed
265
- if self.enable_mcp and self.mcp_app:
266
- self.fastapi_app.mount("/", self.mcp_app)
285
+ if self.enable_mcp_server and self._mcp_app:
286
+ fastapi_app.mount("/", self._mcp_app)
287
+ else:
288
+ # Add the home router
289
+ self._add_router(fastapi_app, get_home_router(self))
267
290
 
268
- # Add middleware (only if app is not set)
269
291
  if not self._app_set:
270
292
 
271
- @self.fastapi_app.exception_handler(HTTPException)
272
- async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
293
+ @fastapi_app.exception_handler(HTTPException)
294
+ async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
273
295
  return JSONResponse(
274
296
  status_code=exc.status_code,
275
297
  content={"detail": str(exc.detail)},
@@ -284,12 +306,12 @@ class AgentOS:
284
306
  content={"detail": str(e)},
285
307
  )
286
308
 
287
- self.fastapi_app.middleware("http")(general_exception_handler)
309
+ fastapi_app.middleware("http")(general_exception_handler)
288
310
 
289
311
  # Update CORS middleware
290
- update_cors_middleware(self.fastapi_app, self.settings.cors_origin_list) # type: ignore
312
+ update_cors_middleware(fastapi_app, self.settings.cors_origin_list) # type: ignore
291
313
 
292
- return self.fastapi_app
314
+ return fastapi_app
293
315
 
294
316
  def get_routes(self) -> List[Any]:
295
317
  """Retrieve all routes from the FastAPI app.
@@ -301,55 +323,37 @@ class AgentOS:
301
323
 
302
324
  return app.routes
303
325
 
304
- def _get_existing_route_paths(self) -> Dict[str, List[str]]:
305
- """Get all existing route paths and methods from the FastAPI app.
306
-
307
- Returns:
308
- Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
309
- """
310
- if not self.fastapi_app:
311
- return {}
312
-
313
- existing_paths: Dict[str, Any] = {}
314
- for route in self.fastapi_app.routes:
315
- if isinstance(route, APIRoute):
316
- path = route.path
317
- methods = list(route.methods) if route.methods else []
318
- if path in existing_paths:
319
- existing_paths[path].extend(methods)
320
- else:
321
- existing_paths[path] = methods
322
- return existing_paths
323
-
324
- def _add_router(self, router: APIRouter) -> None:
326
+ def _add_router(self, fastapi_app: FastAPI, router: APIRouter) -> None:
325
327
  """Add a router to the FastAPI app, avoiding route conflicts.
326
328
 
327
329
  Args:
328
330
  router: The APIRouter to add
329
331
  """
330
- if not self.fastapi_app:
331
- return
332
332
 
333
- # Get existing routes
334
- existing_paths = self._get_existing_route_paths()
333
+ conflicts = find_conflicting_routes(fastapi_app, router)
334
+ conflicting_routes = [conflict["route"] for conflict in conflicts]
335
335
 
336
- # Check for conflicts
337
- conflicts = []
338
- conflicting_routes = []
336
+ if conflicts and self._app_set:
337
+ if self.on_route_conflict == "preserve_base_app":
338
+ # Skip conflicting AgentOS routes, prefer user's existing routes
339
+ for conflict in conflicts:
340
+ methods_str = ", ".join(conflict["methods"]) # type: ignore
341
+ logger.debug(
342
+ f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
343
+ f"Using existing custom route instead"
344
+ )
339
345
 
340
- for route in router.routes:
341
- if isinstance(route, APIRoute):
342
- full_path = route.path
343
- route_methods = list(route.methods) if route.methods else []
346
+ # Create a new router without the conflicting routes
347
+ filtered_router = APIRouter()
348
+ for route in router.routes:
349
+ if route not in conflicting_routes:
350
+ filtered_router.routes.append(route)
344
351
 
345
- if full_path in existing_paths:
346
- conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
347
- if conflicting_methods:
348
- conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
349
- conflicting_routes.append(route)
352
+ # Use the filtered router if it has any routes left
353
+ if filtered_router.routes:
354
+ fastapi_app.include_router(filtered_router)
350
355
 
351
- if conflicts and self._app_set:
352
- if self.replace_routes:
356
+ elif self.on_route_conflict == "preserve_agentos":
353
357
  # Log warnings but still add all routes (AgentOS routes will override)
354
358
  for conflict in conflicts:
355
359
  methods_str = ", ".join(conflict["methods"]) # type: ignore
@@ -359,35 +363,21 @@ class AgentOS:
359
363
  )
360
364
 
361
365
  # Remove conflicting routes
362
- for route in self.fastapi_app.routes:
366
+ for route in fastapi_app.routes:
363
367
  for conflict in conflicts:
364
368
  if isinstance(route, APIRoute):
365
369
  if route.path == conflict["path"] and list(route.methods) == list(conflict["methods"]): # type: ignore
366
- self.fastapi_app.routes.pop(self.fastapi_app.routes.index(route))
370
+ fastapi_app.routes.pop(fastapi_app.routes.index(route))
367
371
 
368
- self.fastapi_app.include_router(router)
372
+ fastapi_app.include_router(router)
369
373
 
370
- else:
371
- # Skip conflicting AgentOS routes, prefer user's existing routes
372
- for conflict in conflicts:
373
- methods_str = ", ".join(conflict["methods"]) # type: ignore
374
- logger.debug(
375
- f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
376
- f"Using existing custom route instead"
377
- )
374
+ elif self.on_route_conflict == "error":
375
+ conflicting_paths = [conflict["path"] for conflict in conflicts]
376
+ raise ValueError(f"Route conflict detected: {conflicting_paths}")
378
377
 
379
- # Create a new router without the conflicting routes
380
- filtered_router = APIRouter()
381
- for route in router.routes:
382
- if route not in conflicting_routes:
383
- filtered_router.routes.append(route)
384
-
385
- # Use the filtered router if it has any routes left
386
- if filtered_router.routes:
387
- self.fastapi_app.include_router(filtered_router)
388
378
  else:
389
379
  # No conflicts, add router normally
390
- self.fastapi_app.include_router(router)
380
+ fastapi_app.include_router(router)
391
381
 
392
382
  def _get_telemetry_data(self) -> Dict[str, Any]:
393
383
  """Get the telemetry data for the OS"""
@@ -398,57 +388,87 @@ class AgentOS:
398
388
  "interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
399
389
  }
400
390
 
401
- def _load_yaml_config(self, config_file_path: str) -> AgentOSConfig:
402
- """Load a YAML config file and return the configuration as an AgentOSConfig instance."""
403
- from pathlib import Path
404
-
405
- import yaml
406
-
407
- # Validate that the path points to a YAML file
408
- path = Path(config_file_path)
409
- if path.suffix.lower() not in [".yaml", ".yml"]:
410
- raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
411
-
412
- # Load the YAML file
413
- with open(config_file_path, "r") as f:
414
- return AgentOSConfig.model_validate(yaml.safe_load(f))
415
-
416
391
  def _auto_discover_databases(self) -> None:
417
392
  """Auto-discover the databases used by all contextual agents, teams and workflows."""
418
- dbs = {}
419
- knowledge_dbs = {} # Track databases specifically used for knowledge
393
+ from agno.db.base import BaseDb
394
+
395
+ dbs: Dict[str, BaseDb] = {}
396
+ knowledge_dbs: Dict[str, BaseDb] = {} # Track databases specifically used for knowledge
420
397
 
421
398
  for agent in self.agents or []:
422
399
  if agent.db:
423
- dbs[agent.db.id] = agent.db
400
+ self._register_db_with_validation(dbs, agent.db)
424
401
  if agent.knowledge and agent.knowledge.contents_db:
425
- knowledge_dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
402
+ self._register_db_with_validation(knowledge_dbs, agent.knowledge.contents_db)
426
403
  # Also add to general dbs if it's used for both purposes
427
404
  if agent.knowledge.contents_db.id not in dbs:
428
- dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
405
+ self._register_db_with_validation(dbs, agent.knowledge.contents_db)
429
406
 
430
407
  for team in self.teams or []:
431
408
  if team.db:
432
- dbs[team.db.id] = team.db
409
+ self._register_db_with_validation(dbs, team.db)
433
410
  if team.knowledge and team.knowledge.contents_db:
434
- knowledge_dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
411
+ self._register_db_with_validation(knowledge_dbs, team.knowledge.contents_db)
435
412
  # Also add to general dbs if it's used for both purposes
436
413
  if team.knowledge.contents_db.id not in dbs:
437
- dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
414
+ self._register_db_with_validation(dbs, team.knowledge.contents_db)
438
415
 
439
416
  for workflow in self.workflows or []:
440
417
  if workflow.db:
441
- dbs[workflow.db.id] = workflow.db
418
+ self._register_db_with_validation(dbs, workflow.db)
442
419
 
443
420
  for interface in self.interfaces or []:
444
421
  if interface.agent and interface.agent.db:
445
- dbs[interface.agent.db.id] = interface.agent.db
422
+ self._register_db_with_validation(dbs, interface.agent.db)
446
423
  elif interface.team and interface.team.db:
447
- dbs[interface.team.db.id] = interface.team.db
424
+ self._register_db_with_validation(dbs, interface.team.db)
448
425
 
449
426
  self.dbs = dbs
450
427
  self.knowledge_dbs = knowledge_dbs
451
428
 
429
+ def _register_db_with_validation(self, registered_dbs: Dict[str, Any], db: BaseDb) -> None:
430
+ """Register a database in the contextual OS after validating it is not conflicting with registered databases"""
431
+ if db.id in registered_dbs:
432
+ existing_db = registered_dbs[db.id]
433
+ if not self._are_db_instances_compatible(existing_db, db):
434
+ raise ValueError(
435
+ f"Database ID conflict detected: Two different database instances have the same ID '{db.id}'. "
436
+ f"Database instances with the same ID must point to the same database with identical configuration."
437
+ )
438
+ registered_dbs[db.id] = db
439
+
440
+ def _are_db_instances_compatible(self, db1: BaseDb, db2: BaseDb) -> bool:
441
+ """
442
+ Return True if the two given database objects are compatible
443
+ Two database objects are compatible if they point to the same database with identical configuration.
444
+ """
445
+ # If they're the same object reference, they're compatible
446
+ if db1 is db2:
447
+ return True
448
+
449
+ if type(db1) is not type(db2):
450
+ return False
451
+
452
+ if hasattr(db1, "db_url") and hasattr(db2, "db_url"):
453
+ if db1.db_url != db2.db_url: # type: ignore
454
+ return False
455
+
456
+ if hasattr(db1, "db_file") and hasattr(db2, "db_file"):
457
+ if db1.db_file != db2.db_file: # type: ignore
458
+ return False
459
+
460
+ # If table names are different, they're not compatible
461
+ if (
462
+ db1.session_table_name != db2.session_table_name
463
+ or db1.memory_table_name != db2.memory_table_name
464
+ or db1.metrics_table_name != db2.metrics_table_name
465
+ or db1.eval_table_name != db2.eval_table_name
466
+ or db1.knowledge_table_name != db2.knowledge_table_name
467
+ ):
468
+ return False
469
+
470
+ return True
471
+
452
472
  def _auto_discover_knowledge_instances(self) -> None:
453
473
  """Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
454
474
  knowledge_instances = []
@@ -594,11 +614,10 @@ class AgentOS:
594
614
  from rich.align import Align
595
615
  from rich.console import Console, Group
596
616
 
597
- panel_group = []
598
- panel_group.append(Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"))
599
- panel_group.append(
600
- Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}")
601
- )
617
+ panel_group = [
618
+ Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"),
619
+ Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}"),
620
+ ]
602
621
  if bool(self.settings.os_security_key):
603
622
  panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
604
623
 
@@ -123,6 +123,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
123
123
  response = await agent.arun(
124
124
  message_text,
125
125
  user_id=phone_number,
126
+ session_id=f"wa:{phone_number}",
126
127
  images=[Image(content=await get_media_async(message_image))] if message_image else None,
127
128
  files=[File(content=await get_media_async(message_doc))] if message_doc else None,
128
129
  videos=[Video(content=await get_media_async(message_video))] if message_video else None,
@@ -132,6 +133,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
132
133
  response = await team.arun( # type: ignore
133
134
  message_text,
134
135
  user_id=phone_number,
136
+ session_id=f"wa:{phone_number}",
135
137
  files=[File(content=await get_media_async(message_doc))] if message_doc else None,
136
138
  images=[Image(content=await get_media_async(message_image))] if message_image else None,
137
139
  videos=[Video(content=await get_media_async(message_video))] if message_video else None,
agno/os/mcp.py CHANGED
@@ -54,7 +54,7 @@ def get_mcp_server(
54
54
  ) # type: ignore
55
55
  async def config() -> ConfigResponse:
56
56
  return ConfigResponse(
57
- os_id=os.os_id or "AgentOS",
57
+ os_id=os.id or "AgentOS",
58
58
  description=os.description,
59
59
  available_models=os.config.available_models if os.config else [],
60
60
  databases=[db.id for db in os.dbs.values()],
@@ -0,0 +1,7 @@
1
+ from agno.os.middleware.jwt import (
2
+ JWTMiddleware,
3
+ )
4
+
5
+ __all__ = [
6
+ "JWTMiddleware",
7
+ ]