agno 2.2.8__py3-none-any.whl → 2.2.9__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 (42) hide show
  1. agno/agent/agent.py +23 -15
  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 +112 -50
  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 +26 -21
  36. agno/vectordb/mongodb/__init__.py +7 -1
  37. agno/vectordb/redis/__init__.py +4 -0
  38. {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/METADATA +11 -13
  39. {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/RECORD +42 -42
  40. {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/WHEEL +0 -0
  41. {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/licenses/LICENSE +0 -0
  42. {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/top_level.txt +0 -0
@@ -116,12 +116,24 @@ class SurrealDb(BaseDb):
116
116
  "workflows": self._workflows_table_name,
117
117
  }
118
118
 
119
- def _table_exists(self, table_name: str) -> bool:
119
+ def table_exists(self, table_name: str) -> bool:
120
+ """Check if a table with the given name exists in the SurrealDB database.
121
+
122
+ Args:
123
+ table_name: Name of the table to check
124
+
125
+ Returns:
126
+ bool: True if the table exists in the database, False otherwise
127
+ """
120
128
  response = self._query_one("INFO FOR DB", {}, dict)
121
129
  if response is None:
122
130
  raise Exception("Failed to retrieve database information")
123
131
  return table_name in response.get("tables", [])
124
132
 
133
+ def _table_exists(self, table_name: str) -> bool:
134
+ """Deprecated: Use table_exists() instead."""
135
+ return self.table_exists(table_name)
136
+
125
137
  def _create_table(self, table_type: TableType, table_name: str):
126
138
  query = get_schema(table_type, table_name)
127
139
  self.client.query(query)
@@ -62,7 +62,6 @@ class DocxReader(Reader):
62
62
  content=doc_content,
63
63
  )
64
64
  ]
65
-
66
65
  if self.chunk:
67
66
  chunked_documents = []
68
67
  for document in documents:
@@ -60,6 +60,7 @@ class AzureAIFoundry(Model):
60
60
  stop: Optional[Union[str, List[str]]] = None
61
61
  seed: Optional[int] = None
62
62
  model_extras: Optional[Dict[str, Any]] = None
63
+ strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
63
64
  request_params: Optional[Dict[str, Any]] = None
64
65
  # Client parameters
65
66
  api_key: Optional[str] = None
@@ -116,7 +117,7 @@ class AzureAIFoundry(Model):
116
117
  name=response_format.__name__,
117
118
  schema=response_format.model_json_schema(), # type: ignore
118
119
  description=response_format.__doc__,
119
- strict=True,
120
+ strict=self.strict_output,
120
121
  ),
121
122
  )
122
123
 
@@ -51,6 +51,7 @@ class Cerebras(Model):
51
51
  temperature: Optional[float] = None
52
52
  top_p: Optional[float] = None
53
53
  top_k: Optional[int] = None
54
+ strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
54
55
  extra_headers: Optional[Any] = None
55
56
  extra_query: Optional[Any] = None
56
57
  extra_body: Optional[Any] = None
@@ -191,10 +192,10 @@ class Cerebras(Model):
191
192
  and response_format.get("type") == "json_schema"
192
193
  and isinstance(response_format.get("json_schema"), dict)
193
194
  ):
194
- # Ensure json_schema has strict=True as required by Cerebras API
195
+ # Ensure json_schema has strict parameter set
195
196
  schema = response_format["json_schema"]
196
197
  if isinstance(schema.get("schema"), dict) and "strict" not in schema:
197
- schema["strict"] = True
198
+ schema["strict"] = self.strict_output
198
199
 
199
200
  request_params["response_format"] = response_format
200
201
 
@@ -65,6 +65,7 @@ class OpenAIChat(Model):
65
65
  user: Optional[str] = None
66
66
  top_p: Optional[float] = None
67
67
  service_tier: Optional[str] = None # "auto" | "default" | "flex" | "priority", defaults to "auto" when not set
68
+ strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
68
69
  extra_headers: Optional[Any] = None
69
70
  extra_query: Optional[Any] = None
70
71
  extra_body: Optional[Any] = None
