agno 2.2.8__py3-none-any.whl → 2.2.10__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 (50) hide show
  1. agno/agent/agent.py +37 -19
  2. agno/db/base.py +23 -0
  3. agno/db/dynamo/dynamo.py +20 -25
  4. agno/db/dynamo/schemas.py +1 -0
  5. agno/db/firestore/firestore.py +11 -0
  6. agno/db/gcs_json/gcs_json_db.py +4 -0
  7. agno/db/in_memory/in_memory_db.py +4 -0
  8. agno/db/json/json_db.py +4 -0
  9. agno/db/mongo/async_mongo.py +27 -0
  10. agno/db/mongo/mongo.py +25 -0
  11. agno/db/mysql/mysql.py +26 -1
  12. agno/db/postgres/async_postgres.py +26 -1
  13. agno/db/postgres/postgres.py +26 -1
  14. agno/db/redis/redis.py +4 -0
  15. agno/db/singlestore/singlestore.py +24 -0
  16. agno/db/sqlite/async_sqlite.py +25 -1
  17. agno/db/sqlite/sqlite.py +25 -1
  18. agno/db/surrealdb/surrealdb.py +13 -1
  19. agno/knowledge/reader/docx_reader.py +0 -1
  20. agno/models/azure/ai_foundry.py +2 -1
  21. agno/models/cerebras/cerebras.py +3 -2
  22. agno/models/openai/chat.py +2 -1
  23. agno/models/openai/responses.py +2 -1
  24. agno/os/app.py +127 -65
  25. agno/os/config.py +1 -0
  26. agno/os/interfaces/agui/router.py +9 -0
  27. agno/os/interfaces/agui/utils.py +49 -3
  28. agno/os/mcp.py +8 -8
  29. agno/os/router.py +27 -9
  30. agno/os/routers/evals/evals.py +12 -7
  31. agno/os/routers/memory/memory.py +18 -10
  32. agno/os/routers/metrics/metrics.py +6 -4
  33. agno/os/routers/session/session.py +21 -11
  34. agno/os/utils.py +57 -11
  35. agno/team/team.py +33 -23
  36. agno/vectordb/mongodb/__init__.py +7 -1
  37. agno/vectordb/redis/__init__.py +4 -0
  38. agno/workflow/agent.py +2 -2
  39. agno/workflow/condition.py +26 -4
  40. agno/workflow/loop.py +9 -0
  41. agno/workflow/parallel.py +39 -16
  42. agno/workflow/router.py +25 -4
  43. agno/workflow/step.py +162 -91
  44. agno/workflow/steps.py +9 -0
  45. agno/workflow/workflow.py +26 -22
  46. {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/METADATA +11 -13
  47. {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/RECORD +50 -50
  48. {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/WHEEL +0 -0
  49. {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/licenses/LICENSE +0 -0
  50. {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/top_level.txt +0 -0
agno/os/mcp.py CHANGED
@@ -57,7 +57,7 @@ def get_mcp_server(
57
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
- databases=[db.id for db in os.dbs.values()],
60
+ databases=[db.id for db_list in os.dbs.values() for db in db_list],
61
61
  chat=os.config.chat if os.config else None,
62
62
  session=os._get_session_config(),
63
63
  memory=os._get_memory_config(),
@@ -103,7 +103,7 @@ def get_mcp_server(
103
103
  sort_by: str = "created_at",
104
104
  sort_order: str = "desc",
105
105
  ):
106
- db = get_db(os.dbs, db_id)
106
+ db = await get_db(os.dbs, db_id)
107
107
  if isinstance(db, AsyncBaseDb):
108
108
  db = cast(AsyncBaseDb, db)
109
109
  sessions = await db.get_sessions(
@@ -136,7 +136,7 @@ def get_mcp_server(
136
136
  sort_by: str = "created_at",
137
137
  sort_order: str = "desc",
138
138
  ):
139
- db = get_db(os.dbs, db_id)
139
+ db = await get_db(os.dbs, db_id)
140
140
  if isinstance(db, AsyncBaseDb):
141
141
  db = cast(AsyncBaseDb, db)
142
142
  sessions = await db.get_sessions(
@@ -169,7 +169,7 @@ def get_mcp_server(
169
169
  sort_by: str = "created_at",
170
170
  sort_order: str = "desc",
171
171
  ):
172
- db = get_db(os.dbs, db_id)
172
+ db = await get_db(os.dbs, db_id)
173
173
  if isinstance(db, AsyncBaseDb):
174
174
  db = cast(AsyncBaseDb, db)
175
175
  sessions = await db.get_sessions(
@@ -202,7 +202,7 @@ def get_mcp_server(
202
202
  user_id: str,
203
203
  topics: Optional[List[str]] = None,
204
204
  ) -> UserMemorySchema:
205
- db = get_db(os.dbs, db_id)
205
+ db = await get_db(os.dbs, db_id)
206
206
  user_memory = db.upsert_user_memory(
207
207
  memory=UserMemory(
208
208
  memory_id=str(uuid4()),
@@ -224,7 +224,7 @@ def get_mcp_server(
224
224
  sort_order: str = "desc",
225
225
  db_id: Optional[str] = None,
226
226
  ):
227
- db = get_db(os.dbs, db_id)
227
+ db = await get_db(os.dbs, db_id)
228
228
  if isinstance(db, AsyncBaseDb):
229
229
  db = cast(AsyncBaseDb, db)
230
230
  user_memories = await db.get_user_memories(
@@ -251,7 +251,7 @@ def get_mcp_server(
251
251
  memory: str,
252
252
  user_id: str,
253
253
  ) -> UserMemorySchema:
254
- db = get_db(os.dbs, db_id)
254
+ db = await get_db(os.dbs, db_id)
255
255
  if isinstance(db, AsyncBaseDb):
256
256
  db = cast(AsyncBaseDb, db)
257
257
  user_memory = await db.upsert_user_memory(
@@ -281,7 +281,7 @@ def get_mcp_server(
281
281
  db_id: str,
282
282
  memory_id: str,
283
283
  ) -> None:
284
- db = get_db(os.dbs, db_id)
284
+ db = await get_db(os.dbs, db_id)
285
285
  if isinstance(db, AsyncBaseDb):
286
286
  db = cast(AsyncBaseDb, db)
287
287
  await db.delete_user_memory(memory_id=memory_id)
agno/os/router.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import json
2
- from itertools import chain
3
2
  from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, List, Optional, Union, cast
4
3
  from uuid import uuid4
5
4
 
@@ -73,33 +72,52 @@ async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict
73
72
  form_data = await request.form()
74
73
  sig = inspect.signature(endpoint_func)
75
74
  known_fields = set(sig.parameters.keys())
76
- kwargs = {key: value for key, value in form_data.items() if key not in known_fields}
75
+ kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
77
76
 
78
77
  # Handle JSON parameters. They are passed as strings and need to be deserialized.
79
78
  if session_state := kwargs.get("session_state"):
80
79
  try:
81
- session_state_dict = json.loads(session_state) # type: ignore
82
- kwargs["session_state"] = session_state_dict
80
+ if isinstance(session_state, str):
81
+ session_state_dict = json.loads(session_state) # type: ignore
82
+ kwargs["session_state"] = session_state_dict
83
83
  except json.JSONDecodeError:
84
84
  kwargs.pop("session_state")
85
85
  log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
86
86
 
87
87
  if dependencies := kwargs.get("dependencies"):
88
88
  try:
89
- dependencies_dict = json.loads(dependencies) # type: ignore
90
- kwargs["dependencies"] = dependencies_dict
89
+ if isinstance(dependencies, str):
90
+ dependencies_dict = json.loads(dependencies) # type: ignore
91
+ kwargs["dependencies"] = dependencies_dict
91
92
  except json.JSONDecodeError:
92
93
  kwargs.pop("dependencies")
93
94
  log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
94
95
 
95
96
  if metadata := kwargs.get("metadata"):
96
97
  try:
97
- metadata_dict = json.loads(metadata) # type: ignore
98
- kwargs["metadata"] = metadata_dict
98
+ if isinstance(metadata, str):
99
+ metadata_dict = json.loads(metadata) # type: ignore
100
+ kwargs["metadata"] = metadata_dict
99
101
  except json.JSONDecodeError:
100
102
  kwargs.pop("metadata")
101
103
  log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
102
104
 
105
+ if knowledge_filters := kwargs.get("knowledge_filters"):
106
+ try:
107
+ if isinstance(knowledge_filters, str):
108
+ knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
109
+ kwargs["knowledge_filters"] = knowledge_filters_dict
110
+ except json.JSONDecodeError:
111
+ kwargs.pop("knowledge_filters")
112
+ log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
113
+
114
+ # Parse boolean and null values
115
+ for key, value in kwargs.items():
116
+ if isinstance(value, str) and value.lower() in ["true", "false"]:
117
+ kwargs[key] = value.lower() == "true"
118
+ elif isinstance(value, str) and value.lower() in ["null", "none"]:
119
+ kwargs[key] = None
120
+
103
121
  return kwargs
104
122
 
105
123
 
@@ -652,7 +670,7 @@ def get_base_router(
652
670
  os_id=os.id or "Unnamed OS",
653
671
  description=os.description,
654
672
  available_models=os.config.available_models if os.config else [],
655
- databases=list({db.id for db in chain(os.dbs.values(), os.knowledge_dbs.values())}),
673
+ databases=list({db.id for db_id, dbs in os.dbs.items() for db in dbs}),
656
674
  chat=os.config.chat if os.config else None,
657
675
  session=os._get_session_config(),
658
676
  memory=os._get_memory_config(),
@@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
34
34
 
35
35
 
36
36
  def get_eval_router(
37
- dbs: dict[str, Union[BaseDb, AsyncBaseDb]],
37
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]],
38
38
  agents: Optional[List[Agent]] = None,
39
39
  teams: Optional[List[Team]] = None,
40
40
  settings: AgnoAPISettings = AgnoAPISettings(),
@@ -56,7 +56,7 @@ def get_eval_router(
56
56
 
57
57
  def attach_routes(
58
58
  router: APIRouter,
59
- dbs: dict[str, Union[BaseDb, AsyncBaseDb]],
59
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]],
60
60
  agents: Optional[List[Agent]] = None,
61
61
  teams: Optional[List[Team]] = None,
62
62
  ) -> APIRouter:
@@ -115,8 +115,9 @@ def attach_routes(
115
115
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort by"),
116
116
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
117
117
  db_id: Optional[str] = Query(default=None, description="The ID of the database to use"),
118
+ table: Optional[str] = Query(default=None, description="The database table to use"),
118
119
  ) -> PaginatedResponse[EvalSchema]:
119
- db = get_db(dbs, db_id)
120
+ db = await get_db(dbs, db_id, table)
120
121
 
121
122
  if isinstance(db, AsyncBaseDb):
122
123
  db = cast(AsyncBaseDb, db)
@@ -198,8 +199,9 @@ def attach_routes(
198
199
  async def get_eval_run(
199
200
  eval_run_id: str,
200
201
  db_id: Optional[str] = Query(default=None, description="The ID of the database to use"),
202
+ table: Optional[str] = Query(default=None, description="Table to query eval run from"),
201
203
  ) -> EvalSchema:
202
- db = get_db(dbs, db_id)
204
+ db = await get_db(dbs, db_id, table)
203
205
  if isinstance(db, AsyncBaseDb):
204
206
  db = cast(AsyncBaseDb, db)
205
207
  eval_run = await db.get_eval_run(eval_run_id=eval_run_id, deserialize=False)
@@ -224,9 +226,10 @@ def attach_routes(
224
226
  async def delete_eval_runs(
225
227
  request: DeleteEvalRunsRequest,
226
228
  db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
229
+ table: Optional[str] = Query(default=None, description="Table to use for deletion"),
227
230
  ) -> None:
228
231
  try:
229
- db = get_db(dbs, db_id)
232
+ db = await get_db(dbs, db_id, table)
230
233
  if isinstance(db, AsyncBaseDb):
231
234
  db = cast(AsyncBaseDb, db)
232
235
  await db.delete_eval_runs(eval_run_ids=request.eval_run_ids)
@@ -277,9 +280,10 @@ def attach_routes(
277
280
  eval_run_id: str,
278
281
  request: UpdateEvalRunRequest,
279
282
  db_id: Optional[str] = Query(default=None, description="The ID of the database to use"),
283
+ table: Optional[str] = Query(default=None, description="Table to use for rename operation"),
280
284
  ) -> EvalSchema:
281
285
  try:
282
- db = get_db(dbs, db_id)
286
+ db = await get_db(dbs, db_id, table)
283
287
  if isinstance(db, AsyncBaseDb):
284
288
  db = cast(AsyncBaseDb, db)
285
289
  eval_run = await db.rename_eval_run(eval_run_id=eval_run_id, name=request.name, deserialize=False)
@@ -336,8 +340,9 @@ def attach_routes(
336
340
  async def run_eval(
337
341
  eval_run_input: EvalRunInput,
338
342
  db_id: Optional[str] = Query(default=None, description="Database ID to use for evaluation"),
343
+ table: Optional[str] = Query(default=None, description="Table to use for evaluation"),
339
344
  ) -> Optional[EvalSchema]:
340
- db = get_db(dbs, db_id)
345
+ db = await get_db(dbs, db_id, table)
341
346
 
342
347
  if eval_run_input.agent_id and eval_run_input.team_id:
343
348
  raise HTTPException(status_code=400, detail="Only one of agent_id or team_id must be provided")
@@ -32,7 +32,7 @@ logger = logging.getLogger(__name__)
32
32
 
33
33
 
34
34
  def get_memory_router(
35
- dbs: dict[str, Union[BaseDb, AsyncBaseDb]], settings: AgnoAPISettings = AgnoAPISettings(), **kwargs
35
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], settings: AgnoAPISettings = AgnoAPISettings(), **kwargs
36
36
  ) -> APIRouter:
37
37
  """Create memory router with comprehensive OpenAPI documentation for user memory management endpoints."""
38
38
  router = APIRouter(
@@ -49,7 +49,7 @@ def get_memory_router(
49
49
  return attach_routes(router=router, dbs=dbs)
50
50
 
51
51
 
52
- def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]]) -> APIRouter:
52
+ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]]) -> APIRouter:
53
53
  @router.post(
54
54
  "/memories",
55
55
  response_model=UserMemorySchema,
@@ -85,6 +85,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
85
85
  request: Request,
86
86
  payload: UserMemoryCreateSchema,
87
87
  db_id: Optional[str] = Query(default=None, description="Database ID to use for memory storage"),
88
+ table: Optional[str] = Query(default=None, description="Table to use for memory storage"),
88
89
  ) -> UserMemorySchema:
89
90
  if hasattr(request.state, "user_id"):
90
91
  user_id = request.state.user_id
@@ -93,7 +94,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
93
94
  if payload.user_id is None:
94
95
  raise HTTPException(status_code=400, detail="User ID is required")
95
96
 
96
- db = get_db(dbs, db_id)
97
+ db = await get_db(dbs, db_id, table)
97
98
 
98
99
  if isinstance(db, AsyncBaseDb):
99
100
  db = cast(AsyncBaseDb, db)
@@ -138,8 +139,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
138
139
  memory_id: str = Path(description="Memory ID to delete"),
139
140
  user_id: Optional[str] = Query(default=None, description="User ID to delete memory for"),
140
141
  db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
142
+ table: Optional[str] = Query(default=None, description="Table to use for deletion"),
141
143
  ) -> None:
142
- db = get_db(dbs, db_id)
144
+ db = await get_db(dbs, db_id, table)
143
145
  if isinstance(db, AsyncBaseDb):
144
146
  db = cast(AsyncBaseDb, db)
145
147
  await db.delete_user_memory(memory_id=memory_id, user_id=user_id)
@@ -164,8 +166,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
164
166
  async def delete_memories(
165
167
  request: DeleteMemoriesRequest,
166
168
  db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
169
+ table: Optional[str] = Query(default=None, description="Table to use for deletion"),
167
170
  ) -> None:
168
- db = get_db(dbs, db_id)
171
+ db = await get_db(dbs, db_id, table)
169
172
  if isinstance(db, AsyncBaseDb):
170
173
  db = cast(AsyncBaseDb, db)
171
174
  await db.delete_user_memories(memory_ids=request.memory_ids, user_id=request.user_id)
@@ -217,8 +220,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
217
220
  sort_by: Optional[str] = Query(default="updated_at", description="Field to sort memories by"),
218
221
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
219
222
  db_id: Optional[str] = Query(default=None, description="Database ID to query memories from"),
223
+ table: Optional[str] = Query(default=None, description="The database table to use"),
220
224
  ) -> PaginatedResponse[UserMemorySchema]:
221
- db = get_db(dbs, db_id)
225
+ db = await get_db(dbs, db_id, table)
222
226
 
223
227
  if hasattr(request.state, "user_id"):
224
228
  user_id = request.state.user_id
@@ -294,8 +298,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
294
298
  memory_id: str = Path(description="Memory ID to retrieve"),
295
299
  user_id: Optional[str] = Query(default=None, description="User ID to query memory for"),
296
300
  db_id: Optional[str] = Query(default=None, description="Database ID to query memory from"),
301
+ table: Optional[str] = Query(default=None, description="Table to query memory from"),
297
302
  ) -> UserMemorySchema:
298
- db = get_db(dbs, db_id)
303
+ db = await get_db(dbs, db_id, table)
299
304
 
300
305
  if hasattr(request.state, "user_id"):
301
306
  user_id = request.state.user_id
@@ -343,8 +348,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
343
348
  )
344
349
  async def get_topics(
345
350
  db_id: Optional[str] = Query(default=None, description="Database ID to query topics from"),
351
+ table: Optional[str] = Query(default=None, description="Table to query topics from"),
346
352
  ) -> List[str]:
347
- db = get_db(dbs, db_id)
353
+ db = await get_db(dbs, db_id, table)
348
354
  if isinstance(db, AsyncBaseDb):
349
355
  db = cast(AsyncBaseDb, db)
350
356
  return await db.get_all_memory_topics()
@@ -389,6 +395,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
389
395
  payload: UserMemoryCreateSchema,
390
396
  memory_id: str = Path(description="Memory ID to update"),
391
397
  db_id: Optional[str] = Query(default=None, description="Database ID to use for update"),
398
+ table: Optional[str] = Query(default=None, description="Table to use for update"),
392
399
  ) -> UserMemorySchema:
393
400
  if hasattr(request.state, "user_id"):
394
401
  user_id = request.state.user_id
@@ -397,7 +404,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
397
404
  if payload.user_id is None:
398
405
  raise HTTPException(status_code=400, detail="User ID is required")
399
406
 
400
- db = get_db(dbs, db_id)
407
+ db = await get_db(dbs, db_id, table)
401
408
 
402
409
  if isinstance(db, AsyncBaseDb):
403
410
  db = cast(AsyncBaseDb, db)
@@ -459,8 +466,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
459
466
  limit: Optional[int] = Query(default=20, description="Number of user statistics to return per page"),
460
467
  page: Optional[int] = Query(default=1, description="Page number for pagination"),
461
468
  db_id: Optional[str] = Query(default=None, description="Database ID to query statistics from"),
469
+ table: Optional[str] = Query(default=None, description="Table to query statistics from"),
462
470
  ) -> PaginatedResponse[UserStatsSchema]:
463
- db = get_db(dbs, db_id)
471
+ db = await get_db(dbs, db_id, table)
464
472
  try:
465
473
  if isinstance(db, AsyncBaseDb):
466
474
  db = cast(AsyncBaseDb, db)
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
24
  def get_metrics_router(
25
- dbs: dict[str, Union[BaseDb, AsyncBaseDb]], settings: AgnoAPISettings = AgnoAPISettings(), **kwargs
25
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], settings: AgnoAPISettings = AgnoAPISettings(), **kwargs
26
26
  ) -> APIRouter:
27
27
  """Create metrics router with comprehensive OpenAPI documentation for system metrics and analytics endpoints."""
28
28
  router = APIRouter(
@@ -39,7 +39,7 @@ def get_metrics_router(
39
39
  return attach_routes(router=router, dbs=dbs)
40
40
 
41
41
 
42
- def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]]) -> APIRouter:
42
+ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]]) -> APIRouter:
43
43
  @router.get(
44
44
  "/metrics",
45
45
  response_model=MetricsResponse,
@@ -99,9 +99,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
99
99
  default=None, description="Ending date for metrics range (YYYY-MM-DD format)"
100
100
  ),
