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.
- agno/agent/agent.py +23 -15
- agno/db/base.py +23 -0
- agno/db/dynamo/dynamo.py +20 -25
- agno/db/dynamo/schemas.py +1 -0
- agno/db/firestore/firestore.py +11 -0
- agno/db/gcs_json/gcs_json_db.py +4 -0
- agno/db/in_memory/in_memory_db.py +4 -0
- agno/db/json/json_db.py +4 -0
- agno/db/mongo/async_mongo.py +27 -0
- agno/db/mongo/mongo.py +25 -0
- agno/db/mysql/mysql.py +26 -1
- agno/db/postgres/async_postgres.py +26 -1
- agno/db/postgres/postgres.py +26 -1
- agno/db/redis/redis.py +4 -0
- agno/db/singlestore/singlestore.py +24 -0
- agno/db/sqlite/async_sqlite.py +25 -1
- agno/db/sqlite/sqlite.py +25 -1
- agno/db/surrealdb/surrealdb.py +13 -1
- agno/knowledge/reader/docx_reader.py +0 -1
- agno/models/azure/ai_foundry.py +2 -1
- agno/models/cerebras/cerebras.py +3 -2
- agno/models/openai/chat.py +2 -1
- agno/models/openai/responses.py +2 -1
- agno/os/app.py +112 -50
- agno/os/config.py +1 -0
- agno/os/interfaces/agui/router.py +9 -0
- agno/os/interfaces/agui/utils.py +49 -3
- agno/os/mcp.py +8 -8
- agno/os/router.py +27 -9
- agno/os/routers/evals/evals.py +12 -7
- agno/os/routers/memory/memory.py +18 -10
- agno/os/routers/metrics/metrics.py +6 -4
- agno/os/routers/session/session.py +21 -11
- agno/os/utils.py +57 -11
- agno/team/team.py +26 -21
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/redis/__init__.py +4 -0
- {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/METADATA +11 -13
- {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/RECORD +42 -42
- {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/WHEEL +0 -0
- {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.8.dist-info → agno-2.2.9.dist-info}/top_level.txt +0 -0
agno/db/surrealdb/surrealdb.py
CHANGED
|
@@ -116,12 +116,24 @@ class SurrealDb(BaseDb):
|
|
|
116
116
|
"workflows": self._workflows_table_name,
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
def
|
|
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)
|
agno/models/azure/ai_foundry.py
CHANGED
|
@@ -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=
|
|
120
|
+
strict=self.strict_output,
|
|
120
121
|
),
|
|
121
122
|
)
|
|
122
123
|
|
agno/models/cerebras/cerebras.py
CHANGED
|
@@ -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
|
|
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"] =
|
|
198
|
+
schema["strict"] = self.strict_output
|
|
198
199
|
|
|
199
200
|
request_params["response_format"] = response_format
|
|
200
201
|
|
agno/models/openai/chat.py
CHANGED
|
@@ -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":
|
|
219
|
+
"strict": self.strict_output,
|
|
219
220
|
},
|
|
220
221
|
}
|
|
221
222
|
else:
|
agno/models/openai/responses.py
CHANGED
|
@@ -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":
|
|
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[
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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(),
|