agno 2.1.1__py3-none-any.whl → 2.1.3__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 (46) hide show
  1. agno/agent/agent.py +12 -0
  2. agno/db/base.py +8 -4
  3. agno/db/dynamo/dynamo.py +69 -17
  4. agno/db/firestore/firestore.py +65 -28
  5. agno/db/gcs_json/gcs_json_db.py +70 -17
  6. agno/db/in_memory/in_memory_db.py +85 -14
  7. agno/db/json/json_db.py +79 -15
  8. agno/db/mongo/mongo.py +27 -8
  9. agno/db/mysql/mysql.py +17 -3
  10. agno/db/postgres/postgres.py +21 -3
  11. agno/db/redis/redis.py +38 -11
  12. agno/db/singlestore/singlestore.py +14 -3
  13. agno/db/sqlite/sqlite.py +34 -46
  14. agno/knowledge/reader/field_labeled_csv_reader.py +294 -0
  15. agno/knowledge/reader/pdf_reader.py +28 -52
  16. agno/knowledge/reader/reader_factory.py +12 -0
  17. agno/memory/manager.py +12 -4
  18. agno/models/anthropic/claude.py +4 -1
  19. agno/models/aws/bedrock.py +52 -112
  20. agno/models/openrouter/openrouter.py +39 -1
  21. agno/models/vertexai/__init__.py +0 -0
  22. agno/models/vertexai/claude.py +74 -0
  23. agno/os/app.py +76 -32
  24. agno/os/interfaces/a2a/__init__.py +3 -0
  25. agno/os/interfaces/a2a/a2a.py +42 -0
  26. agno/os/interfaces/a2a/router.py +252 -0
  27. agno/os/interfaces/a2a/utils.py +924 -0
  28. agno/os/interfaces/agui/router.py +12 -0
  29. agno/os/mcp.py +3 -3
  30. agno/os/router.py +38 -8
  31. agno/os/routers/memory/memory.py +5 -3
  32. agno/os/routers/memory/schemas.py +1 -0
  33. agno/os/utils.py +37 -10
  34. agno/team/team.py +12 -0
  35. agno/tools/file.py +4 -2
  36. agno/tools/mcp.py +46 -1
  37. agno/utils/merge_dict.py +22 -1
  38. agno/utils/streamlit.py +1 -1
  39. agno/workflow/parallel.py +90 -14
  40. agno/workflow/step.py +30 -27
  41. agno/workflow/workflow.py +12 -6
  42. {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/METADATA +16 -14
  43. {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/RECORD +46 -39
  44. {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/WHEEL +0 -0
  45. {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/licenses/LICENSE +0 -0
  46. {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/top_level.txt +0 -0
agno/os/app.py CHANGED
@@ -64,11 +64,34 @@ async def mcp_lifespan(_, mcp_tools):
64
64
  await tool.close()
65
65
 
66
66
 
67
+ def _combine_app_lifespans(lifespans: list) -> Any:
68
+ """Combine multiple FastAPI app lifespan context managers into one."""
69
+ if len(lifespans) == 1:
70
+ return lifespans[0]
71
+
72
+ from contextlib import asynccontextmanager
73
+
74
+ @asynccontextmanager
75
+ async def combined_lifespan(app):
76
+ async def _run_nested(index: int):
77
+ if index >= len(lifespans):
78
+ yield
79
+ return
80
+
81
+ async with lifespans[index](app):
82
+ async for _ in _run_nested(index + 1):
83
+ yield
84
+
85
+ async for _ in _run_nested(0):
86
+ yield
87
+
88
+ return combined_lifespan
89
+
90
+
67
91
  class AgentOS:
68
92
  def __init__(
69
93
  self,
70
94
  id: Optional[str] = None,
71
- os_id: Optional[str] = None, # Deprecated
72
95
  name: Optional[str] = None,
73
96
  description: Optional[str] = None,
74
97
  version: Optional[str] = None,
@@ -76,16 +99,18 @@ class AgentOS:
76
99
  teams: Optional[List[Team]] = None,
77
100
  workflows: Optional[List[Workflow]] = None,
78
101
  interfaces: Optional[List[BaseInterface]] = None,
102
+ a2a_interface: bool = False,
79
103
  config: Optional[Union[str, AgentOSConfig]] = None,
80
104
  settings: Optional[AgnoAPISettings] = None,
81
105
  lifespan: Optional[Any] = None,
82
- enable_mcp: bool = False, # Deprecated
83
106
  enable_mcp_server: bool = False,
84
- fastapi_app: Optional[FastAPI] = None, # Deprecated
85
107
  base_app: Optional[FastAPI] = None,
86
- replace_routes: Optional[bool] = None, # Deprecated
87
108
  on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
88
109
  telemetry: bool = True,
110
+ os_id: Optional[str] = None, # Deprecated
111
+ enable_mcp: bool = False, # Deprecated
112
+ fastapi_app: Optional[FastAPI] = None, # Deprecated
113
+ replace_routes: Optional[bool] = None, # Deprecated
89
114
  ):
90
115
  """Initialize AgentOS.
91
116
 
@@ -98,6 +123,7 @@ class AgentOS:
98
123
  teams: List of teams to include in the OS
99
124
  workflows: List of workflows to include in the OS
100
125
  interfaces: List of interfaces to include in the OS
126
+ a2a_interface: Whether to expose the OS agents and teams in an A2A server
101
127
  config: Configuration file path or AgentOSConfig instance
102
128
  settings: API settings for the OS
103
129
  lifespan: Optional lifespan context manager for the FastAPI app
@@ -105,6 +131,7 @@ class AgentOS:
105
131
  base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
106
132
  on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
107
133
  telemetry: Whether to enable telemetry
134
+
108
135
  """
109
136
  if not agents and not workflows and not teams:
110
137
  raise ValueError("Either agents, teams or workflows must be provided.")
@@ -115,6 +142,7 @@ class AgentOS:
115
142
  self.workflows: Optional[List[Workflow]] = workflows
116
143
  self.teams: Optional[List[Team]] = teams
117
144
  self.interfaces = interfaces or []
145
+ self.a2a_interface = a2a_interface
118
146
 
119
147
  self.settings: AgnoAPISettings = settings or AgnoAPISettings()
120
148
 
@@ -216,7 +244,7 @@ class AgentOS:
216
244
  async with mcp_tools_lifespan(app): # type: ignore
217
245
  yield
218
246
 
219
- app_lifespan = combined_lifespan # type: ignore
247
+ app_lifespan = combined_lifespan
220
248
  else:
221
249
  app_lifespan = mcp_tools_lifespan
222
250
 
@@ -233,6 +261,32 @@ class AgentOS:
233
261
  def get_app(self) -> FastAPI:
234
262
  if self.base_app:
235
263
  fastapi_app = self.base_app
264
+
265
+ # Initialize MCP server if enabled
266
+ if self.enable_mcp_server:
267
+ from agno.os.mcp import get_mcp_server
268
+
269
+ self._mcp_app = get_mcp_server(self)
270
+
271
+ # Collect all lifespans that need to be combined
272
+ lifespans = []
273
+
274
+ if fastapi_app.router.lifespan_context:
275
+ lifespans.append(fastapi_app.router.lifespan_context)
276
+
277
+ if self.mcp_tools:
278
+ lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
279
+
280
+ if self.enable_mcp_server and self._mcp_app:
281
+ lifespans.append(self._mcp_app.lifespan)
282
+
283
+ if self.lifespan:
284
+ lifespans.append(self.lifespan)
285
+
286
+ # Combine lifespans and set them in the app
287
+ if lifespans:
288
+ fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
289
+
236
290
  else:
237
291
  if self.enable_mcp_server:
238
292
  from contextlib import asynccontextmanager
@@ -251,7 +305,7 @@ class AgentOS:
251
305
  async with self._mcp_app.lifespan(app): # type: ignore
252
306
  yield
253
307
 
254
- final_lifespan = combined_lifespan # type: ignore
308
+ final_lifespan = combined_lifespan
255
309
 
256
310
  fastapi_app = self._make_app(lifespan=final_lifespan)
257
311
  else:
@@ -263,10 +317,21 @@ class AgentOS:
263
317
  self._add_router(fastapi_app, get_health_router())
264
318
  self._add_router(fastapi_app, get_home_router(self))
265
319
 
320
+ has_a2a_interface = False
266
321
  for interface in self.interfaces:
322
+ if not has_a2a_interface and interface.__class__.__name__ == "A2A":
323
+ has_a2a_interface = True
267
324
  interface_router = interface.get_router()
268
325
  self._add_router(fastapi_app, interface_router)
269
326
 
327
+ # Add A2A interface if requested and not provided in self.interfaces
328
+ if self.a2a_interface and not has_a2a_interface:
329
+ from agno.os.interfaces.a2a import A2A
330
+
331
+ a2a_interface = A2A(agents=self.agents, teams=self.teams, workflows=self.workflows)
332
+ self.interfaces.append(a2a_interface)
333
+ self._add_router(fastapi_app, a2a_interface.get_router())
334
+
270
335
  self._auto_discover_databases()
271
336
  self._auto_discover_knowledge_instances()
272
337
 
@@ -400,18 +465,12 @@ class AgentOS:
400
465
  self._register_db_with_validation(dbs, agent.db)
401
466
  if agent.knowledge and agent.knowledge.contents_db:
402
467
  self._register_db_with_validation(knowledge_dbs, agent.knowledge.contents_db)
403
- # Also add to general dbs if it's used for both purposes
404
- if agent.knowledge.contents_db.id not in dbs:
405
- self._register_db_with_validation(dbs, agent.knowledge.contents_db)
406
468
 
407
469
  for team in self.teams or []:
408
470
  if team.db:
409
471
  self._register_db_with_validation(dbs, team.db)
410
472
  if team.knowledge and team.knowledge.contents_db:
411
473
  self._register_db_with_validation(knowledge_dbs, team.knowledge.contents_db)
412
- # Also add to general dbs if it's used for both purposes
413
- if team.knowledge.contents_db.id not in dbs:
414
- self._register_db_with_validation(dbs, team.knowledge.contents_db)
415
474
 
416
475
  for workflow in self.workflows or []:
417
476
  if workflow.db:
@@ -488,7 +547,6 @@ class AgentOS:
488
547
  if session_config.dbs is None:
489
548
  session_config.dbs = []
490
549
 
491
- multiple_dbs: bool = len(self.dbs.keys()) > 1
492
550
  dbs_with_specific_config = [db.db_id for db in session_config.dbs]
493
551
 
494
552
  for db_id in self.dbs.keys():
@@ -496,9 +554,7 @@ class AgentOS:
496
554
  session_config.dbs.append(
497
555
  DatabaseConfig(
498
556
  db_id=db_id,
499
- domain_config=SessionDomainConfig(
500
- display_name="Sessions" if not multiple_dbs else "Sessions in database '" + db_id + "'"
501
- ),
557
+ domain_config=SessionDomainConfig(display_name=db_id),
502
558
  )
503
559
  )
504
560
 
@@ -510,7 +566,6 @@ class AgentOS:
510
566
  if memory_config.dbs is None:
511
567
  memory_config.dbs = []
512
568
 
513
- multiple_dbs: bool = len(self.dbs.keys()) > 1
514
569
  dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
515
570
 
516
571
  for db_id in self.dbs.keys():
@@ -518,9 +573,7 @@ class AgentOS:
518
573
  memory_config.dbs.append(
519
574
  DatabaseConfig(
520
575
  db_id=db_id,
521
- domain_config=MemoryDomainConfig(
522
- display_name="Memory" if not multiple_dbs else "Memory in database '" + db_id + "'"
523
- ),
576
+ domain_config=MemoryDomainConfig(display_name=db_id),
524
577
  )
525
578
  )
526
579
 
@@ -532,7 +585,6 @@ class AgentOS:
532
585
  if knowledge_config.dbs is None:
533
586
  knowledge_config.dbs = []
534
587
 
535
- multiple_knowledge_dbs: bool = len(self.knowledge_dbs.keys()) > 1
536
588
  dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
537
589
 
538
590
  # Only add databases that are actually used for knowledge contents
@@ -541,9 +593,7 @@ class AgentOS:
541
593
  knowledge_config.dbs.append(
542
594
  DatabaseConfig(
543
595
  db_id=db_id,
544
- domain_config=KnowledgeDomainConfig(
545
- display_name="Knowledge" if not multiple_knowledge_dbs else "Knowledge in database " + db_id
546
- ),
596
+ domain_config=KnowledgeDomainConfig(display_name=db_id),
547
597
  )
548
598
  )
549
599
 
@@ -555,7 +605,6 @@ class AgentOS:
555
605
  if metrics_config.dbs is None:
556
606
  metrics_config.dbs = []
557
607
 
558
- multiple_dbs: bool = len(self.dbs.keys()) > 1
559
608
  dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
560
609
 
561
610
  for db_id in self.dbs.keys():
@@ -563,9 +612,7 @@ class AgentOS:
563
612
  metrics_config.dbs.append(
564
613
  DatabaseConfig(
565
614
  db_id=db_id,
566
- domain_config=MetricsDomainConfig(
567
- display_name="Metrics" if not multiple_dbs else "Metrics in database '" + db_id + "'"
568
- ),
615
+ domain_config=MetricsDomainConfig(display_name=db_id),
569
616
  )
570
617
  )
571
618
 
@@ -577,7 +624,6 @@ class AgentOS:
577
624
  if evals_config.dbs is None:
578
625
  evals_config.dbs = []
579
626
 
580
- multiple_dbs: bool = len(self.dbs.keys()) > 1
581
627
  dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
582
628
 
583
629
  for db_id in self.dbs.keys():
@@ -585,9 +631,7 @@ class AgentOS:
585
631
  evals_config.dbs.append(
586
632
  DatabaseConfig(
587
633
  db_id=db_id,
588
- domain_config=EvalsDomainConfig(
589
- display_name="Evals" if not multiple_dbs else "Evals in database '" + db_id + "'"
590
- ),
634
+ domain_config=EvalsDomainConfig(display_name=db_id),
591
635
  )
592
636
  )
593
637
 
@@ -0,0 +1,3 @@
1
+ from agno.os.interfaces.a2a.a2a import A2A
2
+
3
+ __all__ = ["A2A"]
@@ -0,0 +1,42 @@
1
+ """Main class for the A2A app, used to expose an Agno Agent, Team, or Workflow in an A2A compatible format."""
2
+
3
+ from typing import Optional
4
+
5
+ from fastapi.routing import APIRouter
6
+ from typing_extensions import List
7
+
8
+ from agno.agent import Agent
9
+ from agno.os.interfaces.a2a.router import attach_routes
10
+ from agno.os.interfaces.base import BaseInterface
11
+ from agno.team import Team
12
+ from agno.workflow import Workflow
13
+
14
+
15
+ class A2A(BaseInterface):
16
+ type = "a2a"
17
+
18
+ router: APIRouter
19
+
20
+ def __init__(
21
+ self,
22
+ agents: Optional[List[Agent]] = None,
23
+ teams: Optional[List[Team]] = None,
24
+ workflows: Optional[List[Workflow]] = None,
25
+ prefix: str = "/a2a",
26
+ tags: Optional[List[str]] = None,
27
+ ):
28
+ self.agents = agents
29
+ self.teams = teams
30
+ self.workflows = workflows
31
+ self.prefix = prefix
32
+ self.tags = tags or ["A2A"]
33
+
34
+ if not (self.agents or self.teams or self.workflows):
35
+ raise ValueError("Agents, Teams, or Workflows are required to setup the A2A interface.")
36
+
37
+ def get_router(self, **kwargs) -> APIRouter:
38
+ self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
39
+
40
+ self.router = attach_routes(router=self.router, agents=self.agents, teams=self.teams, workflows=self.workflows)
41
+
42
+ return self.router
@@ -0,0 +1,252 @@
1
+ """Async router handling exposing an Agno Agent or Team in an A2A compatible format."""
2
+
3
+ from typing import Optional, Union
4
+ from uuid import uuid4
5
+
6
+ from fastapi import HTTPException, Request
7
+ from fastapi.responses import StreamingResponse
8
+ from fastapi.routing import APIRouter
9
+ from typing_extensions import List
10
+
11
+ try:
12
+ from a2a.types import SendMessageSuccessResponse, Task, TaskState, TaskStatus
13
+ except ImportError as e:
14
+ raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a`") from e
15
+
16
+ from agno.agent import Agent
17
+ from agno.os.interfaces.a2a.utils import (
18
+ map_a2a_request_to_run_input,
19
+ map_run_output_to_a2a_task,
20
+ stream_a2a_response_with_error_handling,
21
+ )
22
+ from agno.os.router import _get_request_kwargs
23
+ from agno.os.utils import get_agent_by_id, get_team_by_id, get_workflow_by_id
24
+ from agno.team import Team
25
+ from agno.workflow import Workflow
26
+
27
+
28
+ def attach_routes(
29
+ router: APIRouter,
30
+ agents: Optional[List[Agent]] = None,
31
+ teams: Optional[List[Team]] = None,
32
+ workflows: Optional[List[Workflow]] = None,
33
+ ) -> APIRouter:
34
+ if agents is None and teams is None and workflows is None:
35
+ raise ValueError("Agents, Teams, or Workflows are required to setup the A2A interface.")
36
+
37
+ @router.post(
38
+ "/message/send",
39
+ tags=["A2A"],
40
+ operation_id="send_message",
41
+ summary="Send message to Agent, Team, or Workflow (A2A Protocol)",
42
+ description="Send a message to an Agno Agent, Team, or Workflow. "
43
+ "The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
44
+ "Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
45
+ response_model_exclude_none=True,
46
+ responses={
47
+ 200: {
48
+ "description": "Message sent successfully",
49
+ "content": {
50
+ "application/json": {
51
+ "example": {
52
+ "jsonrpc": "2.0",
53
+ "id": "request-123",
54
+ "result": {
55
+ "task": {
56
+ "id": "task-456",
57
+ "context_id": "context-789",
58
+ "status": "completed",
59
+ "history": [
60
+ {
61
+ "message_id": "msg-1",
62
+ "role": "agent",
63
+ "parts": [{"kind": "text", "text": "Response from agent"}],
64
+ }
65
+ ],
66
+ }
67
+ },
68
+ }
69
+ }
70
+ },
71
+ },
72
+ 400: {"description": "Invalid request or unsupported method"},
73
+ 404: {"description": "Agent, Team, or Workflow not found"},
74
+ },
75
+ response_model=SendMessageSuccessResponse,
76
+ )
77
+ async def a2a_send_message(request: Request):
78
+ request_body = await request.json()
79
+ kwargs = await _get_request_kwargs(request, a2a_send_message)
80
+
81
+ # 1. Get the Agent, Team, or Workflow to run
82
+ agent_id = request_body.get("params", {}).get("message", {}).get("agentId") or request.headers.get("X-Agent-ID")
83
+ if not agent_id:
84
+ raise HTTPException(
85
+ status_code=400,
86
+ detail="Entity ID required. Provide it via 'agentId' in params.message or 'X-Agent-ID' header.",
87
+ )
88
+ entity: Optional[Union[Agent, Team, Workflow]] = None
89
+ if agents:
90
+ entity = get_agent_by_id(agent_id, agents)
91
+ if not entity and teams:
92
+ entity = get_team_by_id(agent_id, teams)
93
+ if not entity and workflows:
94
+ entity = get_workflow_by_id(agent_id, workflows)
95
+ if entity is None:
96
+ raise HTTPException(status_code=404, detail=f"Agent, Team, or Workflow with ID '{agent_id}' not found")
97
+
98
+ # 2. Map the request to our run_input and run variables
99
+ run_input = await map_a2a_request_to_run_input(request_body, stream=False)
100
+ context_id = request_body.get("params", {}).get("message", {}).get("contextId")
101
+ user_id = request.headers.get("X-User-ID")
102
+ if not user_id:
103
+ user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
104
+
105
+ # 3. Run the agent, team, or workflow
106
+ try:
107
+ if isinstance(entity, Workflow):
108
+ response = await entity.arun(
109
+ input=run_input.input_content,
110
+ images=list(run_input.images) if run_input.images else None,
111
+ videos=list(run_input.videos) if run_input.videos else None,
112
+ audio=list(run_input.audios) if run_input.audios else None,
113
+ files=list(run_input.files) if run_input.files else None,
114
+ session_id=context_id,
115
+ user_id=user_id,
116
+ **kwargs,
117
+ )
118
+ else:
119
+ response = await entity.arun(
120
+ input=run_input.input_content,
121
+ images=run_input.images,
122
+ videos=run_input.videos,
123
+ audio=run_input.audios,
124
+ files=run_input.files,
125
+ session_id=context_id,
126
+ user_id=user_id,
127
+ **kwargs,
128
+ )
129
+
130
+ # 4. Send the response
131
+ a2a_task = map_run_output_to_a2a_task(response)
132
+ return SendMessageSuccessResponse(
133
+ id=request_body.get("id", "unknown"),
134
+ result=a2a_task,
135
+ )
136
+
137
+ # Handle all critical errors
138
+ except Exception as e:
139
+ from a2a.types import Message as A2AMessage
140
+ from a2a.types import Part, Role, TextPart
141
+
142
+ error_message = A2AMessage(
143
+ message_id=str(uuid4()),
144
+ role=Role.agent,
145
+ parts=[Part(root=TextPart(text=f"Error: {str(e)}"))],
146
+ context_id=context_id or str(uuid4()),
147
+ )
148
+ failed_task = Task(
149
+ id=str(uuid4()),
150
+ context_id=context_id or str(uuid4()),
151
+ status=TaskStatus(state=TaskState.failed),
152
+ history=[error_message],
153
+ )
154
+
155
+ return SendMessageSuccessResponse(
156
+ id=request_body.get("id", "unknown"),
157
+ result=failed_task,
158
+ )
159
+
160
+ @router.post(
161
+ "/message/stream",
162
+ tags=["A2A"],
163
+ operation_id="stream_message",
164
+ summary="Stream message to Agent, Team, or Workflow (A2A Protocol)",
165
+ description="Stream a message to an Agno Agent, Team, or Workflow."
166
+ "The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
167
+ "Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
168
+ "Returns real-time updates as newline-delimited JSON (NDJSON).",
169
+ response_model_exclude_none=True,
170
+ responses={
171
+ 200: {
172
+ "description": "Streaming response with task updates",
173
+ "content": {
174
+ "application/x-ndjson": {
175
+ "example": '{"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n'
176
+ '{"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n'
177
+ }
178
+ },
179
+ },
180
+ 400: {"description": "Invalid request or unsupported method"},
181
+ 404: {"description": "Agent, Team, or Workflow not found"},
182
+ },
183
+ )
184
+ async def a2a_stream_message(request: Request):
185
+ request_body = await request.json()
186
+ kwargs = await _get_request_kwargs(request, a2a_stream_message)
187
+
188
+ # 1. Get the Agent, Team, or Workflow to run
189
+ agent_id = request_body.get("params", {}).get("message", {}).get("agentId")
190
+ if not agent_id:
191
+ agent_id = request.headers.get("X-Agent-ID")
192
+ if not agent_id:
193
+ raise HTTPException(
194
+ status_code=400,
195
+ detail="Entity ID required. Provide 'agentId' in params.message or 'X-Agent-ID' header.",
196
+ )
197
+ entity: Optional[Union[Agent, Team, Workflow]] = None
198
+ if agents:
199
+ entity = get_agent_by_id(agent_id, agents)
200
+ if not entity and teams:
201
+ entity = get_team_by_id(agent_id, teams)
202
+ if not entity and workflows:
203
+ entity = get_workflow_by_id(agent_id, workflows)
204
+ if entity is None:
205
+ raise HTTPException(status_code=404, detail=f"Agent, Team, or Workflow with ID '{agent_id}' not found")
206
+
207
+ # 2. Map the request to our run_input and run variables
208
+ run_input = await map_a2a_request_to_run_input(request_body, stream=True)
209
+ context_id = request_body.get("params", {}).get("message", {}).get("contextId")
210
+ user_id = request.headers.get("X-User-ID")
211
+ if not user_id:
212
+ user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
213
+
214
+ # 3. Run the Agent, Team, or Workflow and stream the response
215
+ try:
216
+ if isinstance(entity, Workflow):
217
+ event_stream = entity.arun(
218
+ input=run_input.input_content,
219
+ images=list(run_input.images) if run_input.images else None,
220
+ videos=list(run_input.videos) if run_input.videos else None,
221
+ audio=list(run_input.audios) if run_input.audios else None,
222
+ files=list(run_input.files) if run_input.files else None,
223
+ session_id=context_id,
224
+ user_id=user_id,
225
+ stream=True,
226
+ stream_intermediate_steps=True,
227
+ **kwargs,
228
+ )
229
+ else:
230
+ event_stream = entity.arun( # type: ignore[assignment]
231
+ input=run_input.input_content,
232
+ images=run_input.images,
233
+ videos=run_input.videos,
234
+ audio=run_input.audios,
235
+ files=run_input.files,
236
+ session_id=context_id,
237
+ user_id=user_id,
238
+ stream=True,
239
+ stream_intermediate_steps=True,
240
+ **kwargs,
241
+ )
242
+
243
+ # 4. Stream the response
244
+ return StreamingResponse(
245
+ stream_a2a_response_with_error_handling(event_stream=event_stream, request_id=request_body["id"]), # type: ignore[arg-type]
246
+ media_type="application/x-ndjson",
247
+ )
248
+
249
+ except Exception as e:
250
+ raise HTTPException(status_code=500, detail=f"Failed to start run: {str(e)}")
251
+
252
+ return router