@@ -215,7 +216,7 @@ class OpenAIChat(Model):
215
216
  "json_schema": {
216
217
  "name": response_format.__name__,
217
218
  "schema": schema,
218
- "strict": True,
219
+ "strict": self.strict_output,
219
220
  },
220
221
  }
221
222
  else:
@@ -53,6 +53,7 @@ class OpenAIResponses(Model):
53
53
  truncation: Optional[Literal["auto", "disabled"]] = None
54
54
  user: Optional[str] = None
55
55
  service_tier: Optional[Literal["auto", "default", "flex", "priority"]] = None
56
+ strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
56
57
  extra_headers: Optional[Any] = None
57
58
  extra_query: Optional[Any] = None
58
59
  extra_body: Optional[Any] = None
@@ -229,7 +230,7 @@ class OpenAIResponses(Model):
229
230
  "type": "json_schema",
230
231
  "name": response_format.__name__,
231
232
  "schema": schema,
232
- "strict": True,
233
+ "strict": self.strict_output,
233
234
  }
234
235
  else:
235
236
  # JSON mode
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, Literal, Optional, Union
4
+ from typing import Any, Dict, List, Literal, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
7
  from fastapi import APIRouter, FastAPI, HTTPException
@@ -109,6 +109,7 @@ class AgentOS:
109
109
  base_app: Optional[FastAPI] = None,
110
110
  on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
111
111
  telemetry: bool = True,
112
+ auto_provision_dbs: bool = True,
112
113
  os_id: Optional[str] = None, # Deprecated
113
114
  enable_mcp: bool = False, # Deprecated
114
115
  fastapi_app: Optional[FastAPI] = None, # Deprecated
@@ -148,7 +149,7 @@ class AgentOS:
148
149
  self.a2a_interface = a2a_interface
149
150
  self.knowledge = knowledge
150
151
  self.settings: AgnoAPISettings = settings or AgnoAPISettings()
151
-
152
+ self.auto_provision_dbs = auto_provision_dbs
152
153
  self._app_set = False
153
154
 
154
155
  if base_app:
@@ -552,11 +553,12 @@ class AgentOS:
552
553
  }
553
554
 
554
555
  def _auto_discover_databases(self) -> None:
555
- """Auto-discover the databases used by all contextual agents, teams and workflows."""
556
- from agno.db.base import AsyncBaseDb, BaseDb
556
+ """Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
557
557
 
558
- dbs: Dict[str, Union[BaseDb, AsyncBaseDb]] = {}
559
- knowledge_dbs: Dict[str, Union[BaseDb, AsyncBaseDb]] = {} # Track databases specifically used for knowledge
558
+ dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
559
+ knowledge_dbs: Dict[
560
+ str, List[Union[BaseDb, AsyncBaseDb]]
561
+ ] = {} # Track databases specifically used for knowledge
560
562
 
561
563
  for agent in self.agents or []:
562
564
  if agent.db:
@@ -587,48 +589,97 @@ class AgentOS:
587
589
  self.dbs = dbs
588
590
  self.knowledge_dbs = knowledge_dbs
589
591
 
590
- def _register_db_with_validation(self, registered_dbs: Dict[str, Any], db: Union[BaseDb, AsyncBaseDb]) -> None:
591
- """Register a database in the contextual OS after validating it is not conflicting with registered databases"""
592
- if db.id in registered_dbs:
593
- existing_db = registered_dbs[db.id]
594
- if not self._are_db_instances_compatible(existing_db, db):
595
- raise ValueError(
596
- f"Database ID conflict detected: Two different database instances have the same ID '{db.id}'. "
597
- f"Database instances with the same ID must point to the same database with identical configuration."
592
+ # Initialize/scaffold all discovered databases
593
+ if self.auto_provision_dbs:
594
+ import asyncio
595
+ import concurrent.futures
596
+
597
+ try:
598
+ # If we're already in an event loop, run in a separate thread
599
+ asyncio.get_running_loop()
600
+
601
+ def run_in_new_loop():
602
+ new_loop = asyncio.new_event_loop()
603
+ asyncio.set_event_loop(new_loop)
604
+ try:
605
+ return new_loop.run_until_complete(self._initialize_databases())
606
+ finally:
607
+ new_loop.close()
608
+
609
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
610
+ future = executor.submit(run_in_new_loop)
611
+ future.result() # Wait for completion
612
+
613
+ except RuntimeError:
614
+ # No event loop running, use asyncio.run
615
+ asyncio.run(self._initialize_databases())
616
+
617
+ async def _initialize_databases(self) -> None:
618
+ """Initialize all discovered databases and create all Agno tables that don't exist yet."""
619
+ from itertools import chain
620
+
621
+ # Collect all database instances and remove duplicates by identity
622
+ unique_dbs = list(
623
+ {
624
+ id(db): db
625
+ for db in chain(
626
+ chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
598
627
  )
599
- registered_dbs[db.id] = db
600
-
601
- def _are_db_instances_compatible(self, db1: Union[BaseDb, AsyncBaseDb], db2: Union[BaseDb, AsyncBaseDb]) -> bool:
602
- """
603
- Return True if the two given database objects are compatible
604
- Two database objects are compatible if they point to the same database with identical configuration.
605
- """
606
- # If they're the same object reference, they're compatible
607
- if db1 is db2:
608
- return True
609
-
610
- if type(db1) is not type(db2):
611
- return False
612
-
613
- if hasattr(db1, "db_url") and hasattr(db2, "db_url"):
614
- if db1.db_url != db2.db_url: # type: ignore
615
- return False
616
-
617
- if hasattr(db1, "db_file") and hasattr(db2, "db_file"):
618
- if db1.db_file != db2.db_file: # type: ignore
619
- return False
628
+ }.values()
629
+ )
620
630
 
