agno 2.1.0__py3-none-any.whl → 2.1.2__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 +13 -1
- agno/db/base.py +8 -4
- agno/db/dynamo/dynamo.py +69 -17
- agno/db/firestore/firestore.py +68 -29
- agno/db/gcs_json/gcs_json_db.py +68 -17
- agno/db/in_memory/in_memory_db.py +83 -14
- agno/db/json/json_db.py +79 -15
- agno/db/mongo/mongo.py +92 -74
- 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/db/utils.py +50 -22
- agno/knowledge/knowledge.py +6 -0
- 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/openai/responses.py +1 -1
- agno/os/app.py +24 -30
- agno/os/interfaces/__init__.py +1 -0
- 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/agui.py +21 -5
- agno/os/interfaces/agui/router.py +12 -0
- agno/os/interfaces/base.py +4 -2
- agno/os/interfaces/slack/slack.py +13 -8
- agno/os/interfaces/whatsapp/whatsapp.py +12 -5
- agno/os/mcp.py +1 -1
- agno/os/router.py +39 -9
- agno/os/routers/memory/memory.py +5 -3
- agno/os/routers/memory/schemas.py +1 -0
- agno/os/utils.py +36 -10
- agno/run/base.py +2 -13
- agno/team/team.py +13 -1
- agno/tools/mcp.py +46 -1
- agno/utils/merge_dict.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +1 -1
- agno/workflow/parallel.py +90 -14
- agno/workflow/step.py +30 -27
- agno/workflow/types.py +4 -6
- agno/workflow/workflow.py +5 -3
- {agno-2.1.0.dist-info → agno-2.1.2.dist-info}/METADATA +16 -14
- {agno-2.1.0.dist-info → agno-2.1.2.dist-info}/RECORD +53 -47
- {agno-2.1.0.dist-info → agno-2.1.2.dist-info}/WHEEL +0 -0
- {agno-2.1.0.dist-info → agno-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.0.dist-info → agno-2.1.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|