agno 2.3.21__py3-none-any.whl → 2.3.23__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 +48 -2
- agno/agent/remote.py +234 -73
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/v2_3_0.py +92 -53
- agno/db/mysql/async_mysql.py +5 -7
- agno/db/mysql/mysql.py +5 -7
- agno/db/mysql/schemas.py +39 -21
- agno/db/postgres/async_postgres.py +172 -42
- agno/db/postgres/postgres.py +186 -38
- agno/db/postgres/schemas.py +39 -21
- agno/db/postgres/utils.py +6 -2
- agno/db/singlestore/schemas.py +41 -21
- agno/db/singlestore/singlestore.py +14 -3
- agno/db/sqlite/async_sqlite.py +7 -2
- agno/db/sqlite/schemas.py +36 -21
- agno/db/sqlite/sqlite.py +3 -7
- agno/knowledge/chunking/document.py +3 -2
- agno/knowledge/chunking/markdown.py +8 -3
- agno/knowledge/chunking/recursive.py +2 -2
- agno/models/base.py +4 -0
- agno/models/google/gemini.py +27 -4
- agno/models/openai/chat.py +1 -1
- agno/models/openai/responses.py +14 -7
- agno/os/middleware/jwt.py +66 -27
- agno/os/routers/agents/router.py +3 -3
- agno/os/routers/evals/evals.py +2 -2
- agno/os/routers/knowledge/knowledge.py +5 -5
- agno/os/routers/knowledge/schemas.py +1 -1
- agno/os/routers/memory/memory.py +4 -4
- agno/os/routers/session/session.py +2 -2
- agno/os/routers/teams/router.py +4 -4
- agno/os/routers/traces/traces.py +3 -3
- agno/os/routers/workflows/router.py +3 -3
- agno/os/schema.py +1 -1
- agno/reasoning/deepseek.py +11 -1
- agno/reasoning/gemini.py +6 -2
- agno/reasoning/groq.py +8 -3
- agno/reasoning/openai.py +2 -0
- agno/remote/base.py +106 -9
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +370 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/team/remote.py +220 -60
- agno/team/team.py +41 -3
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +150 -13
- agno/tools/function.py +6 -1
- agno/tools/mcp/mcp.py +300 -17
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/toolkit.py +89 -21
- agno/utils/mcp.py +49 -8
- agno/utils/string.py +43 -1
- agno/workflow/condition.py +4 -2
- agno/workflow/loop.py +20 -1
- agno/workflow/remote.py +173 -33
- agno/workflow/router.py +4 -1
- agno/workflow/steps.py +4 -0
- agno/workflow/workflow.py +14 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/METADATA +13 -14
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/RECORD +74 -60
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
agno/utils/string.py
CHANGED
|
@@ -2,13 +2,15 @@ import hashlib
|
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
-
from typing import Optional, Type
|
|
5
|
+
from typing import Any, Optional, Type, Union
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ValidationError
|
|
9
9
|
|
|
10
10
|
from agno.utils.log import logger
|
|
11
11
|
|
|
12
|
+
POSTGRES_INVALID_CHARS_REGEX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]")
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def is_valid_uuid(uuid_str: str) -> bool:
|
|
14
16
|
"""
|
|
@@ -275,3 +277,43 @@ def generate_id_from_name(name: Optional[str] = None) -> str:
|
|
|
275
277
|
return name.lower().replace(" ", "-").replace("_", "-")
|
|
276
278
|
else:
|
|
277
279
|
return str(uuid4())
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def sanitize_postgres_string(value: Optional[str]) -> Optional[str]:
|
|
283
|
+
"""Remove illegal chars from string values to prevent PostgreSQL encoding errors.
|
|
284
|
+
|
|
285
|
+
This function all chars illegal in Postgres UTF-8 text fields.
|
|
286
|
+
Useful to prevent CharacterNotInRepertoireError when storing strings.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
value: The string value to sanitize.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The sanitized string with illegal chars removed, or None if input was None.
|
|
293
|
+
"""
|
|
294
|
+
if value is None:
|
|
295
|
+
return None
|
|
296
|
+
if isinstance(value, str):
|
|
297
|
+
return POSTGRES_INVALID_CHARS_REGEX.sub("", value)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def sanitize_postgres_strings(data: Union[dict, list, str, Any]) -> Union[dict, list, str, Any]:
|
|
301
|
+
"""Recursively sanitize all string values in a dictionary or JSON structure.
|
|
302
|
+
|
|
303
|
+
This function traverses dictionaries, lists, and nested structures to find
|
|
304
|
+
and sanitize all string values, removing null bytes that PostgreSQL cannot handle.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
data: The data structure to sanitize (dict, list, str or any other type).
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
The sanitized data structure with all strings cleaned of null bytes.
|
|
311
|
+
"""
|
|
312
|
+
if isinstance(data, dict):
|
|
313
|
+
return {key: sanitize_postgres_strings(value) for key, value in data.items()}
|
|
314
|
+
elif isinstance(data, list):
|
|
315
|
+
return [sanitize_postgres_strings(item) for item in data]
|
|
316
|
+
elif isinstance(data, str):
|
|
317
|
+
return sanitize_postgres_string(data)
|
|
318
|
+
else:
|
|
319
|
+
return data
|
agno/workflow/condition.py
CHANGED
|
@@ -279,7 +279,7 @@ class Condition:
|
|
|
279
279
|
content=f"Condition {self.name} completed with {len(all_results)} results",
|
|
280
280
|
success=all(result.success for result in all_results) if all_results else True,
|
|
281
281
|
error=None,
|
|
282
|
-
stop=False,
|
|
282
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
283
283
|
steps=all_results,
|
|
284
284
|
)
|
|
285
285
|
|
|
@@ -460,6 +460,7 @@ class Condition:
|
|
|
460
460
|
step_type=StepType.CONDITION,
|
|
461
461
|
content=f"Condition {self.name} completed with {len(all_results)} results",
|
|
462
462
|
success=all(result.success for result in all_results) if all_results else True,
|
|
463
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
463
464
|
steps=all_results,
|
|
464
465
|
)
|
|
465
466
|
|
|
@@ -571,7 +572,7 @@ class Condition:
|
|
|
571
572
|
content=f"Condition {self.name} completed with {len(all_results)} results",
|
|
572
573
|
success=all(result.success for result in all_results) if all_results else True,
|
|
573
574
|
error=None,
|
|
574
|
-
stop=False,
|
|
575
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
575
576
|
steps=all_results,
|
|
576
577
|
)
|
|
577
578
|
|
|
@@ -755,5 +756,6 @@ class Condition:
|
|
|
755
756
|
step_type=StepType.CONDITION,
|
|
756
757
|
content=f"Condition {self.name} completed with {len(all_results)} results",
|
|
757
758
|
success=all(result.success for result in all_results) if all_results else True,
|
|
759
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
758
760
|
steps=all_results,
|
|
759
761
|
)
|
agno/workflow/loop.py
CHANGED
|
@@ -150,6 +150,7 @@ class Loop:
|
|
|
150
150
|
|
|
151
151
|
all_results = []
|
|
152
152
|
iteration = 0
|
|
153
|
+
early_termination = False
|
|
153
154
|
|
|
154
155
|
while iteration < self.max_iterations:
|
|
155
156
|
# Execute all steps in this iteration - mirroring workflow logic
|
|
@@ -182,6 +183,7 @@ class Loop:
|
|
|
182
183
|
|
|
183
184
|
if any(output.stop for output in step_output):
|
|
184
185
|
logger.info(f"Early termination requested by step {step_name}")
|
|
186
|
+
early_termination = True
|
|
185
187
|
break
|
|
186
188
|
else:
|
|
187
189
|
# Single StepOutput
|
|
@@ -191,6 +193,7 @@ class Loop:
|
|
|
191
193
|
|
|
192
194
|
if step_output.stop:
|
|
193
195
|
logger.info(f"Early termination requested by step {step_name}")
|
|
196
|
+
early_termination = True
|
|
194
197
|
break
|
|
195
198
|
|
|
196
199
|
# Update step input for next step
|
|
@@ -209,7 +212,11 @@ class Loop:
|
|
|
209
212
|
break
|
|
210
213
|
except Exception as e:
|
|
211
214
|
logger.warning(f"End condition evaluation failed: {e}")
|
|
212
|
-
|
|
215
|
+
|
|
216
|
+
# Break out of iteration loop if early termination was requested
|
|
217
|
+
if early_termination:
|
|
218
|
+
log_debug(f"Loop ending early due to step termination request at iteration {iteration}")
|
|
219
|
+
break
|
|
213
220
|
|
|
214
221
|
log_debug(f"Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
|
|
215
222
|
|
|
@@ -224,6 +231,7 @@ class Loop:
|
|
|
224
231
|
step_type=StepType.LOOP,
|
|
225
232
|
content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
|
|
226
233
|
success=all(result.success for result in flattened_results) if flattened_results else True,
|
|
234
|
+
stop=any(result.stop for result in flattened_results) if flattened_results else False,
|
|
227
235
|
steps=flattened_results,
|
|
228
236
|
)
|
|
229
237
|
|
|
@@ -434,6 +442,7 @@ class Loop:
|
|
|
434
442
|
step_type=StepType.LOOP,
|
|
435
443
|
content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
|
|
436
444
|
success=all(result.success for result in flattened_results) if flattened_results else True,
|
|
445
|
+
stop=any(result.stop for result in flattened_results) if flattened_results else False,
|
|
437
446
|
steps=flattened_results,
|
|
438
447
|
)
|
|
439
448
|
|
|
@@ -462,6 +471,7 @@ class Loop:
|
|
|
462
471
|
|
|
463
472
|
all_results = []
|
|
464
473
|
iteration = 0
|
|
474
|
+
early_termination = False
|
|
465
475
|
|
|
466
476
|
while iteration < self.max_iterations:
|
|
467
477
|
# Execute all steps in this iteration - mirroring workflow logic
|
|
@@ -494,6 +504,7 @@ class Loop:
|
|
|
494
504
|
|
|
495
505
|
if any(output.stop for output in step_output):
|
|
496
506
|
logger.info(f"Early termination requested by step {step_name}")
|
|
507
|
+
early_termination = True
|
|
497
508
|
break
|
|
498
509
|
else:
|
|
499
510
|
# Single StepOutput
|
|
@@ -503,6 +514,7 @@ class Loop:
|
|
|
503
514
|
|
|
504
515
|
if step_output.stop:
|
|
505
516
|
logger.info(f"Early termination requested by step {step_name}")
|
|
517
|
+
early_termination = True
|
|
506
518
|
break
|
|
507
519
|
|
|
508
520
|
# Update step input for next step
|
|
@@ -525,6 +537,11 @@ class Loop:
|
|
|
525
537
|
except Exception as e:
|
|
526
538
|
logger.warning(f"End condition evaluation failed: {e}")
|
|
527
539
|
|
|
540
|
+
# Break out of iteration loop if early termination was requested
|
|
541
|
+
if early_termination:
|
|
542
|
+
log_debug(f"Loop ending early due to step termination request at iteration {iteration}")
|
|
543
|
+
break
|
|
544
|
+
|
|
528
545
|
# Use workflow logger for async loop completion
|
|
529
546
|
log_debug(f"Async Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
|
|
530
547
|
|
|
@@ -539,6 +556,7 @@ class Loop:
|
|
|
539
556
|
step_type=StepType.LOOP,
|
|
540
557
|
content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
|
|
541
558
|
success=all(result.success for result in flattened_results) if flattened_results else True,
|
|
559
|
+
stop=any(result.stop for result in flattened_results) if flattened_results else False,
|
|
542
560
|
steps=flattened_results,
|
|
543
561
|
)
|
|
544
562
|
|
|
@@ -752,5 +770,6 @@ class Loop:
|
|
|
752
770
|
step_type=StepType.LOOP,
|
|
753
771
|
content=f"Loop {self.name} completed {iteration} iterations with {len(flattened_results)} total steps",
|
|
754
772
|
success=all(result.success for result in flattened_results) if flattened_results else True,
|
|
773
|
+
stop=any(result.stop for result in flattened_results) if flattened_results else False,
|
|
755
774
|
steps=flattened_results,
|
|
756
775
|
)
|
agno/workflow/remote.py
CHANGED
|
@@ -24,21 +24,28 @@ class RemoteWorkflow(BaseRemote):
|
|
|
24
24
|
base_url: str,
|
|
25
25
|
workflow_id: str,
|
|
26
26
|
timeout: float = 300.0,
|
|
27
|
+
protocol: Literal["agentos", "a2a"] = "agentos",
|
|
28
|
+
a2a_protocol: Literal["json-rpc", "rest"] = "rest",
|
|
27
29
|
config_ttl: float = 300.0,
|
|
28
30
|
):
|
|
29
|
-
"""Initialize
|
|
31
|
+
"""Initialize RemoteWorkflow for remote execution.
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Supports two protocols:
|
|
34
|
+
- "agentos": Agno's proprietary AgentOS REST API (default)
|
|
35
|
+
- "a2a": A2A (Agent-to-Agent) protocol for cross-framework communication
|
|
32
36
|
|
|
33
37
|
Args:
|
|
34
|
-
base_url: Base URL for remote
|
|
35
|
-
workflow_id: ID of remote workflow
|
|
38
|
+
base_url: Base URL for remote instance (e.g., "http://localhost:7777")
|
|
39
|
+
workflow_id: ID of remote workflow on the remote server
|
|
36
40
|
timeout: Request timeout in seconds (default: 300)
|
|
41
|
+
protocol: Communication protocol - "agentos" (default) or "a2a"
|
|
42
|
+
a2a_protocol: For A2A protocol only - Whether to use JSON-RPC or REST protocol.
|
|
37
43
|
config_ttl: Time-to-live for cached config in seconds (default: 300)
|
|
38
44
|
"""
|
|
39
|
-
super().__init__(base_url, timeout, config_ttl)
|
|
45
|
+
super().__init__(base_url, timeout, protocol, a2a_protocol, config_ttl)
|
|
40
46
|
self.workflow_id = workflow_id
|
|
41
47
|
self._cached_workflow_config = None
|
|
48
|
+
self._config_ttl = config_ttl
|
|
42
49
|
|
|
43
50
|
@property
|
|
44
51
|
def id(self) -> str:
|
|
@@ -46,13 +53,38 @@ class RemoteWorkflow(BaseRemote):
|
|
|
46
53
|
|
|
47
54
|
async def get_workflow_config(self) -> "WorkflowResponse":
|
|
48
55
|
"""Get the workflow config from remote (always fetches fresh)."""
|
|
49
|
-
|
|
56
|
+
from agno.os.routers.workflows.schema import WorkflowResponse
|
|
57
|
+
|
|
58
|
+
if self.protocol == "a2a":
|
|
59
|
+
from agno.client.a2a.schemas import AgentCard
|
|
60
|
+
|
|
61
|
+
agent_card: Optional[AgentCard] = await self.a2a_client.aget_agent_card() # type: ignore
|
|
62
|
+
|
|
63
|
+
return WorkflowResponse(
|
|
64
|
+
id=self.workflow_id,
|
|
65
|
+
name=agent_card.name if agent_card else self.workflow_id,
|
|
66
|
+
description=agent_card.description if agent_card else f"A2A workflow: {self.workflow_id}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# AgentOS protocol: fetch fresh config from remote
|
|
70
|
+
return await self.agentos_client.aget_workflow(self.workflow_id) # type: ignore
|
|
50
71
|
|
|
51
72
|
@property
|
|
52
73
|
def _workflow_config(self) -> "WorkflowResponse":
|
|
53
74
|
"""Get the workflow config from remote, cached with TTL."""
|
|
54
75
|
from agno.os.routers.workflows.schema import WorkflowResponse
|
|
55
76
|
|
|
77
|
+
if self.protocol == "a2a":
|
|
78
|
+
from agno.client.a2a.schemas import AgentCard
|
|
79
|
+
|
|
80
|
+
agent_card: Optional[AgentCard] = self.a2a_client.get_agent_card() # type: ignore
|
|
81
|
+
|
|
82
|
+
return WorkflowResponse(
|
|
83
|
+
id=self.workflow_id,
|
|
84
|
+
name=agent_card.name if agent_card else self.workflow_id,
|
|
85
|
+
description=agent_card.description if agent_card else f"A2A workflow: {self.workflow_id}",
|
|
86
|
+
)
|
|
87
|
+
|
|
56
88
|
current_time = time.time()
|
|
57
89
|
|
|
58
90
|
# Check if cache is valid
|
|
@@ -62,15 +94,15 @@ class RemoteWorkflow(BaseRemote):
|
|
|
62
94
|
return config
|
|
63
95
|
|
|
64
96
|
# Fetch fresh config
|
|
65
|
-
config: WorkflowResponse = self.
|
|
97
|
+
config: WorkflowResponse = self.agentos_client.get_workflow(self.workflow_id) # type: ignore
|
|
66
98
|
self._cached_workflow_config = (config, current_time)
|
|
67
99
|
return config
|
|
68
100
|
|
|
69
|
-
def refresh_config(self) -> "WorkflowResponse":
|
|
101
|
+
async def refresh_config(self) -> "WorkflowResponse":
|
|
70
102
|
"""Force refresh the cached workflow config."""
|
|
71
103
|
from agno.os.routers.workflows.schema import WorkflowResponse
|
|
72
104
|
|
|
73
|
-
config: WorkflowResponse = self.
|
|
105
|
+
config: WorkflowResponse = await self.agentos_client.aget_workflow(self.workflow_id) # type: ignore
|
|
74
106
|
self._cached_workflow_config = (config, time.time())
|
|
75
107
|
return config
|
|
76
108
|
|
|
@@ -88,10 +120,15 @@ class RemoteWorkflow(BaseRemote):
|
|
|
88
120
|
|
|
89
121
|
@property
|
|
90
122
|
def db(self) -> Optional[RemoteDb]:
|
|
91
|
-
if
|
|
123
|
+
if (
|
|
124
|
+
self.agentos_client
|
|
125
|
+
and self._config
|
|
126
|
+
and self._workflow_config is not None
|
|
127
|
+
and self._workflow_config.db_id is not None
|
|
128
|
+
):
|
|
92
129
|
return RemoteDb.from_config(
|
|
93
130
|
db_id=self._workflow_config.db_id,
|
|
94
|
-
client=self.
|
|
131
|
+
client=self.agentos_client,
|
|
95
132
|
config=self._config,
|
|
96
133
|
)
|
|
97
134
|
return None
|
|
@@ -165,42 +202,145 @@ class RemoteWorkflow(BaseRemote):
|
|
|
165
202
|
serialized_input = serialize_input(validated_input)
|
|
166
203
|
headers = self._get_auth_headers(auth_token)
|
|
167
204
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return self.
|
|
171
|
-
workflow_id=self.workflow_id,
|
|
205
|
+
# A2A protocol path
|
|
206
|
+
if self.a2a_client:
|
|
207
|
+
return self._arun_a2a( # type: ignore[return-value]
|
|
172
208
|
message=serialized_input,
|
|
173
|
-
|
|
174
|
-
run_id=run_id,
|
|
175
|
-
session_id=session_id,
|
|
209
|
+
stream=stream or False,
|
|
176
210
|
user_id=user_id,
|
|
177
|
-
|
|
211
|
+
context_id=session_id, # Map session_id → context_id for A2A
|
|
178
212
|
images=images,
|
|
179
213
|
videos=videos,
|
|
214
|
+
audio=audio,
|
|
180
215
|
files=files,
|
|
181
|
-
session_state=session_state,
|
|
182
|
-
stream_events=stream_events,
|
|
183
216
|
headers=headers,
|
|
184
|
-
**kwargs,
|
|
185
217
|
)
|
|
218
|
+
|
|
219
|
+
# AgentOS protocol path (default)
|
|
220
|
+
if self.agentos_client:
|
|
221
|
+
if stream:
|
|
222
|
+
# Handle streaming response
|
|
223
|
+
return self.agentos_client.run_workflow_stream(
|
|
224
|
+
workflow_id=self.workflow_id,
|
|
225
|
+
message=serialized_input,
|
|
226
|
+
additional_data=additional_data,
|
|
227
|
+
run_id=run_id,
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
user_id=user_id,
|
|
230
|
+
audio=audio,
|
|
231
|
+
images=images,
|
|
232
|
+
videos=videos,
|
|
233
|
+
files=files,
|
|
234
|
+
session_state=session_state,
|
|
235
|
+
stream_events=stream_events,
|
|
236
|
+
headers=headers,
|
|
237
|
+
**kwargs,
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
return self.agentos_client.run_workflow( # type: ignore
|
|
241
|
+
workflow_id=self.workflow_id,
|
|
242
|
+
message=serialized_input,
|
|
243
|
+
additional_data=additional_data,
|
|
244
|
+
run_id=run_id,
|
|
245
|
+
session_id=session_id,
|
|
246
|
+
user_id=user_id,
|
|
247
|
+
audio=audio,
|
|
248
|
+
images=images,
|
|
249
|
+
videos=videos,
|
|
250
|
+
files=files,
|
|
251
|
+
session_state=session_state,
|
|
252
|
+
headers=headers,
|
|
253
|
+
**kwargs,
|
|
254
|
+
)
|
|
186
255
|
else:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
256
|
+
raise ValueError("No client available")
|
|
257
|
+
|
|
258
|
+
def _arun_a2a(
|
|
259
|
+
self,
|
|
260
|
+
message: str,
|
|
261
|
+
stream: bool,
|
|
262
|
+
user_id: Optional[str],
|
|
263
|
+
context_id: Optional[str],
|
|
264
|
+
images: Optional[List[Image]],
|
|
265
|
+
videos: Optional[List[Video]],
|
|
266
|
+
audio: Optional[List[Audio]],
|
|
267
|
+
files: Optional[List[File]],
|
|
268
|
+
headers: Optional[Dict[str, str]],
|
|
269
|
+
) -> Union[WorkflowRunOutput, AsyncIterator[WorkflowRunOutputEvent]]:
|
|
270
|
+
"""Execute via A2A protocol.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
message: Serialized message string
|
|
274
|
+
stream: Whether to stream the response
|
|
275
|
+
user_id: User identifier
|
|
276
|
+
context_id: Session/context ID (maps to session_id)
|
|
277
|
+
images: Images to include
|
|
278
|
+
videos: Videos to include
|
|
279
|
+
audio: Audio files to include
|
|
280
|
+
files: Files to include
|
|
281
|
+
headers: HTTP headers to include in the request (optional)
|
|
282
|
+
Returns:
|
|
283
|
+
WorkflowRunOutput for non-streaming, AsyncIterator[WorkflowRunOutputEvent] for streaming
|
|
284
|
+
"""
|
|
285
|
+
if not self.a2a_client:
|
|
286
|
+
raise ValueError("A2A client not available")
|
|
287
|
+
from agno.client.a2a.utils import map_stream_events_to_workflow_run_events
|
|
288
|
+
|
|
289
|
+
if stream:
|
|
290
|
+
# Return async generator for streaming
|
|
291
|
+
event_stream = self.a2a_client.stream_message(
|
|
292
|
+
message=message,
|
|
293
|
+
context_id=context_id,
|
|
193
294
|
user_id=user_id,
|
|
194
|
-
|
|
295
|
+
images=list(images) if images else None,
|
|
296
|
+
audio=list(audio) if audio else None,
|
|
297
|
+
videos=list(videos) if videos else None,
|
|
298
|
+
files=list(files) if files else None,
|
|
299
|
+
headers=headers,
|
|
300
|
+
)
|
|
301
|
+
return map_stream_events_to_workflow_run_events(event_stream, workflow_id=self.workflow_id) # type: ignore
|
|
302
|
+
else:
|
|
303
|
+
# Return coroutine for non-streaming
|
|
304
|
+
return self._arun_a2a_send( # type: ignore[return-value]
|
|
305
|
+
message=message,
|
|
306
|
+
user_id=user_id,
|
|
307
|
+
context_id=context_id,
|
|
195
308
|
images=images,
|
|
309
|
+
audio=audio,
|
|
196
310
|
videos=videos,
|
|
197
311
|
files=files,
|
|
198
|
-
session_state=session_state,
|
|
199
312
|
headers=headers,
|
|
200
|
-
**kwargs,
|
|
201
313
|
)
|
|
202
314
|
|
|
203
|
-
async def
|
|
315
|
+
async def _arun_a2a_send(
|
|
316
|
+
self,
|
|
317
|
+
message: str,
|
|
318
|
+
user_id: Optional[str],
|
|
319
|
+
context_id: Optional[str],
|
|
320
|
+
images: Optional[List[Image]],
|
|
321
|
+
videos: Optional[List[Video]],
|
|
322
|
+
audio: Optional[List[Audio]],
|
|
323
|
+
files: Optional[List[File]],
|
|
324
|
+
headers: Optional[Dict[str, str]],
|
|
325
|
+
) -> WorkflowRunOutput:
|
|
326
|
+
"""Send a non-streaming A2A message and convert response to WorkflowRunOutput."""
|
|
327
|
+
if not self.a2a_client:
|
|
328
|
+
raise ValueError("A2A client not available")
|
|
329
|
+
from agno.client.a2a.utils import map_task_result_to_workflow_run_output
|
|
330
|
+
|
|
331
|
+
task_result = await self.a2a_client.send_message(
|
|
332
|
+
message=message,
|
|
333
|
+
context_id=context_id,
|
|
334
|
+
user_id=user_id,
|
|
335
|
+
images=list(images) if images else None,
|
|
336
|
+
audio=list(audio) if audio else None,
|
|
337
|
+
videos=list(videos) if videos else None,
|
|
338
|
+
files=list(files) if files else None,
|
|
339
|
+
headers=headers,
|
|
340
|
+
)
|
|
341
|
+
return map_task_result_to_workflow_run_output(task_result, workflow_id=self.workflow_id, user_id=user_id)
|
|
342
|
+
|
|
343
|
+
async def acancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
|
|
204
344
|
"""Cancel a running workflow execution.
|
|
205
345
|
|
|
206
346
|
Args:
|
|
@@ -212,7 +352,7 @@ class RemoteWorkflow(BaseRemote):
|
|
|
212
352
|
"""
|
|
213
353
|
headers = self._get_auth_headers(auth_token)
|
|
214
354
|
try:
|
|
215
|
-
await self.
|
|
355
|
+
await self.get_os_client().cancel_workflow_run(
|
|
216
356
|
workflow_id=self.workflow_id,
|
|
217
357
|
run_id=run_id,
|
|
218
358
|
headers=headers,
|
agno/workflow/router.py
CHANGED
|
@@ -267,6 +267,7 @@ class Router:
|
|
|
267
267
|
step_type=StepType.ROUTER,
|
|
268
268
|
content=f"Router {self.name} completed with {len(all_results)} results",
|
|
269
269
|
success=all(result.success for result in all_results) if all_results else True,
|
|
270
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
270
271
|
steps=all_results,
|
|
271
272
|
)
|
|
272
273
|
|
|
@@ -438,6 +439,7 @@ class Router:
|
|
|
438
439
|
step_type=StepType.ROUTER,
|
|
439
440
|
content=f"Router {self.name} completed with {len(all_results)} results",
|
|
440
441
|
success=all(result.success for result in all_results) if all_results else True,
|
|
442
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
441
443
|
steps=all_results,
|
|
442
444
|
)
|
|
443
445
|
|
|
@@ -544,6 +546,7 @@ class Router:
|
|
|
544
546
|
step_type=StepType.ROUTER,
|
|
545
547
|
content=f"Router {self.name} completed with {len(all_results)} results",
|
|
546
548
|
success=all(result.success for result in all_results) if all_results else True,
|
|
549
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
547
550
|
steps=all_results,
|
|
548
551
|
)
|
|
549
552
|
|
|
@@ -718,6 +721,6 @@ class Router:
|
|
|
718
721
|
content=f"Router {self.name} completed with {len(all_results)} results",
|
|
719
722
|
success=all(result.success for result in all_results) if all_results else True,
|
|
720
723
|
error=None,
|
|
721
|
-
stop=False,
|
|
724
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
722
725
|
steps=all_results,
|
|
723
726
|
)
|
agno/workflow/steps.py
CHANGED
|
@@ -194,6 +194,7 @@ class Steps:
|
|
|
194
194
|
step_type=StepType.STEPS,
|
|
195
195
|
content=f"Steps {self.name} completed with {len(all_results)} results",
|
|
196
196
|
success=all(result.success for result in all_results) if all_results else True,
|
|
197
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
197
198
|
steps=all_results,
|
|
198
199
|
)
|
|
199
200
|
|
|
@@ -351,6 +352,7 @@ class Steps:
|
|
|
351
352
|
step_type=StepType.STEPS,
|
|
352
353
|
content=f"Steps {self.name} completed with {len(all_results)} results",
|
|
353
354
|
success=all(result.success for result in all_results) if all_results else True,
|
|
355
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
354
356
|
steps=all_results,
|
|
355
357
|
)
|
|
356
358
|
|
|
@@ -443,6 +445,7 @@ class Steps:
|
|
|
443
445
|
step_type=StepType.STEPS,
|
|
444
446
|
content=f"Steps {self.name} completed with {len(all_results)} results",
|
|
445
447
|
success=all(result.success for result in all_results) if all_results else True,
|
|
448
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
446
449
|
steps=all_results,
|
|
447
450
|
)
|
|
448
451
|
|
|
@@ -599,6 +602,7 @@ class Steps:
|
|
|
599
602
|
step_type=StepType.STEPS,
|
|
600
603
|
content=f"Steps {self.name} completed with {len(all_results)} results",
|
|
601
604
|
success=all(result.success for result in all_results) if all_results else True,
|
|
605
|
+
stop=any(result.stop for result in all_results) if all_results else False,
|
|
602
606
|
steps=all_results,
|
|
603
607
|
)
|
|
604
608
|
|
agno/workflow/workflow.py
CHANGED
|
@@ -36,6 +36,9 @@ from agno.models.message import Message
|
|
|
36
36
|
from agno.models.metrics import Metrics
|
|
37
37
|
from agno.run import RunContext, RunStatus
|
|
38
38
|
from agno.run.agent import RunContentEvent, RunEvent, RunOutput
|
|
39
|
+
from agno.run.cancel import (
|
|
40
|
+
acancel_run as acancel_run_global,
|
|
41
|
+
)
|
|
39
42
|
from agno.run.cancel import (
|
|
40
43
|
acleanup_run,
|
|
41
44
|
araise_if_cancelled,
|
|
@@ -3496,6 +3499,17 @@ class Workflow:
|
|
|
3496
3499
|
"""
|
|
3497
3500
|
return cancel_run_global(run_id)
|
|
3498
3501
|
|
|
3502
|
+
async def acancel_run(self, run_id: str) -> bool:
|
|
3503
|
+
"""Cancel a running workflow execution (async version).
|
|
3504
|
+
|
|
3505
|
+
Args:
|
|
3506
|
+
run_id (str): The run_id to cancel.
|
|
3507
|
+
|
|
3508
|
+
Returns:
|
|
3509
|
+
bool: True if the run was found and marked for cancellation, False otherwise.
|
|
3510
|
+
"""
|
|
3511
|
+
return await acancel_run_global(run_id)
|
|
3512
|
+
|
|
3499
3513
|
@overload
|
|
3500
3514
|
def run(
|
|
3501
3515
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agno
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.23
|
|
4
4
|
Summary: Agno: a lightweight library for building Multi-Agent Systems
|
|
5
5
|
Author-email: Ashpreet Bedi <ashpreet@agno.com>
|
|
6
6
|
Project-URL: homepage, https://agno.com
|
|
@@ -157,7 +157,7 @@ Requires-Dist: mcp>=1.9.2; extra == "mcp"
|
|
|
157
157
|
Provides-Extra: mem0
|
|
158
158
|
Requires-Dist: mem0ai; extra == "mem0"
|
|
159
159
|
Provides-Extra: memori
|
|
160
|
-
Requires-Dist:
|
|
160
|
+
Requires-Dist: memori>=3.0.5; extra == "memori"
|
|
161
161
|
Provides-Extra: newspaper
|
|
162
162
|
Requires-Dist: newspaper4k; extra == "newspaper"
|
|
163
163
|
Requires-Dist: lxml_html_clean; extra == "newspaper"
|
|
@@ -425,23 +425,22 @@ Dynamic: license-file
|
|
|
425
425
|
|
|
426
426
|
Agno is a multi-agent framework, runtime, and control plane. Use it to build private and secure AI products that run in your cloud.
|
|
427
427
|
|
|
428
|
-
- **Build** agents, teams, and workflows with memory, knowledge, guardrails
|
|
428
|
+
- **Build** agents, teams, and workflows with memory, knowledge, guardrails and 100+ integrations.
|
|
429
429
|
- **Run** in production with a stateless FastAPI runtime. Horizontally scalable.
|
|
430
430
|
- **Manage** with a control plane that connects directly to your runtime — no data leaves your environment.
|
|
431
431
|
|
|
432
432
|
## Why Agno?
|
|
433
433
|
|
|
434
|
-
- **Your cloud, your data:**
|
|
435
|
-
- **
|
|
436
|
-
- **
|
|
434
|
+
- **Your cloud, your data:** Runs entirely in your infrastructure. Nothing leaves your environment.
|
|
435
|
+
- **Ready for production on day one:** Pre-built FastAPI runtime with SSE endpoints, ready to deploy.
|
|
436
|
+
- **Incredibly fast:** 529× faster than LangGraph, 24× lower memory.
|
|
437
437
|
|
|
438
438
|
## Getting Started
|
|
439
439
|
|
|
440
|
-
|
|
440
|
+
Start with the [getting started guide](https://github.com/agno-agi/agno/tree/main/cookbook/00_getting_started), then:
|
|
441
441
|
|
|
442
|
-
Then:
|
|
443
442
|
- Browse the [cookbooks](https://github.com/agno-agi/agno/tree/main/cookbook) for real-world examples
|
|
444
|
-
- Read the [docs](https://docs.agno.com) to learn more
|
|
443
|
+
- Read the [docs](https://docs.agno.com) to learn more
|
|
445
444
|
|
|
446
445
|
## Resources
|
|
447
446
|
|
|
@@ -509,26 +508,26 @@ This isn't a privacy mode or enterprise add-on. It's how Agno works.
|
|
|
509
508
|
|
|
510
509
|
## Features
|
|
511
510
|
|
|
512
|
-
|
|
511
|
+
### Core:
|
|
513
512
|
- Model agnostic — works with OpenAI, Anthropic, Google, local models, whatever
|
|
514
513
|
- Type-safe I/O with `input_schema` and `output_schema`
|
|
515
514
|
- Async-first, built for long-running tasks
|
|
516
515
|
- Natively multimodal (text, images, audio, video, files)
|
|
517
516
|
|
|
518
|
-
|
|
517
|
+
### Memory & Knowledge:
|
|
519
518
|
- Persistent storage for session history and state
|
|
520
519
|
- User memory that persists across sessions
|
|
521
520
|
- Agentic RAG with 20+ vector stores, hybrid search, reranking
|
|
522
521
|
- Culture — shared long-term memory across agents
|
|
523
522
|
|
|
524
|
-
|
|
523
|
+
### Execution:
|
|
525
524
|
- Human-in-the-loop (confirmations, approvals, overrides)
|
|
526
525
|
- Guardrails for validation and security
|
|
527
526
|
- Pre/post hooks for the agent lifecycle
|
|
528
527
|
- First-class MCP and A2A support
|
|
529
528
|
- 100+ built-in toolkits
|
|
530
529
|
|
|
531
|
-
|
|
530
|
+
### Production:
|
|
532
531
|
- Ready-to-use FastAPI runtime
|
|
533
532
|
- Integrated control plane UI
|
|
534
533
|
- Evals for accuracy, performance, latency
|
|
@@ -546,7 +545,7 @@ We're obsessive about performance because agent workloads spawn hundreds of inst
|
|
|
546
545
|
| Instantiation | **3μs** | 1,587μs (529× slower) | 170μs (57× slower) | 210μs (70× slower) |
|
|
547
546
|
| Memory | **6.6 KiB** | 161 KiB (24× higher) | 29 KiB (4× higher) | 66 KiB (10× higher) |
|
|
548
547
|
|
|
549
|
-
Run the benchmarks yourself: [`cookbook/
|
|
548
|
+
Run the benchmarks yourself: [`cookbook/12_evals/performance`](https://github.com/agno-agi/agno/tree/main/cookbook/12_evals/performance)
|
|
550
549
|
|
|
551
550
|
https://github.com/user-attachments/assets/54b98576-1859-4880-9f2d-15e1a426719d
|
|
552
551
|
|