agno 2.0.0a1__py3-none-any.whl → 2.0.0rc2__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 +416 -41
- agno/api/agent.py +2 -2
- agno/api/evals.py +2 -2
- agno/api/os.py +1 -1
- agno/api/settings.py +2 -2
- agno/api/team.py +2 -2
- agno/db/dynamo/dynamo.py +0 -6
- agno/db/firestore/firestore.py +0 -6
- agno/db/in_memory/in_memory_db.py +0 -6
- agno/db/json/json_db.py +0 -6
- agno/db/mongo/mongo.py +8 -9
- agno/db/mysql/utils.py +0 -1
- agno/db/postgres/postgres.py +0 -10
- agno/db/postgres/utils.py +0 -1
- agno/db/redis/redis.py +0 -4
- agno/db/singlestore/singlestore.py +0 -10
- agno/db/singlestore/utils.py +0 -1
- agno/db/sqlite/sqlite.py +0 -4
- agno/db/sqlite/utils.py +0 -1
- agno/eval/accuracy.py +12 -5
- agno/integrations/discord/client.py +5 -1
- agno/knowledge/chunking/strategy.py +14 -14
- agno/knowledge/embedder/aws_bedrock.py +2 -2
- agno/knowledge/knowledge.py +156 -120
- agno/knowledge/reader/arxiv_reader.py +5 -5
- agno/knowledge/reader/csv_reader.py +6 -77
- agno/knowledge/reader/docx_reader.py +5 -5
- agno/knowledge/reader/firecrawl_reader.py +5 -5
- agno/knowledge/reader/json_reader.py +5 -5
- agno/knowledge/reader/markdown_reader.py +31 -9
- agno/knowledge/reader/pdf_reader.py +10 -123
- agno/knowledge/reader/reader_factory.py +65 -72
- agno/knowledge/reader/s3_reader.py +44 -114
- agno/knowledge/reader/text_reader.py +5 -5
- agno/knowledge/reader/url_reader.py +75 -31
- agno/knowledge/reader/web_search_reader.py +6 -29
- agno/knowledge/reader/website_reader.py +5 -5
- agno/knowledge/reader/wikipedia_reader.py +5 -5
- agno/knowledge/reader/youtube_reader.py +6 -6
- agno/knowledge/utils.py +10 -10
- agno/models/anthropic/claude.py +2 -49
- agno/models/aws/bedrock.py +3 -7
- agno/models/base.py +37 -6
- agno/models/message.py +7 -6
- agno/os/app.py +168 -64
- agno/os/interfaces/agui/agui.py +1 -1
- agno/os/interfaces/agui/utils.py +16 -9
- agno/os/interfaces/slack/slack.py +2 -3
- agno/os/interfaces/whatsapp/whatsapp.py +2 -3
- agno/os/mcp.py +235 -0
- agno/os/router.py +576 -19
- agno/os/routers/evals/evals.py +201 -12
- agno/os/routers/knowledge/knowledge.py +455 -18
- agno/os/routers/memory/memory.py +260 -29
- agno/os/routers/metrics/metrics.py +127 -7
- agno/os/routers/session/session.py +398 -25
- agno/os/schema.py +55 -2
- agno/os/settings.py +0 -1
- agno/run/agent.py +96 -2
- agno/run/cancel.py +0 -2
- agno/run/team.py +93 -2
- agno/run/workflow.py +25 -12
- agno/team/team.py +863 -1053
- agno/tools/function.py +65 -7
- agno/tools/linear.py +1 -1
- agno/tools/mcp.py +1 -2
- agno/utils/gemini.py +31 -1
- agno/utils/log.py +52 -2
- agno/utils/mcp.py +55 -3
- agno/utils/models/claude.py +41 -0
- agno/utils/print_response/team.py +177 -73
- agno/utils/streamlit.py +481 -0
- agno/workflow/workflow.py +17 -1
- {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/METADATA +1 -1
- {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/RECORD +78 -77
- agno/knowledge/reader/gcs_reader.py +0 -67
- {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/WHEEL +0 -0
- {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/top_level.txt +0 -0
agno/os/app.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from functools import partial
|
|
1
3
|
from os import getenv
|
|
2
|
-
from typing import Any, Dict, List, Optional,
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
3
5
|
from uuid import uuid4
|
|
4
6
|
|
|
5
7
|
from fastapi import FastAPI, HTTPException
|
|
@@ -34,16 +36,31 @@ from agno.os.routers.session import get_session_router
|
|
|
34
36
|
from agno.os.settings import AgnoAPISettings
|
|
35
37
|
from agno.os.utils import generate_id
|
|
36
38
|
from agno.team.team import Team
|
|
39
|
+
from agno.tools.mcp import MCPTools, MultiMCPTools
|
|
37
40
|
from agno.workflow.workflow import Workflow
|
|
38
41
|
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
@asynccontextmanager
|
|
44
|
+
async def mcp_lifespan(app, mcp_tools: List[Union[MCPTools, MultiMCPTools]]):
|
|
45
|
+
"""Manage MCP connection lifecycle inside a FastAPI app"""
|
|
46
|
+
# Startup logic: connect to all contextual MCP servers
|
|
47
|
+
for tool in mcp_tools:
|
|
48
|
+
await tool.connect()
|
|
49
|
+
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
# Shutdown logic: Close all contextual MCP connections
|
|
53
|
+
for tool in mcp_tools:
|
|
54
|
+
await tool.close()
|
|
42
55
|
|
|
56
|
+
|
|
57
|
+
class AgentOS:
|
|
43
58
|
def __init__(
|
|
44
59
|
self,
|
|
45
60
|
os_id: Optional[str] = None,
|
|
61
|
+
name: Optional[str] = None,
|
|
46
62
|
description: Optional[str] = None,
|
|
63
|
+
version: Optional[str] = None,
|
|
47
64
|
agents: Optional[List[Agent]] = None,
|
|
48
65
|
teams: Optional[List[Team]] = None,
|
|
49
66
|
workflows: Optional[List[Workflow]] = None,
|
|
@@ -51,13 +68,14 @@ class AgentOS:
|
|
|
51
68
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
52
69
|
settings: Optional[AgnoAPISettings] = None,
|
|
53
70
|
fastapi_app: Optional[FastAPI] = None,
|
|
71
|
+
lifespan: Optional[Any] = None,
|
|
72
|
+
enable_mcp: bool = False,
|
|
54
73
|
telemetry: bool = True,
|
|
55
74
|
):
|
|
56
75
|
if not agents and not workflows and not teams:
|
|
57
76
|
raise ValueError("Either agents, teams or workflows must be provided.")
|
|
58
77
|
|
|
59
78
|
self.config = self._load_yaml_config(config) if isinstance(config, str) else config
|
|
60
|
-
self.description = description
|
|
61
79
|
|
|
62
80
|
self.agents: Optional[List[Agent]] = agents
|
|
63
81
|
self.workflows: Optional[List[Workflow]] = workflows
|
|
@@ -65,19 +83,36 @@ class AgentOS:
|
|
|
65
83
|
self.interfaces = interfaces or []
|
|
66
84
|
|
|
67
85
|
self.settings: AgnoAPISettings = settings or AgnoAPISettings()
|
|
68
|
-
|
|
86
|
+
|
|
87
|
+
self._app_set = False
|
|
88
|
+
self.fastapi_app: Optional[FastAPI] = None
|
|
89
|
+
if fastapi_app:
|
|
90
|
+
self.fastapi_app = fastapi_app
|
|
91
|
+
self._app_set = True
|
|
69
92
|
|
|
70
93
|
self.interfaces = interfaces or []
|
|
71
94
|
|
|
72
95
|
self.os_id: Optional[str] = os_id
|
|
96
|
+
self.name = name
|
|
97
|
+
self.version = version
|
|
73
98
|
self.description = description
|
|
74
99
|
|
|
75
100
|
self.telemetry = telemetry
|
|
76
101
|
|
|
77
|
-
self.
|
|
102
|
+
self.enable_mcp = enable_mcp
|
|
103
|
+
self.lifespan = lifespan
|
|
104
|
+
|
|
105
|
+
# List of all MCP tools used inside the AgentOS
|
|
106
|
+
self.mcp_tools: List[Union[MCPTools, MultiMCPTools]] = []
|
|
78
107
|
|
|
79
108
|
if self.agents:
|
|
80
109
|
for agent in self.agents:
|
|
110
|
+
# Track all MCP tools to later handle their connection
|
|
111
|
+
if agent.tools:
|
|
112
|
+
for tool in agent.tools:
|
|
113
|
+
if isinstance(tool, MCPTools) or isinstance(tool, MultiMCPTools):
|
|
114
|
+
self.mcp_tools.append(tool)
|
|
115
|
+
|
|
81
116
|
agent.initialize_agent()
|
|
82
117
|
|
|
83
118
|
# Required for the built-in routes to work
|
|
@@ -85,6 +120,12 @@ class AgentOS:
|
|
|
85
120
|
|
|
86
121
|
if self.teams:
|
|
87
122
|
for team in self.teams:
|
|
123
|
+
# Track all MCP tools to later handle their connection
|
|
124
|
+
if team.tools:
|
|
125
|
+
for tool in team.tools:
|
|
126
|
+
if isinstance(tool, MCPTools) or isinstance(tool, MultiMCPTools):
|
|
127
|
+
self.mcp_tools.append(tool)
|
|
128
|
+
|
|
88
129
|
team.initialize_team()
|
|
89
130
|
|
|
90
131
|
# Required for the built-in routes to work
|
|
@@ -99,6 +140,7 @@ class AgentOS:
|
|
|
99
140
|
|
|
100
141
|
if self.workflows:
|
|
101
142
|
for workflow in self.workflows:
|
|
143
|
+
# TODO: track MCP tools in workflow members
|
|
102
144
|
if not workflow.id:
|
|
103
145
|
workflow.id = generate_id(workflow.name)
|
|
104
146
|
|
|
@@ -107,6 +149,117 @@ class AgentOS:
|
|
|
107
149
|
|
|
108
150
|
log_os_telemetry(launch=OSLaunch(os_id=self.os_id, data=self._get_telemetry_data()))
|
|
109
151
|
|
|
152
|
+
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
153
|
+
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
154
|
+
app_lifespan = lifespan
|
|
155
|
+
if self.mcp_tools is not None:
|
|
156
|
+
mcp_tools_lifespan = partial(mcp_lifespan, mcp_tools=self.mcp_tools)
|
|
157
|
+
# If there is already a lifespan, combine it with the MCP lifespan
|
|
158
|
+
if lifespan is not None:
|
|
159
|
+
# Combine both lifespans
|
|
160
|
+
@asynccontextmanager
|
|
161
|
+
async def combined_lifespan(app: FastAPI):
|
|
162
|
+
# Run both lifespans
|
|
163
|
+
async with lifespan(app): # type: ignore
|
|
164
|
+
async with mcp_tools_lifespan(app): # type: ignore
|
|
165
|
+
yield
|
|
166
|
+
|
|
167
|
+
app_lifespan = combined_lifespan # type: ignore
|
|
168
|
+
else:
|
|
169
|
+
app_lifespan = mcp_tools_lifespan
|
|
170
|
+
|
|
171
|
+
return FastAPI(
|
|
172
|
+
title=self.name or "Agno AgentOS",
|
|
173
|
+
version=self.version or "1.0.0",
|
|
174
|
+
description=self.description or "An agent operating system.",
|
|
175
|
+
docs_url="/docs" if self.settings.docs_enabled else None,
|
|
176
|
+
redoc_url="/redoc" if self.settings.docs_enabled else None,
|
|
177
|
+
openapi_url="/openapi.json" if self.settings.docs_enabled else None,
|
|
178
|
+
lifespan=app_lifespan,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def get_app(self) -> FastAPI:
|
|
182
|
+
if not self.fastapi_app:
|
|
183
|
+
if self.enable_mcp:
|
|
184
|
+
from contextlib import asynccontextmanager
|
|
185
|
+
|
|
186
|
+
from agno.os.mcp import get_mcp_server
|
|
187
|
+
|
|
188
|
+
self.mcp_app = get_mcp_server(self)
|
|
189
|
+
|
|
190
|
+
final_lifespan = self.mcp_app.lifespan
|
|
191
|
+
if self.lifespan is not None:
|
|
192
|
+
# Combine both lifespans
|
|
193
|
+
@asynccontextmanager
|
|
194
|
+
async def combined_lifespan(app: FastAPI):
|
|
195
|
+
# Run both lifespans
|
|
196
|
+
async with self.lifespan(app): # type: ignore
|
|
197
|
+
async with self.mcp_app.lifespan(app): # type: ignore
|
|
198
|
+
yield
|
|
199
|
+
|
|
200
|
+
final_lifespan = combined_lifespan # type: ignore
|
|
201
|
+
|
|
202
|
+
self.fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
203
|
+
else:
|
|
204
|
+
self.fastapi_app = self._make_app(lifespan=self.lifespan)
|
|
205
|
+
|
|
206
|
+
# Add routes
|
|
207
|
+
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
208
|
+
|
|
209
|
+
for interface in self.interfaces:
|
|
210
|
+
interface_router = interface.get_router()
|
|
211
|
+
self.fastapi_app.include_router(interface_router)
|
|
212
|
+
|
|
213
|
+
self._auto_discover_databases()
|
|
214
|
+
self._auto_discover_knowledge_instances()
|
|
215
|
+
self._setup_routers()
|
|
216
|
+
|
|
217
|
+
# Mount MCP if needed
|
|
218
|
+
if self.enable_mcp and self.mcp_app:
|
|
219
|
+
self.fastapi_app.mount("/", self.mcp_app)
|
|
220
|
+
|
|
221
|
+
# Add middleware (only if app is not set)
|
|
222
|
+
if not self._app_set:
|
|
223
|
+
|
|
224
|
+
@self.fastapi_app.exception_handler(HTTPException)
|
|
225
|
+
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
226
|
+
return JSONResponse(
|
|
227
|
+
status_code=exc.status_code,
|
|
228
|
+
content={"detail": str(exc.detail)},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def general_exception_handler(request: Request, call_next):
|
|
232
|
+
try:
|
|
233
|
+
return await call_next(request)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return JSONResponse(
|
|
236
|
+
status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
|
|
237
|
+
content={"detail": str(e)},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
self.fastapi_app.middleware("http")(general_exception_handler)
|
|
241
|
+
|
|
242
|
+
self.fastapi_app.add_middleware(
|
|
243
|
+
CORSMiddleware,
|
|
244
|
+
allow_origins=self.settings.cors_origin_list, # type: ignore
|
|
245
|
+
allow_credentials=True,
|
|
246
|
+
allow_methods=["*"],
|
|
247
|
+
allow_headers=["*"],
|
|
248
|
+
expose_headers=["*"],
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return self.fastapi_app
|
|
252
|
+
|
|
253
|
+
def get_routes(self) -> List[Any]:
|
|
254
|
+
"""Retrieve all routes from the FastAPI app.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List[Any]: List of routes included in the FastAPI app.
|
|
258
|
+
"""
|
|
259
|
+
app = self.get_app()
|
|
260
|
+
|
|
261
|
+
return app.routes
|
|
262
|
+
|
|
110
263
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
111
264
|
"""Get the telemetry data for the OS"""
|
|
112
265
|
return {
|
|
@@ -187,7 +340,7 @@ class AgentOS:
|
|
|
187
340
|
DatabaseConfig(
|
|
188
341
|
db_id=db_id,
|
|
189
342
|
domain_config=SessionDomainConfig(
|
|
190
|
-
display_name="Sessions" if not multiple_dbs else "Sessions " + db_id
|
|
343
|
+
display_name="Sessions" if not multiple_dbs else "Sessions in database '" + db_id + "'"
|
|
191
344
|
),
|
|
192
345
|
)
|
|
193
346
|
)
|
|
@@ -209,7 +362,7 @@ class AgentOS:
|
|
|
209
362
|
DatabaseConfig(
|
|
210
363
|
db_id=db_id,
|
|
211
364
|
domain_config=MemoryDomainConfig(
|
|
212
|
-
display_name="Memory" if not multiple_dbs else "Memory " + db_id
|
|
365
|
+
display_name="Memory" if not multiple_dbs else "Memory in database '" + db_id + "'"
|
|
213
366
|
),
|
|
214
367
|
)
|
|
215
368
|
)
|
|
@@ -231,7 +384,7 @@ class AgentOS:
|
|
|
231
384
|
DatabaseConfig(
|
|
232
385
|
db_id=db_id,
|
|
233
386
|
domain_config=KnowledgeDomainConfig(
|
|
234
|
-
display_name="Knowledge" if not multiple_dbs else "Knowledge " + db_id
|
|
387
|
+
display_name="Knowledge" if not multiple_dbs else "Knowledge in database " + db_id
|
|
235
388
|
),
|
|
236
389
|
)
|
|
237
390
|
)
|
|
@@ -253,7 +406,7 @@ class AgentOS:
|
|
|
253
406
|
DatabaseConfig(
|
|
254
407
|
db_id=db_id,
|
|
255
408
|
domain_config=MetricsDomainConfig(
|
|
256
|
-
display_name="Metrics" if not multiple_dbs else "Metrics " + db_id
|
|
409
|
+
display_name="Metrics" if not multiple_dbs else "Metrics in database '" + db_id + "'"
|
|
257
410
|
),
|
|
258
411
|
)
|
|
259
412
|
)
|
|
@@ -274,7 +427,9 @@ class AgentOS:
|
|
|
274
427
|
evals_config.dbs.append(
|
|
275
428
|
DatabaseConfig(
|
|
276
429
|
db_id=db_id,
|
|
277
|
-
domain_config=EvalsDomainConfig(
|
|
430
|
+
domain_config=EvalsDomainConfig(
|
|
431
|
+
display_name="Evals" if not multiple_dbs else "Evals in database '" + db_id + "'"
|
|
432
|
+
),
|
|
278
433
|
)
|
|
279
434
|
)
|
|
280
435
|
|
|
@@ -303,58 +458,6 @@ class AgentOS:
|
|
|
303
458
|
|
|
304
459
|
return self.os_id
|
|
305
460
|
|
|
306
|
-
def get_app(self) -> FastAPI:
|
|
307
|
-
if not self.fastapi_app:
|
|
308
|
-
self.fastapi_app = FastAPI(
|
|
309
|
-
title=self.settings.title,
|
|
310
|
-
docs_url="/docs" if self.settings.docs_enabled else None,
|
|
311
|
-
redoc_url="/redoc" if self.settings.docs_enabled else None,
|
|
312
|
-
openapi_url="/openapi.json" if self.settings.docs_enabled else None,
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
if not self.fastapi_app:
|
|
316
|
-
raise Exception("API App could not be created.")
|
|
317
|
-
|
|
318
|
-
@self.fastapi_app.exception_handler(HTTPException)
|
|
319
|
-
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
320
|
-
return JSONResponse(
|
|
321
|
-
status_code=exc.status_code,
|
|
322
|
-
content={"detail": str(exc.detail)},
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
async def general_exception_handler(request: Request, call_next):
|
|
326
|
-
try:
|
|
327
|
-
return await call_next(request)
|
|
328
|
-
except Exception as e:
|
|
329
|
-
return JSONResponse(
|
|
330
|
-
status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
|
|
331
|
-
content={"detail": str(e)},
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
self.fastapi_app.middleware("http")(general_exception_handler)
|
|
335
|
-
|
|
336
|
-
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
337
|
-
|
|
338
|
-
for interface in self.interfaces:
|
|
339
|
-
interface_router = interface.get_router()
|
|
340
|
-
self.fastapi_app.include_router(interface_router)
|
|
341
|
-
self.interfaces_loaded.append((interface.type, interface.router_prefix))
|
|
342
|
-
|
|
343
|
-
self._auto_discover_databases()
|
|
344
|
-
self._auto_discover_knowledge_instances()
|
|
345
|
-
self._setup_routers()
|
|
346
|
-
|
|
347
|
-
self.fastapi_app.add_middleware(
|
|
348
|
-
CORSMiddleware,
|
|
349
|
-
allow_origins=self.settings.cors_origin_list, # type: ignore
|
|
350
|
-
allow_credentials=True,
|
|
351
|
-
allow_methods=["*"],
|
|
352
|
-
allow_headers=["*"],
|
|
353
|
-
expose_headers=["*"],
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
return self.fastapi_app
|
|
357
|
-
|
|
358
461
|
def serve(
|
|
359
462
|
self,
|
|
360
463
|
app: Union[str, FastAPI],
|
|
@@ -362,6 +465,7 @@ class AgentOS:
|
|
|
362
465
|
host: str = "localhost",
|
|
363
466
|
port: int = 7777,
|
|
364
467
|
reload: bool = False,
|
|
468
|
+
workers: Optional[int] = None,
|
|
365
469
|
**kwargs,
|
|
366
470
|
):
|
|
367
471
|
import uvicorn
|
|
@@ -390,4 +494,4 @@ class AgentOS:
|
|
|
390
494
|
)
|
|
391
495
|
)
|
|
392
496
|
|
|
393
|
-
uvicorn.run(app=app, host=host, port=port, reload=reload, **kwargs)
|
|
497
|
+
uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, **kwargs)
|
agno/os/interfaces/agui/agui.py
CHANGED
|
@@ -22,7 +22,7 @@ class AGUI(BaseInterface):
|
|
|
22
22
|
if not self.agent and not self.team:
|
|
23
23
|
raise ValueError("AGUI requires an agent and a team")
|
|
24
24
|
|
|
25
|
-
def get_router(self,
|
|
25
|
+
def get_router(self, **kwargs) -> APIRouter:
|
|
26
26
|
# Cannot be overridden
|
|
27
27
|
self.router = APIRouter(tags=["AGUI"])
|
|
28
28
|
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -27,6 +27,7 @@ from agno.models.message import Message
|
|
|
27
27
|
from agno.run.agent import RunContentEvent, RunEvent, RunOutputEvent, RunPausedEvent
|
|
28
28
|
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
29
29
|
from agno.run.team import TeamRunEvent, TeamRunOutputEvent
|
|
30
|
+
from agno.utils.message import get_text_from_message
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass
|
|
@@ -106,17 +107,23 @@ def extract_team_response_chunk_content(response: TeamRunContentEvent) -> str:
|
|
|
106
107
|
members_content.append(f"Team member: {member_content}")
|
|
107
108
|
members_response = "\n".join(members_content) if members_content else ""
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
# Handle structured outputs
|
|
111
|
+
main_content = get_text_from_message(response.content) if response.content is not None else ""
|
|
112
|
+
|
|
113
|
+
return main_content + members_response
|
|
110
114
|
|
|
111
115
|
|
|
112
116
|
def extract_response_chunk_content(response: RunContentEvent) -> str:
|
|
113
117
|
"""Given a response stream chunk, find and extract the content."""
|
|
118
|
+
|
|
114
119
|
if hasattr(response, "messages") and response.messages: # type: ignore
|
|
115
120
|
for msg in reversed(response.messages): # type: ignore
|
|
116
121
|
if hasattr(msg, "role") and msg.role == "assistant" and hasattr(msg, "content") and msg.content:
|
|
117
|
-
|
|
122
|
+
# Handle structured outputs from messages
|
|
123
|
+
return get_text_from_message(msg.content)
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
# Handle structured outputs
|
|
126
|
+
return get_text_from_message(response.content) if response.content is not None else ""
|
|
120
127
|
|
|
121
128
|
|
|
122
129
|
def _create_events_from_chunk(
|
|
@@ -212,11 +219,11 @@ def _create_events_from_chunk(
|
|
|
212
219
|
|
|
213
220
|
# Handle reasoning
|
|
214
221
|
elif chunk.event == RunEvent.reasoning_started:
|
|
215
|
-
|
|
216
|
-
events_to_emit.append(
|
|
222
|
+
step_started_event = StepStartedEvent(type=EventType.STEP_STARTED, step_name="reasoning") # type: ignore
|
|
223
|
+
events_to_emit.append(step_started_event) # type: ignore
|
|
217
224
|
elif chunk.event == RunEvent.reasoning_completed:
|
|
218
|
-
|
|
219
|
-
events_to_emit.append(
|
|
225
|
+
step_started_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning") # type: ignore
|
|
226
|
+
events_to_emit.append(step_started_event) # type: ignore
|
|
220
227
|
|
|
221
228
|
return events_to_emit, message_started # type: ignore
|
|
222
229
|
|
|
@@ -230,7 +237,7 @@ def _create_completion_events(
|
|
|
230
237
|
run_id: str,
|
|
231
238
|
) -> List[BaseEvent]:
|
|
232
239
|
"""Create events for run completion."""
|
|
233
|
-
events_to_emit = []
|
|
240
|
+
events_to_emit: List[BaseEvent] = []
|
|
234
241
|
|
|
235
242
|
# End remaining active tool calls if needed
|
|
236
243
|
for tool_call_id in list(event_buffer.active_tool_call_ids):
|
|
@@ -308,7 +315,7 @@ def _create_completion_events(
|
|
|
308
315
|
|
|
309
316
|
def _emit_event_logic(event: BaseEvent, event_buffer: EventBuffer) -> List[BaseEvent]:
|
|
310
317
|
"""Process an event through the buffer and return events to actually emit."""
|
|
311
|
-
events_to_emit = []
|
|
318
|
+
events_to_emit: List[BaseEvent] = []
|
|
312
319
|
|
|
313
320
|
if event_buffer.is_blocked():
|
|
314
321
|
# Handle events related to the current blocking tool call
|
|
@@ -23,10 +23,9 @@ class Slack(BaseInterface):
|
|
|
23
23
|
if not self.agent and not self.team:
|
|
24
24
|
raise ValueError("Slack requires an agent and a team")
|
|
25
25
|
|
|
26
|
-
def get_router(self,
|
|
26
|
+
def get_router(self, **kwargs) -> APIRouter:
|
|
27
27
|
# Cannot be overridden
|
|
28
|
-
self.
|
|
29
|
-
self.router = APIRouter(prefix=self.router_prefix, tags=["Slack"])
|
|
28
|
+
self.router = APIRouter(prefix="/slack", tags=["Slack"])
|
|
30
29
|
|
|
31
30
|
self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
|
|
32
31
|
|
|
@@ -20,10 +20,9 @@ class Whatsapp(BaseInterface):
|
|
|
20
20
|
if not self.agent and not self.team:
|
|
21
21
|
raise ValueError("Whatsapp requires an agent and a team")
|
|
22
22
|
|
|
23
|
-
def get_router(self,
|
|
23
|
+
def get_router(self, **kwargs) -> APIRouter:
|
|
24
24
|
# Cannot be overridden
|
|
25
|
-
self.
|
|
26
|
-
self.router = APIRouter(prefix=self.router_prefix, tags=["Whatsapp"])
|
|
25
|
+
self.router = APIRouter(prefix="/whatsapp", tags=["Whatsapp"])
|
|
27
26
|
|
|
28
27
|
self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
|
|
29
28
|
|
agno/os/mcp.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Router for MCP interface providing Model Context Protocol endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
from fastmcp.server.http import (
|
|
9
|
+
StarletteWithLifespan,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from agno.db.base import SessionType
|
|
13
|
+
from agno.db.schemas import UserMemory
|
|
14
|
+
from agno.os.routers.memory.schemas import (
|
|
15
|
+
UserMemorySchema,
|
|
16
|
+
)
|
|
17
|
+
from agno.os.schema import (
|
|
18
|
+
AgentSummaryResponse,
|
|
19
|
+
ConfigResponse,
|
|
20
|
+
InterfaceResponse,
|
|
21
|
+
SessionSchema,
|
|
22
|
+
TeamSummaryResponse,
|
|
23
|
+
WorkflowSummaryResponse,
|
|
24
|
+
)
|
|
25
|
+
from agno.os.utils import (
|
|
26
|
+
get_agent_by_id,
|
|
27
|
+
get_db,
|
|
28
|
+
get_team_by_id,
|
|
29
|
+
get_workflow_by_id,
|
|
30
|
+
)
|
|
31
|
+
from agno.run.agent import RunOutput
|
|
32
|
+
from agno.run.team import TeamRunOutput
|
|
33
|
+
from agno.run.workflow import WorkflowRunOutput
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from agno.os.app import AgentOS
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_mcp_server(
|
|
42
|
+
os: "AgentOS",
|
|
43
|
+
) -> StarletteWithLifespan:
|
|
44
|
+
"""Attach MCP routes to the provided router."""
|
|
45
|
+
|
|
46
|
+
# Create an MCP server
|
|
47
|
+
mcp = FastMCP(os.name or "AgentOS")
|
|
48
|
+
|
|
49
|
+
@mcp.tool(
|
|
50
|
+
name="get_agentos_config",
|
|
51
|
+
description="Get the configuration of the AgentOS",
|
|
52
|
+
tags={"core"},
|
|
53
|
+
output_schema=ConfigResponse.model_json_schema(),
|
|
54
|
+
) # type: ignore
|
|
55
|
+
async def config() -> ConfigResponse:
|
|
56
|
+
return ConfigResponse(
|
|
57
|
+
os_id=os.os_id or "AgentOS",
|
|
58
|
+
description=os.description,
|
|
59
|
+
available_models=os.config.available_models if os.config else [],
|
|
60
|
+
databases=[db.id for db in os.dbs.values()],
|
|
61
|
+
chat=os.config.chat if os.config else None,
|
|
62
|
+
session=os._get_session_config(),
|
|
63
|
+
memory=os._get_memory_config(),
|
|
64
|
+
knowledge=os._get_knowledge_config(),
|
|
65
|
+
evals=os._get_evals_config(),
|
|
66
|
+
metrics=os._get_metrics_config(),
|
|
67
|
+
agents=[AgentSummaryResponse.from_agent(agent) for agent in os.agents] if os.agents else [],
|
|
68
|
+
teams=[TeamSummaryResponse.from_team(team) for team in os.teams] if os.teams else [],
|
|
69
|
+
workflows=[WorkflowSummaryResponse.from_workflow(w) for w in os.workflows] if os.workflows else [],
|
|
70
|
+
interfaces=[
|
|
71
|
+
InterfaceResponse(type=interface.type, version=interface.version, route=interface.router_prefix)
|
|
72
|
+
for interface in os.interfaces
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@mcp.tool(name="run_agent", description="Run an agent", tags={"core"}) # type: ignore
|
|
77
|
+
async def run_agent(agent_id: str, message: str) -> RunOutput:
|
|
78
|
+
agent = get_agent_by_id(agent_id, os.agents)
|
|
79
|
+
if agent is None:
|
|
80
|
+
raise Exception(f"Agent {agent_id} not found")
|
|
81
|
+
return agent.run(message)
|
|
82
|
+
|
|
83
|
+
@mcp.tool(name="run_team", description="Run a team", tags={"core"}) # type: ignore
|
|
84
|
+
async def run_team(team_id: str, message: str) -> TeamRunOutput:
|
|
85
|
+
team = get_team_by_id(team_id, os.teams)
|
|
86
|
+
if team is None:
|
|
87
|
+
raise Exception(f"Team {team_id} not found")
|
|
88
|
+
return team.run(message)
|
|
89
|
+
|
|
90
|
+
@mcp.tool(name="run_workflow", description="Run a workflow", tags={"core"}) # type: ignore
|
|
91
|
+
async def run_workflow(workflow_id: str, message: str) -> WorkflowRunOutput:
|
|
92
|
+
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
93
|
+
if workflow is None:
|
|
94
|
+
raise Exception(f"Workflow {workflow_id} not found")
|
|
95
|
+
return workflow.run(message)
|
|
96
|
+
|
|
97
|
+
# Session Management Tools
|
|
98
|
+
@mcp.tool(name="get_sessions_for_agent", description="Get list of sessions for an agent", tags={"session"}) # type: ignore
|
|
99
|
+
async def get_sessions_for_agent(
|
|
100
|
+
agent_id: str,
|
|
101
|
+
db_id: str,
|
|
102
|
+
user_id: Optional[str] = None,
|
|
103
|
+
sort_by: str = "created_at",
|
|
104
|
+
sort_order: str = "desc",
|
|
105
|
+
):
|
|
106
|
+
db = get_db(os.dbs, db_id)
|
|
107
|
+
sessions, _ = db.get_sessions(
|
|
108
|
+
session_type=SessionType.AGENT,
|
|
109
|
+
component_id=agent_id,
|
|
110
|
+
user_id=user_id,
|
|
111
|
+
sort_by=sort_by,
|
|
112
|
+
sort_order=sort_order,
|
|
113
|
+
deserialize=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"data": [SessionSchema.from_dict(session) for session in sessions], # type: ignore
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@mcp.tool(name="get_sessions_for_team", description="Get list of sessions for a team", tags={"session"}) # type: ignore
|
|
121
|
+
async def get_sessions_for_team(
|
|
122
|
+
team_id: str,
|
|
123
|
+
db_id: str,
|
|
124
|
+
user_id: Optional[str] = None,
|
|
125
|
+
sort_by: str = "created_at",
|
|
126
|
+
sort_order: str = "desc",
|
|
127
|
+
):
|
|
128
|
+
db = get_db(os.dbs, db_id)
|
|
129
|
+
sessions, _ = db.get_sessions(
|
|
130
|
+
session_type=SessionType.TEAM,
|
|
131
|
+
component_id=team_id,
|
|
132
|
+
user_id=user_id,
|
|
133
|
+
sort_by=sort_by,
|
|
134
|
+
sort_order=sort_order,
|
|
135
|
+
deserialize=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"data": [SessionSchema.from_dict(session) for session in sessions], # type: ignore
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@mcp.tool(name="get_sessions_for_workflow", description="Get list of sessions for a workflow", tags={"session"}) # type: ignore
|
|
143
|
+
async def get_sessions_for_workflow(
|
|
144
|
+
workflow_id: str,
|
|
145
|
+
db_id: str,
|
|
146
|
+
user_id: Optional[str] = None,
|
|
147
|
+
sort_by: str = "created_at",
|
|
148
|
+
sort_order: str = "desc",
|
|
149
|
+
):
|
|
150
|
+
db = get_db(os.dbs, db_id)
|
|
151
|
+
sessions, _ = db.get_sessions(
|
|
152
|
+
session_type=SessionType.WORKFLOW,
|
|
153
|
+
component_id=workflow_id,
|
|
154
|
+
user_id=user_id,
|
|
155
|
+
sort_by=sort_by,
|
|
156
|
+
sort_order=sort_order,
|
|
157
|
+
deserialize=False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"data": [SessionSchema.from_dict(session) for session in sessions], # type: ignore
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Memory Management Tools
|
|
165
|
+
@mcp.tool(name="create_memory", description="Create a new user memory", tags={"memory"}) # type: ignore
|
|
166
|
+
async def create_memory(
|
|
167
|
+
db_id: str,
|
|
168
|
+
memory: str,
|
|
169
|
+
user_id: str,
|
|
170
|
+
topics: Optional[List[str]] = None,
|
|
171
|
+
) -> UserMemorySchema:
|
|
172
|
+
db = get_db(os.dbs, db_id)
|
|
173
|
+
user_memory = db.upsert_user_memory(
|
|
174
|
+
memory=UserMemory(
|
|
175
|
+
memory_id=str(uuid4()),
|
|
176
|
+
memory=memory,
|
|
177
|
+
topics=topics or [],
|
|
178
|
+
user_id=user_id,
|
|
179
|
+
),
|
|
180
|
+
deserialize=False,
|
|
181
|
+
)
|
|
182
|
+
if not user_memory:
|
|
183
|
+
raise Exception("Failed to create memory")
|
|
184
|
+
|
|
185
|
+
return UserMemorySchema.from_dict(user_memory) # type: ignore
|
|
186
|
+
|
|
187
|
+
@mcp.tool(name="get_memories_for_user", description="Get a list of memories for a user", tags={"memory"}) # type: ignore
|
|
188
|
+
async def get_memories_for_user(
|
|
189
|
+
user_id: str,
|
|
190
|
+
sort_by: str = "updated_at",
|
|
191
|
+
sort_order: str = "desc",
|
|
192
|
+
db_id: Optional[str] = None,
|
|
193
|
+
):
|
|
194
|
+
db = get_db(os.dbs, db_id)
|
|
195
|
+
user_memories, _ = db.get_user_memories(
|
|
196
|
+
user_id=user_id,
|
|
197
|
+
sort_by=sort_by,
|
|
198
|
+
sort_order=sort_order,
|
|
199
|
+
deserialize=False,
|
|
200
|
+
)
|
|
201
|
+
return {
|
|
202
|
+
"data": [UserMemorySchema.from_dict(user_memory) for user_memory in user_memories], # type: ignore
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@mcp.tool(name="update_memory", description="Update a memory", tags={"memory"}) # type: ignore
|
|
206
|
+
async def update_memory(
|
|
207
|
+
db_id: str,
|
|
208
|
+
memory_id: str,
|
|
209
|
+
memory: str,
|
|
210
|
+
user_id: str,
|
|
211
|
+
) -> UserMemorySchema:
|
|
212
|
+
db = get_db(os.dbs, db_id)
|
|
213
|
+
user_memory = db.upsert_user_memory(
|
|
214
|
+
memory=UserMemory(
|
|
215
|
+
memory_id=memory_id,
|
|
216
|
+
memory=memory,
|
|
217
|
+
user_id=user_id,
|
|
218
|
+
),
|
|
219
|
+
deserialize=False,
|
|
220
|
+
)
|
|
221
|
+
if not user_memory:
|
|
222
|
+
raise Exception("Failed to update memory")
|
|
223
|
+
|
|
224
|
+
return UserMemorySchema.from_dict(user_memory) # type: ignore
|
|
225
|
+
|
|
226
|
+
@mcp.tool(name="delete_memory", description="Delete a memory by ID", tags={"memory"}) # type: ignore
|
|
227
|
+
async def delete_memory(
|
|
228
|
+
db_id: str,
|
|
229
|
+
memory_id: str,
|
|
230
|
+
) -> None:
|
|
231
|
+
db = get_db(os.dbs, db_id)
|
|
232
|
+
db.delete_user_memory(memory_id=memory_id)
|
|
233
|
+
|
|
234
|
+
mcp_app = mcp.http_app(path="/mcp")
|
|
235
|
+
return mcp_app
|