101
101
  db_id: Optional[str] = Query(default=None, description="Database ID to query metrics from"),
102
+ table: Optional[str] = Query(default=None, description="The database table to use"),
102
103
  ) -> MetricsResponse:
103
104
  try:
104
- db = get_db(dbs, db_id)
105
+ db = await get_db(dbs, db_id, table)
105
106
  if isinstance(db, AsyncBaseDb):
106
107
  db = cast(AsyncBaseDb, db)
107
108
  metrics, latest_updated_at = await db.get_metrics(starting_date=starting_date, ending_date=ending_date)
@@ -169,9 +170,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
169
170
  )
170
171
  async def calculate_metrics(
171
172
  db_id: Optional[str] = Query(default=None, description="Database ID to use for metrics calculation"),
173
+ table: Optional[str] = Query(default=None, description="Table to use for metrics calculation"),
172
174
  ) -> List[DayAggregatedMetrics]:
173
175
  try:
174
- db = get_db(dbs, db_id)
176
+ db = await get_db(dbs, db_id, table)
175
177
  if isinstance(db, AsyncBaseDb):
176
178
  db = cast(AsyncBaseDb, db)
177
179
  result = await db.calculate_metrics()
@@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
35
35
 
36
36
 
37
37
  def get_session_router(
38
- dbs: dict[str, Union[BaseDb, AsyncBaseDb]], settings: AgnoAPISettings = AgnoAPISettings()
38
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], settings: AgnoAPISettings = AgnoAPISettings()
39
39
  ) -> APIRouter:
