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.
Files changed (79) hide show
  1. agno/agent/agent.py +416 -41
  2. agno/api/agent.py +2 -2
  3. agno/api/evals.py +2 -2
  4. agno/api/os.py +1 -1
  5. agno/api/settings.py +2 -2
  6. agno/api/team.py +2 -2
  7. agno/db/dynamo/dynamo.py +0 -6
  8. agno/db/firestore/firestore.py +0 -6
  9. agno/db/in_memory/in_memory_db.py +0 -6
  10. agno/db/json/json_db.py +0 -6
  11. agno/db/mongo/mongo.py +8 -9
  12. agno/db/mysql/utils.py +0 -1
  13. agno/db/postgres/postgres.py +0 -10
  14. agno/db/postgres/utils.py +0 -1
  15. agno/db/redis/redis.py +0 -4
  16. agno/db/singlestore/singlestore.py +0 -10
  17. agno/db/singlestore/utils.py +0 -1
  18. agno/db/sqlite/sqlite.py +0 -4
  19. agno/db/sqlite/utils.py +0 -1
  20. agno/eval/accuracy.py +12 -5
  21. agno/integrations/discord/client.py +5 -1
  22. agno/knowledge/chunking/strategy.py +14 -14
  23. agno/knowledge/embedder/aws_bedrock.py +2 -2
  24. agno/knowledge/knowledge.py +156 -120
  25. agno/knowledge/reader/arxiv_reader.py +5 -5
  26. agno/knowledge/reader/csv_reader.py +6 -77
  27. agno/knowledge/reader/docx_reader.py +5 -5
  28. agno/knowledge/reader/firecrawl_reader.py +5 -5
  29. agno/knowledge/reader/json_reader.py +5 -5
  30. agno/knowledge/reader/markdown_reader.py +31 -9
  31. agno/knowledge/reader/pdf_reader.py +10 -123
  32. agno/knowledge/reader/reader_factory.py +65 -72
  33. agno/knowledge/reader/s3_reader.py +44 -114
  34. agno/knowledge/reader/text_reader.py +5 -5
  35. agno/knowledge/reader/url_reader.py +75 -31
  36. agno/knowledge/reader/web_search_reader.py +6 -29
  37. agno/knowledge/reader/website_reader.py +5 -5
  38. agno/knowledge/reader/wikipedia_reader.py +5 -5
  39. agno/knowledge/reader/youtube_reader.py +6 -6
  40. agno/knowledge/utils.py +10 -10
  41. agno/models/anthropic/claude.py +2 -49
  42. agno/models/aws/bedrock.py +3 -7
  43. agno/models/base.py +37 -6
  44. agno/models/message.py +7 -6
  45. agno/os/app.py +168 -64
  46. agno/os/interfaces/agui/agui.py +1 -1
  47. agno/os/interfaces/agui/utils.py +16 -9
  48. agno/os/interfaces/slack/slack.py +2 -3
  49. agno/os/interfaces/whatsapp/whatsapp.py +2 -3
  50. agno/os/mcp.py +235 -0
  51. agno/os/router.py +576 -19
  52. agno/os/routers/evals/evals.py +201 -12
  53. agno/os/routers/knowledge/knowledge.py +455 -18
  54. agno/os/routers/memory/memory.py +260 -29
  55. agno/os/routers/metrics/metrics.py +127 -7
  56. agno/os/routers/session/session.py +398 -25
  57. agno/os/schema.py +55 -2
  58. agno/os/settings.py +0 -1
  59. agno/run/agent.py +96 -2
  60. agno/run/cancel.py +0 -2
  61. agno/run/team.py +93 -2
  62. agno/run/workflow.py +25 -12
  63. agno/team/team.py +863 -1053
  64. agno/tools/function.py +65 -7
  65. agno/tools/linear.py +1 -1
  66. agno/tools/mcp.py +1 -2
  67. agno/utils/gemini.py +31 -1
  68. agno/utils/log.py +52 -2
  69. agno/utils/mcp.py +55 -3
  70. agno/utils/models/claude.py +41 -0
  71. agno/utils/print_response/team.py +177 -73
  72. agno/utils/streamlit.py +481 -0
  73. agno/workflow/workflow.py +17 -1
  74. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/METADATA +1 -1
  75. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/RECORD +78 -77
  76. agno/knowledge/reader/gcs_reader.py +0 -67
  77. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/WHEEL +0 -0
  78. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  79. {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, Tuple, Union
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
- class AgentOS:
41
- host_url: Optional[str] = None
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
- self.fastapi_app: Optional[FastAPI] = fastapi_app
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.interfaces_loaded: List[Tuple[str, str]] = []
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(display_name="Evals" if not multiple_dbs else "Evals " + db_id),
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)
@@ -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, use_async: bool = True, **kwargs) -> APIRouter:
25
+ def get_router(self, **kwargs) -> APIRouter:
26
26
  # Cannot be overridden
27
27
  self.router = APIRouter(tags=["AGUI"])
28
28
 
@@ -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
- return str(response.content) + members_response
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
- return str(msg.content)
122
+ # Handle structured outputs from messages
123
+ return get_text_from_message(msg.content)
118
124
 
119
- return str(response.content) if response.content else ""
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
- step_event = StepStartedEvent(type=EventType.STEP_STARTED, step_name="reasoning") # type: ignore
216
- events_to_emit.append(step_event) # type: ignore
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
- step_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning") # type: ignore
219
- events_to_emit.append(step_event) # type: ignore
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, use_async: bool = True, **kwargs) -> APIRouter:
26
+ def get_router(self, **kwargs) -> APIRouter:
27
27
  # Cannot be overridden
28
- self.router_prefix = "/slack"
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, use_async: bool = True, **kwargs) -> APIRouter:
23
+ def get_router(self, **kwargs) -> APIRouter:
24
24
  # Cannot be overridden
25
- self.router_prefix = "/whatsapp"
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