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.
- agno/agent/agent.py +12 -0
- agno/db/base.py +8 -4
- agno/db/dynamo/dynamo.py +69 -17
- agno/db/firestore/firestore.py +65 -28
- agno/db/gcs_json/gcs_json_db.py +70 -17
- agno/db/in_memory/in_memory_db.py +85 -14
- agno/db/json/json_db.py +79 -15
- agno/db/mongo/mongo.py +27 -8
- agno/db/mysql/mysql.py +17 -3
- agno/db/postgres/postgres.py +21 -3
- agno/db/redis/redis.py +38 -11
- agno/db/singlestore/singlestore.py +14 -3
- agno/db/sqlite/sqlite.py +34 -46
- agno/knowledge/reader/field_labeled_csv_reader.py +294 -0
- agno/knowledge/reader/pdf_reader.py +28 -52
- agno/knowledge/reader/reader_factory.py +12 -0
- agno/memory/manager.py +12 -4
- agno/models/anthropic/claude.py +4 -1
- agno/models/aws/bedrock.py +52 -112
- agno/models/openrouter/openrouter.py +39 -1
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +74 -0
- agno/os/app.py +76 -32
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +252 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/router.py +12 -0
- agno/os/mcp.py +3 -3
- agno/os/router.py +38 -8
- agno/os/routers/memory/memory.py +5 -3
- agno/os/routers/memory/schemas.py +1 -0
- agno/os/utils.py +37 -10
- agno/team/team.py +12 -0
- agno/tools/file.py +4 -2
- agno/tools/mcp.py +46 -1
- agno/utils/merge_dict.py +22 -1
- agno/utils/streamlit.py +1 -1
- agno/workflow/parallel.py +90 -14
- agno/workflow/step.py +30 -27
- agno/workflow/workflow.py +12 -6
- {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/METADATA +16 -14
- {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/RECORD +46 -39
- {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/WHEEL +0 -0
- {agno-2.1.1.dist-info → agno-2.1.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
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,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
|