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