40
40
  """Create session router with comprehensive OpenAPI documentation for session management endpoints."""
41
41
  session_router = APIRouter(
@@ -52,7 +52,7 @@ def get_session_router(
52
52
  return attach_routes(router=session_router, dbs=dbs)
53
53
 
54
54
 
55
- def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]]) -> APIRouter:
55
+ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]]) -> APIRouter:
56
56
  @router.get(
57
57
  "/sessions",
58
58
  response_model=PaginatedResponse[SessionSchema],
@@ -89,6 +89,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
89
89
  },
90
90
  },
91
91
  400: {"description": "Invalid session type or filter parameters", "model": BadRequestResponse},
92
+ 404: {"description": "Not found", "model": NotFoundResponse},
92
93
  422: {"description": "Validation error in query parameters", "model": ValidationErrorResponse},
93
94
  },
94
95
  )
@@ -109,8 +110,12 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
109
110
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort sessions by"),
110
111
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
111
112
  db_id: Optional[str] = Query(default=None, description="Database ID to query sessions from"),
113
+ table: Optional[str] = Query(default=None, description="The database table to use"),
112
114
  ) -> PaginatedResponse[SessionSchema]:
113
- db = get_db(dbs, db_id)
115
+ try:
116
+ db = await get_db(dbs, db_id, table)
117
+ except Exception as e:
118
+ raise HTTPException(status_code=404, detail=f"{e}")
114
119
 
