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.
- agno/agent/agent.py +37 -19
- 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 +127 -65
- 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 +33 -23
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/redis/__init__.py +4 -0
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +26 -4
- agno/workflow/loop.py +9 -0
- agno/workflow/parallel.py +39 -16
- agno/workflow/router.py +25 -4
- agno/workflow/step.py +162 -91
- agno/workflow/steps.py +9 -0
- agno/workflow/workflow.py +26 -22
- {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/METADATA +11 -13
- {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/RECORD +50 -50
- {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/WHEEL +0 -0
- {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.8.dist-info → agno-2.2.10.dist-info}/top_level.txt +0 -0
|
@@ -112,6 +112,17 @@ class SingleStoreDb(BaseDb):
|
|
|
112
112
|
self.Session: scoped_session = scoped_session(sessionmaker(bind=self.db_engine))
|
|
113
113
|
|
|
114
114
|
# -- DB methods --
|
|
115
|
+
def table_exists(self, table_name: str) -> bool:
|
|
116
|
+
"""Check if a table with the given name exists in the SingleStore database.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
table_name: Name of the table to check
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bool: True if the table exists in the database, False otherwise
|
|
123
|
+
"""
|
|
124
|
+
with self.Session() as sess:
|
|
125
|
+
return is_table_available(session=sess, table_name=table_name, db_schema=self.db_schema)
|
|
115
126
|
|
|
116
127
|
def _create_table_structure_only(self, table_name: str, table_type: str, db_schema: Optional[str]) -> Table:
|
|
117
128
|
"""
|
|
@@ -157,6 +168,19 @@ class SingleStoreDb(BaseDb):
|
|
|
157
168
|
log_error(f"Could not create table structure for {table_ref}: {e}")
|
|
158
169
|
raise
|
|
159
170
|
|
|
171
|
+
def _create_all_tables(self):
|
|
172
|
+
"""Create all tables for the database."""
|
|
173
|
+
tables_to_create = [
|
|
174
|
+
(self.session_table_name, "sessions"),
|
|
175
|
+
(self.memory_table_name, "memories"),
|
|
176
|
+
(self.metrics_table_name, "metrics"),
|
|
177
|
+
(self.eval_table_name, "evals"),
|
|
178
|
+
(self.knowledge_table_name, "knowledge"),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for table_name, table_type in tables_to_create:
|
|
182
|
+
self._create_table(table_name=table_name, table_type=table_type, db_schema=self.db_schema)
|
|
183
|
+
|
|
160
184
|
def _create_table(self, table_name: str, table_type: str, db_schema: Optional[str]) -> Table:
|
|
161
185
|
"""
|
|
162
186
|
Create a table with the appropriate schema based on the table type.
|
agno/db/sqlite/async_sqlite.py
CHANGED
|
@@ -112,6 +112,30 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
112
112
|
self.async_session_factory = async_sessionmaker(bind=self.db_engine, expire_on_commit=False)
|
|
113
113
|
|
|
114
114
|
# -- DB methods --
|
|
115
|
+
async def table_exists(self, table_name: str) -> bool:
|
|
116
|
+
"""Check if a table with the given name exists in the SQLite database.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
table_name: Name of the table to check
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bool: True if the table exists in the database, False otherwise
|
|
123
|
+
"""
|
|
124
|
+
async with self.async_session_factory() as sess:
|
|
125
|
+
return await ais_table_available(session=sess, table_name=table_name)
|
|
126
|
+
|
|
127
|
+
async def _create_all_tables(self):
|
|
128
|
+
"""Create all tables for the database."""
|
|
129
|
+
tables_to_create = [
|
|
130
|
+
(self.session_table_name, "sessions"),
|
|
131
|
+
(self.memory_table_name, "memories"),
|
|
132
|
+
(self.metrics_table_name, "metrics"),
|
|
133
|
+
(self.eval_table_name, "evals"),
|
|
134
|
+
(self.knowledge_table_name, "knowledge"),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
for table_name, table_type in tables_to_create:
|
|
138
|
+
await self._create_table(table_name=table_name, table_type=table_type)
|
|
115
139
|
|
|
116
140
|
async def _create_table(self, table_name: str, table_type: str) -> Table:
|
|
117
141
|
"""
|
|
@@ -188,7 +212,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
188
212
|
except Exception as e:
|
|
189
213
|
log_warning(f"Error creating index {idx.name}: {e}")
|
|
190
214
|
|
|
191
|
-
|
|
215
|
+
log_debug(f"Successfully created table '{table_name}'")
|
|
192
216
|
return table
|
|
193
217
|
|
|
194
218
|
except Exception as e:
|
agno/db/sqlite/sqlite.py
CHANGED
|
@@ -113,6 +113,30 @@ class SqliteDb(BaseDb):
|
|
|
113
113
|
self.Session: scoped_session = scoped_session(sessionmaker(bind=self.db_engine))
|
|
114
114
|
|
|
115
115
|
# -- DB methods --
|
|
116
|
+
def table_exists(self, table_name: str) -> bool:
|
|
117
|
+
"""Check if a table with the given name exists in the SQLite database.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
table_name: Name of the table to check
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
bool: True if the table exists in the database, False otherwise
|
|
124
|
+
"""
|
|
125
|
+
with self.Session() as sess:
|
|
126
|
+
return is_table_available(session=sess, table_name=table_name)
|
|
127
|
+
|
|
128
|
+
def _create_all_tables(self):
|
|
129
|
+
"""Create all tables for the database."""
|
|
130
|
+
tables_to_create = [
|
|
131
|
+
(self.session_table_name, "sessions"),
|
|
132
|
+
(self.memory_table_name, "memories"),
|
|
133
|
+
(self.metrics_table_name, "metrics"),
|
|
134
|
+
(self.eval_table_name, "evals"),
|
|
135
|
+
(self.knowledge_table_name, "knowledge"),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for table_name, table_type in tables_to_create:
|
|
139
|
+
self._create_table(table_name=table_name, table_type=table_type)
|
|
116
140
|
|
|
117
141
|
def _create_table(self, table_name: str, table_type: str) -> Table:
|
|
118
142
|
"""
|
|
@@ -186,7 +210,7 @@ class SqliteDb(BaseDb):
|
|
|
186
210
|
except Exception as e:
|
|
187
211
|
log_warning(f"Error creating index {idx.name}: {e}")
|
|
188
212
|
|
|
189
|
-
|
|
213
|
+
log_debug(f"Successfully created table '{table_name}'")
|
|
190
214
|
return table
|
|
191
215
|
|
|
192
216
|
except Exception as e:
|
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:
|
|
@@ -237,28 +238,35 @@ class AgentOS:
|
|
|
237
238
|
"""Re-provision all routes for the AgentOS."""
|
|
238
239
|
updated_routers = [
|
|
239
240
|
get_session_router(dbs=self.dbs),
|
|
240
|
-
get_memory_router(dbs=self.dbs),
|
|
241
|
-
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
242
241
|
get_metrics_router(dbs=self.dbs),
|
|
243
242
|
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
243
|
+
get_memory_router(dbs=self.dbs),
|
|
244
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
244
245
|
]
|
|
245
246
|
|
|
246
247
|
# Clear all previously existing routes
|
|
247
|
-
app.router.routes = [
|
|
248
|
+
app.router.routes = [
|
|
249
|
+
route
|
|
250
|
+
for route in app.router.routes
|
|
251
|
+
if hasattr(route, "path") and route.path in ["/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"] # type: ignore
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Add the built-in routes
|
|
255
|
+
self._add_built_in_routes(app=app)
|
|
248
256
|
|
|
249
257
|
# Add the updated routes
|
|
250
258
|
for router in updated_routers:
|
|
251
259
|
self._add_router(app, router)
|
|
252
260
|
|
|
253
|
-
# Add the built-in routes
|
|
254
|
-
self._add_built_in_routes(app=app)
|
|
255
|
-
|
|
256
261
|
def _add_built_in_routes(self, app: FastAPI) -> None:
|
|
257
262
|
"""Add all AgentOSbuilt-in routes to the given app."""
|
|
263
|
+
# Add the home router if MCP server is not enabled
|
|
264
|
+
if not self.enable_mcp_server:
|
|
265
|
+
self._add_router(app, get_home_router(self))
|
|
266
|
+
|
|
267
|
+
self._add_router(app, get_health_router())
|
|
258
268
|
self._add_router(app, get_base_router(self, settings=self.settings))
|
|
259
269
|
self._add_router(app, get_websocket_router(self, settings=self.settings))
|
|
260
|
-
self._add_router(app, get_health_router())
|
|
261
|
-
self._add_router(app, get_home_router(self))
|
|
262
270
|
|
|
263
271
|
# Add A2A interface if relevant
|
|
264
272
|
has_a2a_interface = False
|
|
@@ -274,10 +282,6 @@ class AgentOS:
|
|
|
274
282
|
self.interfaces.append(a2a_interface)
|
|
275
283
|
self._add_router(app, a2a_interface.get_router())
|
|
276
284
|
|
|
277
|
-
# Add the home router if MCP server is not enabled
|
|
278
|
-
if not self.enable_mcp_server:
|
|
279
|
-
self._add_router(app, get_home_router(self))
|
|
280
|
-
|
|
281
285
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
282
286
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
283
287
|
app_lifespan = lifespan
|
|
@@ -446,9 +450,6 @@ class AgentOS:
|
|
|
446
450
|
# Mount MCP if needed
|
|
447
451
|
if self.enable_mcp_server and self._mcp_app:
|
|
448
452
|
fastapi_app.mount("/", self._mcp_app)
|
|
449
|
-
else:
|
|
450
|
-
# Add the home router
|
|
451
|
-
self._add_router(fastapi_app, get_home_router(self))
|
|
452
453
|
|
|
453
454
|
if not self._app_set:
|
|
454
455
|
|
|
@@ -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
|