621
- # If table names are different, they're not compatible
622
- if (
623
- db1.session_table_name != db2.session_table_name
624
- or db1.memory_table_name != db2.memory_table_name
625
- or db1.metrics_table_name != db2.metrics_table_name
626
- or db1.eval_table_name != db2.eval_table_name
627
- or db1.knowledge_table_name != db2.knowledge_table_name
628
- ):
629
- return False
631
+ # Separate sync and async databases
632
+ sync_dbs: List[Tuple[str, BaseDb]] = []
633
+ async_dbs: List[Tuple[str, AsyncBaseDb]] = []
634
+
635
+ for db in unique_dbs:
636
+ target = async_dbs if isinstance(db, AsyncBaseDb) else sync_dbs
637
+ target.append((db.id, db)) # type: ignore
638
+
639
+ # Initialize sync databases
640
+ for db_id, db in sync_dbs:
641
+ try:
642
+ if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
643
+ db._create_all_tables()
644
+ else:
645
+ log_debug(f"No table initialization needed for {db.__class__.__name__}")
646
+
647
+ except Exception as e:
648
+ log_warning(f"Failed to initialize {db.__class__.__name__} (id: {db_id}): {e}")
649
+
650
+ # Initialize async databases
651
+ for db_id, db in async_dbs:
652
+ try:
653
+ log_debug(f"Initializing async {db.__class__.__name__} (id: {db_id})")
654
+
655
+ if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
656
+ await db._create_all_tables()
657
+ else:
658
+ log_debug(f"No table initialization needed for async {db.__class__.__name__}")
659
+
660
+ except Exception as e:
661
+ log_warning(f"Failed to initialize async database {db.__class__.__name__} (id: {db_id}): {e}")
662
+
663
+ def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
664
+ """Get the table names for a database"""
665
+ table_names = {
666
+ "session_table_name": db.session_table_name,
667
+ "culture_table_name": db.culture_table_name,
668
+ "memory_table_name": db.memory_table_name,
669
+ "metrics_table_name": db.metrics_table_name,
670
+ "evals_table_name": db.eval_table_name,
671
+ "knowledge_table_name": db.knowledge_table_name,
672
+ }
673
+ return {k: v for k, v in table_names.items() if v is not None}
630
674
 
631
- return True
675
+ def _register_db_with_validation(
676
+ self, registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]], db: Union[BaseDb, AsyncBaseDb]
677
+ ) -> None:
678
+ """Register a database in the contextual OS after validating it is not conflicting with registered databases"""
679
+ if db.id in registered_dbs:
680
+ registered_dbs[db.id].append(db)
681
+ else:
682
+ registered_dbs[db.id] = [db]
632
683
 