115
120
  if hasattr(request.state, "user_id"):
116
121
  user_id = request.state.user_id
@@ -202,7 +207,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
202
207
  ),
203
208
  db_id: Optional[str] = Query(default=None, description="Database ID to create session in"),
204
209
  ) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
205
- db = get_db(dbs, db_id)
210
+ db = await get_db(dbs, db_id)
206
211
 
207
212
  # Get user_id from request state if available (from auth middleware)
208
213
  user_id = create_session_request.user_id
@@ -373,8 +378,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
373
378
  ),
374
379
  user_id: Optional[str] = Query(default=None, description="User ID to query session from"),
375
380
  db_id: Optional[str] = Query(default=None, description="Database ID to query session from"),
381
+ table: Optional[str] = Query(default=None, description="Table to query session from"),
376
382
  ) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
377
- db = get_db(dbs, db_id)
383
+ db = await get_db(dbs, db_id, table)
378
384
 
379
385
  if hasattr(request.state, "user_id"):
380
386
  user_id = request.state.user_id
@@ -528,8 +534,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
528
534
  description="Filter runs created before this Unix timestamp (epoch time in seconds)",
529
535
  ),
530
536
  db_id: Optional[str] = Query(default=None, description="Database ID to query runs from"),
537
+ table: Optional[str] = Query(default=None, description="Table to query runs from"),
531
538
  ) -> List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]]:
