agno 2.3.12__py3-none-any.whl → 2.3.14__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 +1125 -1401
- agno/eval/__init__.py +21 -8
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/models/anthropic/claude.py +4 -1
- agno/models/azure/openai_chat.py +11 -5
- agno/models/base.py +8 -4
- agno/models/openai/chat.py +0 -2
- agno/models/openai/responses.py +2 -2
- agno/os/app.py +112 -5
- agno/os/auth.py +190 -3
- agno/os/config.py +9 -0
- agno/os/interfaces/a2a/router.py +619 -9
- agno/os/interfaces/a2a/utils.py +31 -32
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +670 -108
- agno/os/router.py +0 -1
- agno/os/routers/agents/router.py +22 -4
- agno/os/routers/agents/schema.py +14 -1
- agno/os/routers/teams/router.py +20 -4
- agno/os/routers/teams/schema.py +14 -1
- agno/os/routers/workflows/router.py +88 -9
- agno/os/scopes.py +469 -0
- agno/os/utils.py +86 -53
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +91 -1
- agno/reasoning/gemini.py +81 -1
- agno/reasoning/groq.py +103 -1
- agno/reasoning/manager.py +1244 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +113 -1
- agno/reasoning/vertexai.py +85 -1
- agno/run/agent.py +11 -0
- agno/run/base.py +1 -1
- agno/run/team.py +11 -0
- agno/session/team.py +0 -3
- agno/team/team.py +1204 -1452
- agno/tools/postgres.py +1 -1
- agno/utils/cryptography.py +22 -0
- agno/utils/events.py +69 -2
- agno/utils/hooks.py +4 -10
- agno/utils/print_response/agent.py +52 -2
- agno/utils/print_response/team.py +141 -10
- agno/utils/prompts.py +8 -6
- agno/utils/string.py +46 -0
- agno/utils/team.py +1 -1
- agno/vectordb/chroma/chromadb.py +1 -0
- agno/vectordb/milvus/milvus.py +32 -3
- agno/vectordb/redis/redisdb.py +16 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/os/interfaces/a2a/router.py
CHANGED
|
@@ -9,10 +9,20 @@ from fastapi.routing import APIRouter
|
|
|
9
9
|
from typing_extensions import List
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
|
-
from a2a.types import
|
|
12
|
+
from a2a.types import (
|
|
13
|
+
AgentCapabilities,
|
|
14
|
+
AgentCard,
|
|
15
|
+
AgentSkill,
|
|
16
|
+
SendMessageSuccessResponse,
|
|
17
|
+
Task,
|
|
18
|
+
TaskState,
|
|
19
|
+
TaskStatus,
|
|
20
|
+
)
|
|
13
21
|
except ImportError as e:
|
|
14
22
|
raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a-sdk`") from e
|
|
15
23
|
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
16
26
|
from agno.agent import Agent
|
|
17
27
|
from agno.os.interfaces.a2a.utils import (
|
|
18
28
|
map_a2a_request_to_run_input,
|
|
@@ -33,11 +43,599 @@ def attach_routes(
|
|
|
33
43
|
if agents is None and teams is None and workflows is None:
|
|
34
44
|
raise ValueError("Agents, Teams, or Workflows are required to setup the A2A interface.")
|
|
35
45
|
|
|
46
|
+
# ============= AGENTS =============
|
|
47
|
+
@router.get("/agents/{id}/.well-known/agent-card.json")
|
|
48
|
+
async def get_agent_card(request: Request, id: str):
|
|
49
|
+
agent = get_agent_by_id(id, agents)
|
|
50
|
+
if not agent:
|
|
51
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
52
|
+
|
|
53
|
+
base_url = str(request.base_url).rstrip("/")
|
|
54
|
+
skill = AgentSkill(
|
|
55
|
+
id=agent.id or "",
|
|
56
|
+
name=agent.name or "",
|
|
57
|
+
description=agent.description or "",
|
|
58
|
+
tags=["agno"],
|
|
59
|
+
examples=["search", "ok"],
|
|
60
|
+
output_modes=["application/json"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return AgentCard(
|
|
64
|
+
name=agent.name or "",
|
|
65
|
+
version="1.0.0",
|
|
66
|
+
description=agent.description or "",
|
|
67
|
+
url=f"{base_url}/a2a/agents/{agent.id}/v1/message:stream",
|
|
68
|
+
default_input_modes=["text"],
|
|
69
|
+
default_output_modes=["text"],
|
|
70
|
+
capabilities=AgentCapabilities(streaming=True, push_notifications=False, state_transition_history=False),
|
|
71
|
+
skills=[skill],
|
|
72
|
+
supports_authenticated_extended_card=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@router.post(
|
|
76
|
+
"/agents/{id}/v1/message:send",
|
|
77
|
+
operation_id="run_message_agent",
|
|
78
|
+
name="run_message_agent",
|
|
79
|
+
description="Send a message to an Agno Agent (non-streaming). The Agent is identified via the path parameter '{id}'. "
|
|
80
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
|
|
81
|
+
response_model_exclude_none=True,
|
|
82
|
+
responses={
|
|
83
|
+
200: {
|
|
84
|
+
"description": "Message sent successfully",
|
|
85
|
+
"content": {
|
|
86
|
+
"application/json": {
|
|
87
|
+
"example": {
|
|
88
|
+
"jsonrpc": "2.0",
|
|
89
|
+
"id": "request-123",
|
|
90
|
+
"result": {
|
|
91
|
+
"task": {
|
|
92
|
+
"id": "task-456",
|
|
93
|
+
"context_id": "context-789",
|
|
94
|
+
"status": "completed",
|
|
95
|
+
"history": [
|
|
96
|
+
{
|
|
97
|
+
"message_id": "msg-1",
|
|
98
|
+
"role": "agent",
|
|
99
|
+
"parts": [{"kind": "text", "text": "Response from agent"}],
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
400: {"description": "Invalid request"},
|
|
109
|
+
404: {"description": "Agent not found"},
|
|
110
|
+
},
|
|
111
|
+
response_model=SendMessageSuccessResponse,
|
|
112
|
+
)
|
|
113
|
+
async def a2a_run_agent(request: Request, id: str):
|
|
114
|
+
if not agents:
|
|
115
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
116
|
+
|
|
117
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
118
|
+
request_body = await request.json()
|
|
119
|
+
kwargs = await get_request_kwargs(request, a2a_run_agent)
|
|
120
|
+
|
|
121
|
+
# 1. Get the Agent to run
|
|
122
|
+
agent = get_agent_by_id(id, agents)
|
|
123
|
+
if not agent:
|
|
124
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
125
|
+
|
|
126
|
+
# 2. Map the request to our run_input and run variables
|
|
127
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=False)
|
|
128
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
129
|
+
user_id = request.headers.get("X-User-ID")
|
|
130
|
+
if not user_id:
|
|
131
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
132
|
+
|
|
133
|
+
# 3. Run the Agent
|
|
134
|
+
try:
|
|
135
|
+
response = await agent.arun(
|
|
136
|
+
input=run_input.input_content,
|
|
137
|
+
images=run_input.images,
|
|
138
|
+
videos=run_input.videos,
|
|
139
|
+
audio=run_input.audios,
|
|
140
|
+
files=run_input.files,
|
|
141
|
+
session_id=context_id,
|
|
142
|
+
user_id=user_id,
|
|
143
|
+
**kwargs,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# 4. Send the response
|
|
147
|
+
a2a_task = map_run_output_to_a2a_task(response)
|
|
148
|
+
return SendMessageSuccessResponse(
|
|
149
|
+
id=request_body.get("id", "unknown"),
|
|
150
|
+
result=a2a_task,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Handle any critical error
|
|
154
|
+
except Exception as e:
|
|
155
|
+
from a2a.types import Message as A2AMessage
|
|
156
|
+
from a2a.types import Part, Role, TextPart
|
|
157
|
+
|
|
158
|
+
error_message = A2AMessage(
|
|
159
|
+
message_id=str(uuid4()),
|
|
160
|
+
role=Role.agent,
|
|
161
|
+
parts=[Part(root=TextPart(text=f"Error: {str(e)}"))],
|
|
162
|
+
context_id=context_id or str(uuid4()),
|
|
163
|
+
)
|
|
164
|
+
failed_task = Task(
|
|
165
|
+
id=str(uuid4()),
|
|
166
|
+
context_id=context_id or str(uuid4()),
|
|
167
|
+
status=TaskStatus(state=TaskState.failed),
|
|
168
|
+
history=[error_message],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return SendMessageSuccessResponse(
|
|
172
|
+
id=request_body.get("id", "unknown"),
|
|
173
|
+
result=failed_task,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@router.post(
|
|
177
|
+
"/agents/{id}/v1/message:stream",
|
|
178
|
+
operation_id="stream_message_agent",
|
|
179
|
+
name="stream_message_agent",
|
|
180
|
+
description="Stream a message to an Agno Agent (streaming). The Agent is identified via the path parameter '{id}'. "
|
|
181
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
|
|
182
|
+
"Returns real-time updates as newline-delimited JSON (NDJSON).",
|
|
183
|
+
response_model_exclude_none=True,
|
|
184
|
+
responses={
|
|
185
|
+
200: {
|
|
186
|
+
"description": "Streaming response with task updates",
|
|
187
|
+
"content": {
|
|
188
|
+
"text/event-stream": {
|
|
189
|
+
"example": 'event: TaskStatusUpdateEvent\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n\n'
|
|
190
|
+
'event: Message\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n\n'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
400: {"description": "Invalid request"},
|
|
195
|
+
404: {"description": "Agent not found"},
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
async def a2a_stream_agent(request: Request, id: str):
|
|
199
|
+
if not agents:
|
|
200
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
201
|
+
|
|
202
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
203
|
+
request_body = await request.json()
|
|
204
|
+
kwargs = await get_request_kwargs(request, a2a_stream_agent)
|
|
205
|
+
|
|
206
|
+
# 1. Get the Agent to run
|
|
207
|
+
agent = get_agent_by_id(id, agents)
|
|
208
|
+
if not agent:
|
|
209
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
210
|
+
|
|
211
|
+
# 2. Map the request to our run_input and run variables
|
|
212
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=True)
|
|
213
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
214
|
+
user_id = request.headers.get("X-User-ID")
|
|
215
|
+
if not user_id:
|
|
216
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
217
|
+
|
|
218
|
+
# 3. Run the Agent and stream the response
|
|
219
|
+
try:
|
|
220
|
+
event_stream = agent.arun(
|
|
221
|
+
input=run_input.input_content,
|
|
222
|
+
images=run_input.images,
|
|
223
|
+
videos=run_input.videos,
|
|
224
|
+
audio=run_input.audios,
|
|
225
|
+
files=run_input.files,
|
|
226
|
+
session_id=context_id,
|
|
227
|
+
user_id=user_id,
|
|
228
|
+
stream=True,
|
|
229
|
+
stream_events=True,
|
|
230
|
+
**kwargs,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# 4. Stream the response
|
|
234
|
+
return StreamingResponse(
|
|
235
|
+
stream_a2a_response_with_error_handling(event_stream=event_stream, request_id=request_body["id"]), # type: ignore[arg-type]
|
|
236
|
+
media_type="text/event-stream",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise HTTPException(status_code=500, detail=f"Failed to start run: {str(e)}")
|
|
241
|
+
|
|
242
|
+
# ============= TEAMS =============
|
|
243
|
+
@router.get("/teams/{id}/.well-known/agent-card.json")
|
|
244
|
+
async def get_team_card(request: Request, id: str):
|
|
245
|
+
team = get_team_by_id(id, teams)
|
|
246
|
+
if not team:
|
|
247
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
248
|
+
|
|
249
|
+
base_url = str(request.base_url).rstrip("/")
|
|
250
|
+
skill = AgentSkill(
|
|
251
|
+
id=team.id or "",
|
|
252
|
+
name=team.name or "",
|
|
253
|
+
description=team.description or "",
|
|
254
|
+
tags=["agno"],
|
|
255
|
+
examples=["search", "ok"],
|
|
256
|
+
output_modes=["application/json"],
|
|
257
|
+
)
|
|
258
|
+
return AgentCard(
|
|
259
|
+
name=team.name or "",
|
|
260
|
+
version="1.0.0",
|
|
261
|
+
description=team.description or "",
|
|
262
|
+
url=f"{base_url}/a2a/teams/{team.id}/v1/message:stream",
|
|
263
|
+
default_input_modes=["text"],
|
|
264
|
+
default_output_modes=["text"],
|
|
265
|
+
capabilities=AgentCapabilities(streaming=True, push_notifications=False, state_transition_history=False),
|
|
266
|
+
skills=[skill],
|
|
267
|
+
supports_authenticated_extended_card=False,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@router.post(
|
|
271
|
+
"/teams/{id}/v1/message:send",
|
|
272
|
+
operation_id="run_message_team",
|
|
273
|
+
name="run_message_team",
|
|
274
|
+
description="Send a message to an Agno Team (non-streaming). The Team is identified via the path parameter '{id}'. "
|
|
275
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
|
|
276
|
+
response_model_exclude_none=True,
|
|
277
|
+
responses={
|
|
278
|
+
200: {
|
|
279
|
+
"description": "Message sent successfully",
|
|
280
|
+
"content": {
|
|
281
|
+
"application/json": {
|
|
282
|
+
"example": {
|
|
283
|
+
"jsonrpc": "2.0",
|
|
284
|
+
"id": "request-123",
|
|
285
|
+
"result": {
|
|
286
|
+
"task": {
|
|
287
|
+
"id": "task-456",
|
|
288
|
+
"context_id": "context-789",
|
|
289
|
+
"status": "completed",
|
|
290
|
+
"history": [
|
|
291
|
+
{
|
|
292
|
+
"message_id": "msg-1",
|
|
293
|
+
"role": "agent",
|
|
294
|
+
"parts": [{"kind": "text", "text": "Response from agent"}],
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
400: {"description": "Invalid request"},
|
|
304
|
+
404: {"description": "Team not found"},
|
|
305
|
+
},
|
|
306
|
+
response_model=SendMessageSuccessResponse,
|
|
307
|
+
)
|
|
308
|
+
async def a2a_run_team(request: Request, id: str):
|
|
309
|
+
if not teams:
|
|
310
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
311
|
+
|
|
312
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
313
|
+
request_body = await request.json()
|
|
314
|
+
kwargs = await get_request_kwargs(request, a2a_run_team)
|
|
315
|
+
|
|
316
|
+
# 1. Get the Team to run
|
|
317
|
+
team = get_team_by_id(id, teams)
|
|
318
|
+
if not team:
|
|
319
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
320
|
+
|
|
321
|
+
# 2. Map the request to our run_input and run variables
|
|
322
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=False)
|
|
323
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
324
|
+
user_id = request.headers.get("X-User-ID")
|
|
325
|
+
if not user_id:
|
|
326
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
327
|
+
|
|
328
|
+
# 3. Run the Team
|
|
329
|
+
try:
|
|
330
|
+
response = await team.arun(
|
|
331
|
+
input=run_input.input_content,
|
|
332
|
+
images=run_input.images,
|
|
333
|
+
videos=run_input.videos,
|
|
334
|
+
audio=run_input.audios,
|
|
335
|
+
files=run_input.files,
|
|
336
|
+
session_id=context_id,
|
|
337
|
+
user_id=user_id,
|
|
338
|
+
**kwargs,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# 4. Send the response
|
|
342
|
+
a2a_task = map_run_output_to_a2a_task(response)
|
|
343
|
+
return SendMessageSuccessResponse(
|
|
344
|
+
id=request_body.get("id", "unknown"),
|
|
345
|
+
result=a2a_task,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Handle all critical errors
|
|
349
|
+
except Exception as e:
|
|
350
|
+
from a2a.types import Message as A2AMessage
|
|
351
|
+
from a2a.types import Part, Role, TextPart
|
|
352
|
+
|
|
353
|
+
error_message = A2AMessage(
|
|
354
|
+
message_id=str(uuid4()),
|
|
355
|
+
role=Role.agent,
|
|
356
|
+
parts=[Part(root=TextPart(text=f"Error: {str(e)}"))],
|
|
357
|
+
context_id=context_id or str(uuid4()),
|
|
358
|
+
)
|
|
359
|
+
failed_task = Task(
|
|
360
|
+
id=str(uuid4()),
|
|
361
|
+
context_id=context_id or str(uuid4()),
|
|
362
|
+
status=TaskStatus(state=TaskState.failed),
|
|
363
|
+
history=[error_message],
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return SendMessageSuccessResponse(
|
|
367
|
+
id=request_body.get("id", "unknown"),
|
|
368
|
+
result=failed_task,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@router.post(
|
|
372
|
+
"/teams/{id}/v1/message:stream",
|
|
373
|
+
operation_id="stream_message_team",
|
|
374
|
+
name="stream_message_team",
|
|
375
|
+
description="Stream a message to an Agno Team (streaming). The Team is identified via the path parameter '{id}'. "
|
|
376
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
|
|
377
|
+
"Returns real-time updates as newline-delimited JSON (NDJSON).",
|
|
378
|
+
response_model_exclude_none=True,
|
|
379
|
+
responses={
|
|
380
|
+
200: {
|
|
381
|
+
"description": "Streaming response with task updates",
|
|
382
|
+
"content": {
|
|
383
|
+
"text/event-stream": {
|
|
384
|
+
"example": 'event: TaskStatusUpdateEvent\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n\n'
|
|
385
|
+
'event: Message\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n\n'
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
400: {"description": "Invalid request"},
|
|
390
|
+
404: {"description": "Team not found"},
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
async def a2a_stream_team(request: Request, id: str):
|
|
394
|
+
if not teams:
|
|
395
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
396
|
+
|
|
397
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
398
|
+
request_body = await request.json()
|
|
399
|
+
kwargs = await get_request_kwargs(request, a2a_stream_team)
|
|
400
|
+
|
|
401
|
+
# 1. Get the Team to run
|
|
402
|
+
team = get_team_by_id(id, teams)
|
|
403
|
+
if not team:
|
|
404
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
405
|
+
|
|
406
|
+
# 2. Map the request to our run_input and run variables
|
|
407
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=True)
|
|
408
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
409
|
+
user_id = request.headers.get("X-User-ID")
|
|
410
|
+
if not user_id:
|
|
411
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
412
|
+
|
|
413
|
+
# 3. Run the Team and stream the response
|
|
414
|
+
try:
|
|
415
|
+
event_stream = team.arun(
|
|
416
|
+
input=run_input.input_content,
|
|
417
|
+
images=run_input.images,
|
|
418
|
+
videos=run_input.videos,
|
|
419
|
+
audio=run_input.audios,
|
|
420
|
+
files=run_input.files,
|
|
421
|
+
session_id=context_id,
|
|
422
|
+
user_id=user_id,
|
|
423
|
+
stream=True,
|
|
424
|
+
stream_events=True,
|
|
425
|
+
**kwargs,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# 4. Stream the response
|
|
429
|
+
return StreamingResponse(
|
|
430
|
+
stream_a2a_response_with_error_handling(event_stream=event_stream, request_id=request_body["id"]), # type: ignore[arg-type]
|
|
431
|
+
media_type="text/event-stream",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
raise HTTPException(status_code=500, detail=f"Failed to start run: {str(e)}")
|
|
436
|
+
|
|
437
|
+
# ============= WORKFLOWS =============
|
|
438
|
+
@router.get("/workflows/{id}/.well-known/agent-card.json")
|
|
439
|
+
async def get_workflow_card(request: Request, id: str):
|
|
440
|
+
workflow = get_workflow_by_id(id, workflows)
|
|
441
|
+
if not workflow:
|
|
442
|
+
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
443
|
+
|
|
444
|
+
base_url = str(request.base_url).rstrip("/")
|
|
445
|
+
skill = AgentSkill(
|
|
446
|
+
id=workflow.id or "",
|
|
447
|
+
name=workflow.name or "",
|
|
448
|
+
description=workflow.description or "",
|
|
449
|
+
tags=["agno"],
|
|
450
|
+
examples=["search", "ok"],
|
|
451
|
+
output_modes=["application/json"],
|
|
452
|
+
)
|
|
453
|
+
return AgentCard(
|
|
454
|
+
name=workflow.name or "",
|
|
455
|
+
version="1.0.0",
|
|
456
|
+
description=workflow.description or "",
|
|
457
|
+
url=f"{base_url}/a2a/workflows/{workflow.id}/v1/message:stream",
|
|
458
|
+
default_input_modes=["text"],
|
|
459
|
+
default_output_modes=["text"],
|
|
460
|
+
capabilities=AgentCapabilities(streaming=False, push_notifications=False, state_transition_history=False),
|
|
461
|
+
skills=[skill],
|
|
462
|
+
supports_authenticated_extended_card=False,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
@router.post(
|
|
466
|
+
"/workflows/{id}/v1/message:send",
|
|
467
|
+
operation_id="run_message_workflow",
|
|
468
|
+
name="run_message_workflow",
|
|
469
|
+
description="Send a message to an Agno Workflow (non-streaming). The Workflow is identified via the path parameter '{id}'. "
|
|
470
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
|
|
471
|
+
response_model_exclude_none=True,
|
|
472
|
+
responses={
|
|
473
|
+
200: {
|
|
474
|
+
"description": "Message sent successfully",
|
|
475
|
+
"content": {
|
|
476
|
+
"application/json": {
|
|
477
|
+
"example": {
|
|
478
|
+
"jsonrpc": "2.0",
|
|
479
|
+
"id": "request-123",
|
|
480
|
+
"result": {
|
|
481
|
+
"task": {
|
|
482
|
+
"id": "task-456",
|
|
483
|
+
"context_id": "context-789",
|
|
484
|
+
"status": "completed",
|
|
485
|
+
"history": [
|
|
486
|
+
{
|
|
487
|
+
"message_id": "msg-1",
|
|
488
|
+
"role": "agent",
|
|
489
|
+
"parts": [{"kind": "text", "text": "Response from agent"}],
|
|
490
|
+
}
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
400: {"description": "Invalid request"},
|
|
499
|
+
404: {"description": "Workflow not found"},
|
|
500
|
+
},
|
|
501
|
+
response_model=SendMessageSuccessResponse,
|
|
502
|
+
)
|
|
503
|
+
async def a2a_run_workflow(request: Request, id: str):
|
|
504
|
+
if not workflows:
|
|
505
|
+
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
506
|
+
|
|
507
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
508
|
+
request_body = await request.json()
|
|
509
|
+
kwargs = await get_request_kwargs(request, a2a_run_workflow)
|
|
510
|
+
|
|
511
|
+
# 1. Get the Workflow to run
|
|
512
|
+
workflow = get_workflow_by_id(id, workflows)
|
|
513
|
+
if not workflow:
|
|
514
|
+
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
515
|
+
|
|
516
|
+
# 2. Map the request to our run_input and run variables
|
|
517
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=False)
|
|
518
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
519
|
+
user_id = request.headers.get("X-User-ID")
|
|
520
|
+
if not user_id:
|
|
521
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
522
|
+
|
|
523
|
+
# 3. Run the Workflow
|
|
524
|
+
try:
|
|
525
|
+
response = await workflow.arun(
|
|
526
|
+
input=run_input.input_content,
|
|
527
|
+
images=list(run_input.images) if run_input.images else None,
|
|
528
|
+
videos=list(run_input.videos) if run_input.videos else None,
|
|
529
|
+
audio=list(run_input.audios) if run_input.audios else None,
|
|
530
|
+
files=list(run_input.files) if run_input.files else None,
|
|
531
|
+
session_id=context_id,
|
|
532
|
+
user_id=user_id,
|
|
533
|
+
**kwargs,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# 4. Send the response
|
|
537
|
+
a2a_task = map_run_output_to_a2a_task(response)
|
|
538
|
+
return SendMessageSuccessResponse(
|
|
539
|
+
id=request_body.get("id", "unknown"),
|
|
540
|
+
result=a2a_task,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Handle all critical errors
|
|
544
|
+
except Exception as e:
|
|
545
|
+
from a2a.types import Message as A2AMessage
|
|
546
|
+
from a2a.types import Part, Role, TextPart
|
|
547
|
+
|
|
548
|
+
error_message = A2AMessage(
|
|
549
|
+
message_id=str(uuid4()),
|
|
550
|
+
role=Role.agent,
|
|
551
|
+
parts=[Part(root=TextPart(text=f"Error: {str(e)}"))],
|
|
552
|
+
context_id=context_id or str(uuid4()),
|
|
553
|
+
)
|
|
554
|
+
failed_task = Task(
|
|
555
|
+
id=str(uuid4()),
|
|
556
|
+
context_id=context_id or str(uuid4()),
|
|
557
|
+
status=TaskStatus(state=TaskState.failed),
|
|
558
|
+
history=[error_message],
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
return SendMessageSuccessResponse(
|
|
562
|
+
id=request_body.get("id", "unknown"),
|
|
563
|
+
result=failed_task,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@router.post(
|
|
567
|
+
"/workflows/{id}/v1/message:stream",
|
|
568
|
+
operation_id="stream_message_workflow",
|
|
569
|
+
name="stream_message_workflow",
|
|
570
|
+
description="Stream a message to an Agno Workflow (streaming). The Workflow is identified via the path parameter '{id}'. "
|
|
571
|
+
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
|
|
572
|
+
"Returns real-time updates as newline-delimited JSON (NDJSON).",
|
|
573
|
+
response_model_exclude_none=True,
|
|
574
|
+
responses={
|
|
575
|
+
200: {
|
|
576
|
+
"description": "Streaming response with task updates",
|
|
577
|
+
"content": {
|
|
578
|
+
"text/event-stream": {
|
|
579
|
+
"example": 'event: TaskStatusUpdateEvent\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n\n'
|
|
580
|
+
'event: Message\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n\n'
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
400: {"description": "Invalid request"},
|
|
585
|
+
404: {"description": "Workflow not found"},
|
|
586
|
+
},
|
|
587
|
+
)
|
|
588
|
+
async def a2a_stream_workflow(request: Request, id: str):
|
|
589
|
+
if not workflows:
|
|
590
|
+
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
591
|
+
|
|
592
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
593
|
+
request_body = await request.json()
|
|
594
|
+
kwargs = await get_request_kwargs(request, a2a_stream_workflow)
|
|
595
|
+
|
|
596
|
+
# 1. Get the Workflow to run
|
|
597
|
+
workflow = get_workflow_by_id(id, workflows)
|
|
598
|
+
if not workflow:
|
|
599
|
+
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
600
|
+
|
|
601
|
+
# 2. Map the request to our run_input and run variables
|
|
602
|
+
run_input = await map_a2a_request_to_run_input(request_body, stream=True)
|
|
603
|
+
context_id = request_body.get("params", {}).get("message", {}).get("contextId")
|
|
604
|
+
user_id = request.headers.get("X-User-ID")
|
|
605
|
+
if not user_id:
|
|
606
|
+
user_id = request_body.get("params", {}).get("message", {}).get("metadata", {}).get("userId")
|
|
607
|
+
|
|
608
|
+
# 3. Run the Workflow and stream the response
|
|
609
|
+
try:
|
|
610
|
+
event_stream = workflow.arun(
|
|
611
|
+
input=run_input.input_content,
|
|
612
|
+
images=list(run_input.images) if run_input.images else None,
|
|
613
|
+
videos=list(run_input.videos) if run_input.videos else None,
|
|
614
|
+
audio=list(run_input.audios) if run_input.audios else None,
|
|
615
|
+
files=list(run_input.files) if run_input.files else None,
|
|
616
|
+
session_id=context_id,
|
|
617
|
+
user_id=user_id,
|
|
618
|
+
stream=True,
|
|
619
|
+
stream_events=True,
|
|
620
|
+
**kwargs,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# 4. Stream the response
|
|
624
|
+
return StreamingResponse(
|
|
625
|
+
stream_a2a_response_with_error_handling(event_stream=event_stream, request_id=request_body["id"]), # type: ignore[arg-type]
|
|
626
|
+
media_type="text/event-stream",
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
except Exception as e:
|
|
630
|
+
raise HTTPException(status_code=500, detail=f"Failed to start run: {str(e)}")
|
|
631
|
+
|
|
632
|
+
# ============= DEPRECATED ENDPOINTS =============
|
|
633
|
+
|
|
36
634
|
@router.post(
|
|
37
635
|
"/message/send",
|
|
38
636
|
operation_id="send_message",
|
|
39
637
|
name="send_message",
|
|
40
|
-
description="Send a message to an Agno Agent, Team, or Workflow. "
|
|
638
|
+
description="[DEPRECATED] Send a message to an Agno Agent, Team, or Workflow. "
|
|
41
639
|
"The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
|
|
42
640
|
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
|
|
43
641
|
response_model_exclude_none=True,
|
|
@@ -73,6 +671,12 @@ def attach_routes(
|
|
|
73
671
|
response_model=SendMessageSuccessResponse,
|
|
74
672
|
)
|
|
75
673
|
async def a2a_send_message(request: Request):
|
|
674
|
+
warnings.warn(
|
|
675
|
+
"This endpoint will be deprecated soon. Use /agents/{agents_id}/v1/message:send, /teams/{teams_id}/v1/message:send, or /workflows/{workflows_id}/v1/message:send instead.",
|
|
676
|
+
DeprecationWarning,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
76
680
|
request_body = await request.json()
|
|
77
681
|
kwargs = await get_request_kwargs(request, a2a_send_message)
|
|
78
682
|
|
|
@@ -103,7 +707,7 @@ def attach_routes(
|
|
|
103
707
|
# 3. Run the agent, team, or workflow
|
|
104
708
|
try:
|
|
105
709
|
if isinstance(entity, Workflow):
|
|
106
|
-
response =
|
|
710
|
+
response = entity.arun(
|
|
107
711
|
input=run_input.input_content,
|
|
108
712
|
images=list(run_input.images) if run_input.images else None,
|
|
109
713
|
videos=list(run_input.videos) if run_input.videos else None,
|
|
@@ -114,7 +718,7 @@ def attach_routes(
|
|
|
114
718
|
**kwargs,
|
|
115
719
|
)
|
|
116
720
|
else:
|
|
117
|
-
response =
|
|
721
|
+
response = entity.arun(
|
|
118
722
|
input=run_input.input_content,
|
|
119
723
|
images=run_input.images,
|
|
120
724
|
videos=run_input.videos,
|
|
@@ -159,7 +763,7 @@ def attach_routes(
|
|
|
159
763
|
"/message/stream",
|
|
160
764
|
operation_id="stream_message",
|
|
161
765
|
name="stream_message",
|
|
162
|
-
description="Stream a message to an Agno Agent, Team, or Workflow."
|
|
766
|
+
description="[DEPRECATED] Stream a message to an Agno Agent, Team, or Workflow. "
|
|
163
767
|
"The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
|
|
164
768
|
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
|
|
165
769
|
"Returns real-time updates as newline-delimited JSON (NDJSON).",
|
|
@@ -168,9 +772,9 @@ def attach_routes(
|
|
|
168
772
|
200: {
|
|
169
773
|
"description": "Streaming response with task updates",
|
|
170
774
|
"content": {
|
|
171
|
-
"
|
|
172
|
-
"example": '{"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n'
|
|
173
|
-
'{"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n'
|
|
775
|
+
"text/event-stream": {
|
|
776
|
+
"example": 'event: TaskStatusUpdateEvent\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"taskId":"task-456","status":"working"}}\n\n'
|
|
777
|
+
'event: Message\ndata: {"jsonrpc":"2.0","id":"request-123","result":{"messageId":"msg-1","role":"agent","parts":[{"kind":"text","text":"Response"}]}}\n\n'
|
|
174
778
|
}
|
|
175
779
|
},
|
|
176
780
|
},
|
|
@@ -179,6 +783,12 @@ def attach_routes(
|
|
|
179
783
|
},
|
|
180
784
|
)
|
|
181
785
|
async def a2a_stream_message(request: Request):
|
|
786
|
+
warnings.warn(
|
|
787
|
+
"This endpoint will be deprecated soon. Use /agents/{agents_id}/v1/message:stream, /teams/{teams_id}/v1/message:stream, or /workflows/{workflows_id}/v1/message:stream instead.",
|
|
788
|
+
DeprecationWarning,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Load the request body. Unknown args are passed down as kwargs.
|
|
182
792
|
request_body = await request.json()
|
|
183
793
|
kwargs = await get_request_kwargs(request, a2a_stream_message)
|
|
184
794
|
|
|
@@ -240,7 +850,7 @@ def attach_routes(
|
|
|
240
850
|
# 4. Stream the response
|
|
241
851
|
return StreamingResponse(
|
|
242
852
|
stream_a2a_response_with_error_handling(event_stream=event_stream, request_id=request_body["id"]), # type: ignore[arg-type]
|
|
243
|
-
media_type="
|
|
853
|
+
media_type="text/event-stream",
|
|
244
854
|
)
|
|
245
855
|
|
|
246
856
|
except Exception as e:
|