633
684
  def _auto_discover_knowledge_instances(self) -> None:
634
685
  """Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
@@ -665,13 +716,15 @@ class AgentOS:
665
716
  session_config.dbs = []
666
717
 
667
718
  dbs_with_specific_config = [db.db_id for db in session_config.dbs]
668
-
669
- for db_id in self.dbs.keys():
719
+ for db_id, dbs in self.dbs.items():
670
720
  if db_id not in dbs_with_specific_config:
721
+ # Collect unique table names from all databases with the same id
722
+ unique_tables = list(set(db.session_table_name for db in dbs))
671
723
  session_config.dbs.append(
672
724
  DatabaseConfig(
673
725
  db_id=db_id,
674
726
  domain_config=SessionDomainConfig(display_name=db_id),
727
+ tables=unique_tables,
675
728
  )
676
729
  )
677
730
 
@@ -685,12 +738,15 @@ class AgentOS:
685
738
 
686
739
  dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
687
740
 
688
- for db_id in self.dbs.keys():
741
+ for db_id, dbs in self.dbs.items():
689
742
  if db_id not in dbs_with_specific_config:
743
+ # Collect unique table names from all databases with the same id
744
+ unique_tables = list(set(db.memory_table_name for db in dbs))
690
745
  memory_config.dbs.append(
691
746
  DatabaseConfig(
692
747
  db_id=db_id,
693
748
  domain_config=MemoryDomainConfig(display_name=db_id),
749
+ tables=unique_tables,
694
750
  )
695
751
  )
696
752
 
@@ -724,12 +780,15 @@ class AgentOS:
724
780
 
725
781
  dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
726
782
 
727
- for db_id in self.dbs.keys():
783
+ for db_id, dbs in self.dbs.items():
728
784
  if db_id not in dbs_with_specific_config:
785
+ # Collect unique table names from all databases with the same id
786
+ unique_tables = list(set(db.metrics_table_name for db in dbs))
729
787
  metrics_config.dbs.append(
730
788
  DatabaseConfig(
731
789
  db_id=db_id,
732
790
  domain_config=MetricsDomainConfig(display_name=db_id),
791
+ tables=unique_tables,
733
792
  )
734
793
  )
735
794
 
@@ -743,12 +802,15 @@ class AgentOS:
743
802
 
744
803
  dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
745
804
 
746
- for db_id in self.dbs.keys():
805
+ for db_id, dbs in self.dbs.items():
747
806
  if db_id not in dbs_with_specific_config:
807
+ # Collect unique table names from all databases with the same id
808
+ unique_tables = list(set(db.eval_table_name for db in dbs))
748
809
  evals_config.dbs.append(
749
810
  DatabaseConfig(
750
811
  db_id=db_id,
751
812
  domain_config=EvalsDomainConfig(display_name=db_id),
813
+ tables=unique_tables,
752
814
  )
753
815
  )
754
816
 
agno/os/config.py CHANGED
@@ -44,6 +44,7 @@ class DatabaseConfig(BaseModel, Generic[DomainConfigType]):
44
44
 
45
45
  db_id: str
46
46
  domain_config: Optional[DomainConfigType] = None
47
+ tables: Optional[List[str]] = None
47
48
 
48
49
 
49
50
  class EvalsConfig(EvalsDomainConfig):
@@ -19,6 +19,7 @@ from agno.agent.agent import Agent
19
19
  from agno.os.interfaces.agui.utils import (
20
20
  async_stream_agno_response_as_agui_events,
21
21
  convert_agui_messages_to_agno_messages,
22
+ validate_agui_state,
22
23
  )
23
24
  from agno.team.team import Team
24
25
 
@@ -39,6 +40,9 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
39
40
  if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
40
41
  user_id = run_input.forwarded_props.get("user_id")
41
42
 
43
+ # Validating the session state is of the expected type (dict)
44
+ session_state = validate_agui_state(run_input.state, run_input.thread_id)
45
+
42
46
  # Request streaming response from agent
43
47
  response_stream = agent.arun(
44
48
  input=messages,
@@ -46,6 +50,7 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
46
50
  stream=True,
47
51
  stream_events=True,
48
52
  user_id=user_id,
53
+ session_state=session_state,
49
54
  )
50
55
 
51
56
  # Stream the response content in AG-UI format
@@ -75,6 +80,9 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
75
80
  if input.forwarded_props and isinstance(input.forwarded_props, dict):
76
81
  user_id = input.forwarded_props.get("user_id")
77
82
 
83
+ # Validating the session state is of the expected type (dict)
84
+ session_state = validate_agui_state(input.state, input.thread_id)
85
+
78
86
  # Request streaming response from team
79
87
  response_stream = team.arun(
80
88
  input=messages,
@@ -82,6 +90,7 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
82
90
  stream=True,
83
91
  stream_steps=True,
84
92
  user_id=user_id,
93
+ session_state=session_state,
85
94
  )
86
95
 
87
96
  # Stream the response content in AG-UI format
@@ -3,8 +3,8 @@
3
3
  import json
4
4
  import uuid
5
5
  from collections.abc import Iterator
6
- from dataclasses import dataclass
7
- from typing import AsyncIterator, List, Set, Tuple, Union
6
+ from dataclasses import asdict, dataclass, is_dataclass
7
+ from typing import Any, AsyncIterator, Dict, List, Optional, Set, Tuple, Union
8
8
 
9
9
  from ag_ui.core import (
10
10
  BaseEvent,
@@ -22,14 +22,48 @@ from ag_ui.core import (
22
22
  ToolCallStartEvent,
23
23
  )
24
24
  from ag_ui.core.types import Message as AGUIMessage
25
+ from pydantic import BaseModel
25
26
 
26
27
  from agno.models.message import Message
27
28
  from agno.run.agent import RunContentEvent, RunEvent, RunOutputEvent, RunPausedEvent
28
29
  from agno.run.team import RunContentEvent as TeamRunContentEvent
29
30
  from agno.run.team import TeamRunEvent, TeamRunOutputEvent
31
+ from agno.utils.log import log_warning
30
32
  from agno.utils.message import get_text_from_message
31
33
 
32
34
 
35
+ def validate_agui_state(state: Any, thread_id: str) -> Optional[Dict[str, Any]]:
36
+ """Validate the given AGUI state is of the expected type (dict)."""
37
+ if state is None:
38
+ return None
39
+
40
+ if isinstance(state, dict):
41
+ return state
42
+
43
+ if isinstance(state, BaseModel):
44
+ try:
45
+ return state.model_dump()
46
+ except Exception:
47
+ pass
48
+
49
+ if is_dataclass(state):
50
+ try:
51
+ return asdict(state) # type: ignore
52
+ except Exception:
53
+ pass
54
+
55
+ if hasattr(state, "to_dict") and callable(getattr(state, "to_dict")):
56
+ try:
57
+ result = state.to_dict() # type: ignore
58
+ if isinstance(result, dict):
59
+ return result
60
+ except Exception:
61
+ pass
62
+
63
+ log_warning(f"AGUI state must be a dict, got {type(state).__name__}. State will be ignored. Thread: {thread_id}")
64
+ return None
65
+
66
+
33
67
  @dataclass
34
68
  class EventBuffer:
35
69
  """Buffer to manage event ordering constraints, relevant when mapping Agno responses to AG-UI events."""
@@ -264,7 +298,19 @@ def _create_events_from_chunk(
264
298
 
265
299
  # Handle custom events
266
300
  elif chunk.event == RunEvent.custom_event:
267
- custom_event = CustomEvent(name=chunk.event, value=chunk.content)
301
+ # Use the name of the event class if available, otherwise default to the CustomEvent
302
+ try:
303
+ custom_event_name = chunk.__class__.__name__
304
+ except Exception:
305
+ custom_event_name = chunk.event
306
+
307
+ # Use the complete Agno event as value if parsing it works, else the event content field
308
+ try:
309
+ custom_event_value = chunk.to_dict()
310
+ except Exception:
311
+ custom_event_value = chunk.content # type: ignore
312
+
313
+ custom_event = CustomEvent(name=custom_event_name, value=custom_event_value)
268
314
  events_to_emit.append(custom_event)
269
315
 
270
316
  return events_to_emit, message_started, message_id
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(),