532
- db = get_db(dbs, db_id)
539
+ db = await get_db(dbs, db_id, table)
533
540
 
534
541
  if hasattr(request.state, "user_id"):
535
542
  user_id = request.state.user_id
@@ -644,7 +651,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
644
651
  user_id: Optional[str] = Query(default=None, description="User ID to query run from"),
645
652
  db_id: Optional[str] = Query(default=None, description="Database ID to query run from"),
646
653
  ) -> Union[RunSchema, TeamRunSchema, WorkflowRunSchema]:
647
- db = get_db(dbs, db_id)
654
+ db = await get_db(dbs, db_id)
648
655
 
649
656
  if hasattr(request.state, "user_id"):
650
657
  user_id = request.state.user_id
@@ -702,8 +709,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
702
709
  async def delete_session(
703
710
  session_id: str = Path(description="Session ID to delete"),
704
711
  db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
712
+ table: Optional[str] = Query(default=None, description="Table to use for deletion"),
705
713
  ) -> None:
706
- db = get_db(dbs, db_id)
714
+ db = await get_db(dbs, db_id, table)
707
715
  if isinstance(db, AsyncBaseDb):
708
716
  db = cast(AsyncBaseDb, db)
709
717
  await db.delete_session(session_id=session_id)
@@ -734,11 +742,12 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
734
742
  default=SessionType.AGENT, description="Default session type filter", alias="type"
735
743
  ),
736
744
  db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
745
+ table: Optional[str] = Query(default=None, description="Table to use for deletion"),
737
746
  ) -> None:
738
747
  if len(request.session_ids) != len(request.session_types):
739
748
  raise HTTPException(status_code=400, detail="Session IDs and session types must have the same length")
740
749
 
741
- db = get_db(dbs, db_id)
750
+ db = await get_db(dbs, db_id, table)
742
751
  if isinstance(db, AsyncBaseDb):
743
752
  db = cast(AsyncBaseDb, db)
744
753
  await db.delete_sessions(session_ids=request.session_ids)
@@ -840,8 +849,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
840
849
  ),
841
850
  session_name: str = Body(embed=True, description="New name for the session"),
842
851
  db_id: Optional[str] = Query(default=None, description="Database ID to use for rename operation"),
852
+ table: Optional[str] = Query(default=None, description="Table to use for rename operation"),
843
853
  ) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
844
- db = get_db(dbs, db_id)
854
+ db = await get_db(dbs, db_id, table)
845
855
  if isinstance(db, AsyncBaseDb):
846
856
  db = cast(AsyncBaseDb, db)
847
857
  session = await db.rename_session(
@@ -926,7 +936,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
926
936
  user_id: Optional[str] = Query(default=None, description="User ID"),
927
937
  db_id: Optional[str] = Query(default=None, description="Database ID to use for update operation"),
928
938
  ) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
929
- db = get_db(dbs, db_id)
939
+ db = await get_db(dbs, db_id)
930
940
 
931
941
  if hasattr(request.state, "user_id"):
932
942
  user_id = request.state.user_id
agno/os/utils.py CHANGED
@@ -19,23 +19,69 @@ from agno.utils.log import logger
19
19
  from agno.workflow.workflow import Workflow
20
20
 
21
21
 
22
- def get_db(dbs: dict[str, Union[BaseDb, AsyncBaseDb]], db_id: Optional[str] = None) -> Union[BaseDb, AsyncBaseDb]:
23
- """Return the database with the given ID, or the first database if no ID is provided."""
22
+ async def get_db(
23
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
24
+ ) -> Union[BaseDb, AsyncBaseDb]:
25
+ """Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
26
+
27
+ if table and not db_id:
28
+ raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
29
+
30
+ async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
31
+ """Check if this database has the specified table (configured and actually exists)."""
32
+ # First check if table name is configured
33
+ is_configured = (
34
+ hasattr(db, "session_table_name")
35
+ and db.session_table_name == table_name
36
+ or hasattr(db, "memory_table_name")
37
+ and db.memory_table_name == table_name
38
+ or hasattr(db, "metrics_table_name")
39
+ and db.metrics_table_name == table_name
40
+ or hasattr(db, "eval_table_name")
41
+ and db.eval_table_name == table_name
42
+ or hasattr(db, "knowledge_table_name")
43
+ and db.knowledge_table_name == table_name
44
+ )
45
+
46
+ if not is_configured:
47
+ return False
48
+
49
+ # Then check if table actually exists in the database
50
+ try:
51
+ if isinstance(db, AsyncBaseDb):
52
+ # For async databases, await the check
53
+ return await db.table_exists(table_name)
54
+ else:
55
+ # For sync databases, call directly
56
+ return db.table_exists(table_name)
57
+ except (NotImplementedError, AttributeError):
58
+ # If table_exists not implemented, fall back to configuration check
59
+ return is_configured
60
+
61
+ # If db_id is provided, first find the database with that ID
62
+ if db_id:
63
+ target_db_list = dbs.get(db_id)
64
+ if not target_db_list:
65
+ raise HTTPException(status_code=404, detail=f"No database found with id '{db_id}'")
66
+
67
+ # If table is also specified, search through all databases with this ID to find one with the table
68
+ if table:
69
+ for db in target_db_list:
70
+ if await _has_table(db, table):
71
+ return db
72
+ raise HTTPException(status_code=404, detail=f"No database with id '{db_id}' has table '{table}'")
73
+
74
+ # If no table specified, return the first database with this ID
75
+ return target_db_list[0]
24
76
 
25
77
  # Raise if multiple databases are provided but no db_id is provided
26
- if not db_id and len(dbs) > 1:
78
+ if len(dbs) > 1:
27
79
  raise HTTPException(
28
80
  status_code=400, detail="The db_id query parameter is required when using multiple databases"
29
81
  )
30
82
 
31
- # Get and return the database with the given ID, or raise if not found
32
- if db_id:
33
- db = dbs.get(db_id)
34
- if not db:
35
- raise HTTPException(status_code=404, detail=f"Database with id '{db_id}' not found")
36
- else:
37
- db = next(iter(dbs.values()))
38
- return db
83
+ # Return the first (and only) database
84
+ return next(db for dbs in dbs.values() for db in dbs)
39
85
 
40
86
 
41
87
  def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge: