agno 2.3.21__py3-none-any.whl → 2.3.22__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 +26 -1
- agno/agent/remote.py +233 -72
- 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/postgres/async_postgres.py +162 -40
- agno/db/postgres/postgres.py +181 -31
- agno/db/postgres/utils.py +6 -2
- agno/knowledge/chunking/document.py +3 -2
- agno/knowledge/chunking/markdown.py +8 -3
- agno/knowledge/chunking/recursive.py +2 -2
- 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 +2 -2
- agno/os/routers/knowledge/knowledge.py +3 -3
- agno/os/routers/teams/router.py +2 -2
- agno/os/routers/workflows/router.py +2 -2
- 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 +105 -8
- 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 +219 -59
- agno/team/team.py +22 -2
- agno/tools/mcp/mcp.py +299 -17
- agno/tools/mcp/multi_mcp.py +269 -14
- 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 +172 -32
- agno/workflow/router.py +4 -1
- agno/workflow/steps.py +4 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/METADATA +13 -14
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/RECORD +52 -38
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/top_level.txt +0 -0
agno/team/remote.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import time
|
|
3
2
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Literal, Optional, Sequence, Tuple, Union, overload
|
|
4
3
|
|
|
5
4
|
from pydantic import BaseModel
|
|
@@ -27,19 +26,25 @@ class RemoteTeam(BaseRemote):
|
|
|
27
26
|
base_url: str,
|
|
28
27
|
team_id: str,
|
|
29
28
|
timeout: float = 300.0,
|
|
29
|
+
protocol: Literal["agentos", "a2a"] = "agentos",
|
|
30
|
+
a2a_protocol: Literal["json-rpc", "rest"] = "rest",
|
|
30
31
|
config_ttl: float = 300.0,
|
|
31
32
|
):
|
|
32
|
-
"""Initialize
|
|
33
|
+
"""Initialize RemoteTeam for remote execution.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Supports two protocols:
|
|
36
|
+
- "agentos": Agno's proprietary AgentOS REST API (default)
|
|
37
|
+
- "a2a": A2A (Agent-to-Agent) protocol for cross-framework communication
|
|
35
38
|
|
|
36
39
|
Args:
|
|
37
|
-
base_url: Base URL for remote
|
|
38
|
-
team_id: ID of remote team
|
|
40
|
+
base_url: Base URL for remote instance (e.g., "http://localhost:7777")
|
|
41
|
+
team_id: ID of remote team on the remote server
|
|
39
42
|
timeout: Request timeout in seconds (default: 300)
|
|
43
|
+
protocol: Communication protocol - "agentos" (default) or "a2a"
|
|
44
|
+
a2a_protocol: For A2A protocol only - Whether to use JSON-RPC or REST protocol.
|
|
40
45
|
config_ttl: Time-to-live for cached config in seconds (default: 300)
|
|
41
46
|
"""
|
|
42
|
-
super().__init__(base_url, timeout, config_ttl)
|
|
47
|
+
super().__init__(base_url, timeout, protocol, a2a_protocol, config_ttl)
|
|
43
48
|
self.team_id = team_id
|
|
44
49
|
self._cached_team_config = None
|
|
45
50
|
|
|
@@ -48,48 +53,95 @@ class RemoteTeam(BaseRemote):
|
|
|
48
53
|
return self.team_id
|
|
49
54
|
|
|
50
55
|
async def get_team_config(self) -> "TeamResponse":
|
|
51
|
-
"""
|
|
52
|
-
|
|
56
|
+
"""
|
|
57
|
+
Get the team config from remote.
|
|
58
|
+
|
|
59
|
+
- For AgentOS protocol, always fetches fresh config from the remote.
|
|
60
|
+
- For A2A protocol, returns a minimal TeamResponse because A2A servers
|
|
61
|
+
do not expose detailed config endpoints.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
TeamResponse: The remote team configuration.
|
|
65
|
+
"""
|
|
66
|
+
from agno.os.routers.teams.schema import TeamResponse
|
|
67
|
+
|
|
68
|
+
if self.a2a_client:
|
|
69
|
+
from agno.client.a2a.schemas import AgentCard
|
|
70
|
+
|
|
71
|
+
agent_card: Optional[AgentCard] = await self.a2a_client.aget_agent_card()
|
|
72
|
+
|
|
73
|
+
return TeamResponse(
|
|
74
|
+
id=self.team_id,
|
|
75
|
+
name=agent_card.name if agent_card else self.team_id,
|
|
76
|
+
description=agent_card.description if agent_card else f"A2A team: {self.team_id}",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Fetch fresh config from remote for AgentOS
|
|
80
|
+
return await self.agentos_client.aget_team(self.team_id) # type: ignore
|
|
53
81
|
|
|
54
82
|
@property
|
|
55
|
-
def _team_config(self) -> "TeamResponse":
|
|
56
|
-
"""
|
|
83
|
+
def _team_config(self) -> Optional["TeamResponse"]:
|
|
84
|
+
"""
|
|
85
|
+
Get the team config from remote, cached with TTL.
|
|
86
|
+
|
|
87
|
+
- Returns None for A2A protocol (no config available).
|
|
88
|
+
- For AgentOS protocol, uses TTL caching for efficiency.
|
|
89
|
+
"""
|
|
90
|
+
import time
|
|
91
|
+
|
|
57
92
|
from agno.os.routers.teams.schema import TeamResponse
|
|
58
93
|
|
|
59
|
-
|
|
94
|
+
if self.a2a_client:
|
|
95
|
+
from agno.client.a2a.schemas import AgentCard
|
|
96
|
+
|
|
97
|
+
agent_card: Optional[AgentCard] = self.a2a_client.get_agent_card()
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
return TeamResponse(
|
|
100
|
+
id=self.team_id,
|
|
101
|
+
name=agent_card.name if agent_card else self.team_id,
|
|
102
|
+
description=agent_card.description if agent_card else f"A2A team: {self.team_id}",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
current_time = time.time()
|
|
62
106
|
if self._cached_team_config is not None:
|
|
63
107
|
config, cached_at = self._cached_team_config
|
|
64
108
|
if current_time - cached_at < self.config_ttl:
|
|
65
109
|
return config
|
|
66
110
|
|
|
67
|
-
# Fetch fresh config
|
|
68
|
-
config: TeamResponse = self.
|
|
111
|
+
# Fetch fresh config and update cache
|
|
112
|
+
config: TeamResponse = self.agentos_client.get_team(self.team_id) # type: ignore
|
|
69
113
|
self._cached_team_config = (config, current_time)
|
|
70
114
|
return config
|
|
71
115
|
|
|
72
|
-
def refresh_config(self) -> "TeamResponse":
|
|
73
|
-
"""
|
|
116
|
+
async def refresh_config(self) -> Optional["TeamResponse"]:
|
|
117
|
+
"""
|
|
118
|
+
Force refresh the cached team config from remote.
|
|
119
|
+
"""
|
|
120
|
+
import time
|
|
121
|
+
|
|
74
122
|
from agno.os.routers.teams.schema import TeamResponse
|
|
75
123
|
|
|
76
|
-
|
|
124
|
+
if self.a2a_client:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
config: TeamResponse = await self.agentos_client.aget_team(self.team_id) # type: ignore
|
|
77
128
|
self._cached_team_config = (config, time.time())
|
|
78
129
|
return config
|
|
79
130
|
|
|
80
131
|
@property
|
|
81
132
|
def name(self) -> Optional[str]:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
133
|
+
config = self._team_config
|
|
134
|
+
if config is not None:
|
|
135
|
+
return config.name
|
|
136
|
+
return self.team_id
|
|
85
137
|
|
|
86
138
|
@property
|
|
87
139
|
def description(self) -> Optional[str]:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
config = self._team_config
|
|
141
|
+
if config is not None:
|
|
142
|
+
return config.description
|
|
143
|
+
return ""
|
|
91
144
|
|
|
92
|
-
@property
|
|
93
145
|
def role(self) -> Optional[str]:
|
|
94
146
|
if self._team_config is not None:
|
|
95
147
|
return self._team_config.role
|
|
@@ -107,10 +159,15 @@ class RemoteTeam(BaseRemote):
|
|
|
107
159
|
|
|
108
160
|
@property
|
|
109
161
|
def db(self) -> Optional[RemoteDb]:
|
|
110
|
-
if
|
|
162
|
+
if (
|
|
163
|
+
self.agentos_client
|
|
164
|
+
and self._config
|
|
165
|
+
and self._team_config is not None
|
|
166
|
+
and self._team_config.db_id is not None
|
|
167
|
+
):
|
|
111
168
|
return RemoteDb.from_config(
|
|
112
169
|
db_id=self._team_config.db_id,
|
|
113
|
-
client=self.
|
|
170
|
+
client=self.agentos_client,
|
|
114
171
|
config=self._config,
|
|
115
172
|
)
|
|
116
173
|
return None
|
|
@@ -118,12 +175,12 @@ class RemoteTeam(BaseRemote):
|
|
|
118
175
|
@property
|
|
119
176
|
def knowledge(self) -> Optional[RemoteKnowledge]:
|
|
120
177
|
"""Whether the team has knowledge enabled."""
|
|
121
|
-
if self._team_config is not None and self._team_config.knowledge is not None:
|
|
178
|
+
if self.agentos_client and self._team_config is not None and self._team_config.knowledge is not None:
|
|
122
179
|
return RemoteKnowledge(
|
|
123
|
-
client=self.
|
|
180
|
+
client=self.agentos_client,
|
|
124
181
|
contents_db=RemoteDb(
|
|
125
182
|
id=self._team_config.knowledge.get("db_id"), # type: ignore
|
|
126
|
-
client=self.
|
|
183
|
+
client=self.agentos_client,
|
|
127
184
|
knowledge_table_name=self._team_config.knowledge.get("knowledge_table"),
|
|
128
185
|
)
|
|
129
186
|
if self._team_config.knowledge.get("db_id") is not None
|
|
@@ -219,52 +276,155 @@ class RemoteTeam(BaseRemote):
|
|
|
219
276
|
serialized_input = serialize_input(validated_input)
|
|
220
277
|
headers = self._get_auth_headers(auth_token)
|
|
221
278
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return self.
|
|
225
|
-
team_id=self.team_id,
|
|
279
|
+
# A2A protocol path
|
|
280
|
+
if self.a2a_client:
|
|
281
|
+
return self._arun_a2a( # type: ignore[return-value]
|
|
226
282
|
message=serialized_input,
|
|
227
|
-
|
|
283
|
+
stream=stream or False,
|
|
228
284
|
user_id=user_id,
|
|
285
|
+
context_id=session_id, # Map session_id → context_id for A2A
|
|
229
286
|
audio=audio,
|
|
230
287
|
images=images,
|
|
231
288
|
videos=videos,
|
|
232
289
|
files=files,
|
|
233
|
-
session_state=session_state,
|
|
234
|
-
stream_events=stream_events,
|
|
235
|
-
retries=retries,
|
|
236
|
-
knowledge_filters=knowledge_filters,
|
|
237
|
-
add_history_to_context=add_history_to_context,
|
|
238
|
-
add_dependencies_to_context=add_dependencies_to_context,
|
|
239
|
-
add_session_state_to_context=add_session_state_to_context,
|
|
240
|
-
dependencies=dependencies,
|
|
241
|
-
metadata=metadata,
|
|
242
290
|
headers=headers,
|
|
243
|
-
**kwargs,
|
|
244
291
|
)
|
|
292
|
+
|
|
293
|
+
# AgentOS protocol path (default)
|
|
294
|
+
if self.agentos_client:
|
|
295
|
+
if stream:
|
|
296
|
+
# Handle streaming response
|
|
297
|
+
return self.agentos_client.run_team_stream( # type: ignore
|
|
298
|
+
team_id=self.team_id,
|
|
299
|
+
message=serialized_input,
|
|
300
|
+
session_id=session_id,
|
|
301
|
+
user_id=user_id,
|
|
302
|
+
audio=audio,
|
|
303
|
+
images=images,
|
|
304
|
+
videos=videos,
|
|
305
|
+
files=files,
|
|
306
|
+
session_state=session_state,
|
|
307
|
+
stream_events=stream_events,
|
|
308
|
+
retries=retries,
|
|
309
|
+
knowledge_filters=knowledge_filters,
|
|
310
|
+
add_history_to_context=add_history_to_context,
|
|
311
|
+
add_dependencies_to_context=add_dependencies_to_context,
|
|
312
|
+
add_session_state_to_context=add_session_state_to_context,
|
|
313
|
+
dependencies=dependencies,
|
|
314
|
+
metadata=metadata,
|
|
315
|
+
headers=headers,
|
|
316
|
+
**kwargs,
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
return self.agentos_client.run_team( # type: ignore
|
|
320
|
+
team_id=self.team_id,
|
|
321
|
+
message=serialized_input,
|
|
322
|
+
session_id=session_id,
|
|
323
|
+
user_id=user_id,
|
|
324
|
+
audio=audio,
|
|
325
|
+
images=images,
|
|
326
|
+
videos=videos,
|
|
327
|
+
files=files,
|
|
328
|
+
session_state=session_state,
|
|
329
|
+
stream_events=stream_events,
|
|
330
|
+
retries=retries,
|
|
331
|
+
knowledge_filters=knowledge_filters,
|
|
332
|
+
add_history_to_context=add_history_to_context,
|
|
333
|
+
add_dependencies_to_context=add_dependencies_to_context,
|
|
334
|
+
add_session_state_to_context=add_session_state_to_context,
|
|
335
|
+
dependencies=dependencies,
|
|
336
|
+
metadata=metadata,
|
|
337
|
+
headers=headers,
|
|
338
|
+
**kwargs,
|
|
339
|
+
)
|
|
245
340
|
else:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
341
|
+
raise ValueError("No client available")
|
|
342
|
+
|
|
343
|
+
def _arun_a2a(
|
|
344
|
+
self,
|
|
345
|
+
message: str,
|
|
346
|
+
stream: bool,
|
|
347
|
+
user_id: Optional[str],
|
|
348
|
+
context_id: Optional[str],
|
|
349
|
+
audio: Optional[Sequence[Audio]],
|
|
350
|
+
images: Optional[Sequence[Image]],
|
|
351
|
+
videos: Optional[Sequence[Video]],
|
|
352
|
+
files: Optional[Sequence[File]],
|
|
353
|
+
headers: Optional[Dict[str, str]],
|
|
354
|
+
) -> Union[TeamRunOutput, AsyncIterator[TeamRunOutputEvent]]:
|
|
355
|
+
"""Execute via A2A protocol.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
message: Serialized message string
|
|
359
|
+
stream: Whether to stream the response
|
|
360
|
+
user_id: User identifier
|
|
361
|
+
context_id: Session/context ID (maps to session_id)
|
|
362
|
+
audio: Audio files to include
|
|
363
|
+
images: Images to include
|
|
364
|
+
videos: Videos to include
|
|
365
|
+
files: Files to include
|
|
366
|
+
headers: HTTP headers to include in the request (optional)
|
|
367
|
+
Returns:
|
|
368
|
+
TeamRunOutput for non-streaming, AsyncIterator[TeamRunOutputEvent] for streaming
|
|
369
|
+
"""
|
|
370
|
+
from agno.client.a2a.utils import map_stream_events_to_team_run_events
|
|
371
|
+
|
|
372
|
+
if not self.a2a_client:
|
|
373
|
+
raise ValueError("A2A client not available")
|
|
374
|
+
if stream:
|
|
375
|
+
# Return async generator for streaming
|
|
376
|
+
event_stream = self.a2a_client.stream_message(
|
|
377
|
+
message=message,
|
|
378
|
+
context_id=context_id,
|
|
250
379
|
user_id=user_id,
|
|
380
|
+
audio=list(audio) if audio else None,
|
|
381
|
+
images=list(images) if images else None,
|
|
382
|
+
videos=list(videos) if videos else None,
|
|
383
|
+
files=list(files) if files else None,
|
|
384
|
+
headers=headers,
|
|
385
|
+
)
|
|
386
|
+
return map_stream_events_to_team_run_events(event_stream, team_id=self.team_id)
|
|
387
|
+
else:
|
|
388
|
+
# Return coroutine for non-streaming
|
|
389
|
+
return self._arun_a2a_send( # type: ignore[return-value]
|
|
390
|
+
message=message,
|
|
391
|
+
user_id=user_id,
|
|
392
|
+
context_id=context_id,
|
|
251
393
|
audio=audio,
|
|
252
394
|
images=images,
|
|
253
395
|
videos=videos,
|
|
254
396
|
files=files,
|
|
255
|
-
session_state=session_state,
|
|
256
|
-
stream_events=stream_events,
|
|
257
|
-
retries=retries,
|
|
258
|
-
knowledge_filters=knowledge_filters,
|
|
259
|
-
add_history_to_context=add_history_to_context,
|
|
260
|
-
add_dependencies_to_context=add_dependencies_to_context,
|
|
261
|
-
add_session_state_to_context=add_session_state_to_context,
|
|
262
|
-
dependencies=dependencies,
|
|
263
|
-
metadata=metadata,
|
|
264
397
|
headers=headers,
|
|
265
|
-
**kwargs,
|
|
266
398
|
)
|
|
267
399
|
|
|
400
|
+
async def _arun_a2a_send(
|
|
401
|
+
self,
|
|
402
|
+
message: str,
|
|
403
|
+
user_id: Optional[str],
|
|
404
|
+
context_id: Optional[str],
|
|
405
|
+
audio: Optional[Sequence[Audio]],
|
|
406
|
+
images: Optional[Sequence[Image]],
|
|
407
|
+
videos: Optional[Sequence[Video]],
|
|
408
|
+
files: Optional[Sequence[File]],
|
|
409
|
+
headers: Optional[Dict[str, str]],
|
|
410
|
+
) -> TeamRunOutput:
|
|
411
|
+
"""Send a non-streaming A2A message and convert response to TeamRunOutput."""
|
|
412
|
+
if not self.a2a_client:
|
|
413
|
+
raise ValueError("A2A client not available")
|
|
414
|
+
from agno.client.a2a.utils import map_task_result_to_team_run_output
|
|
415
|
+
|
|
416
|
+
task_result = await self.a2a_client.send_message(
|
|
417
|
+
message=message,
|
|
418
|
+
context_id=context_id,
|
|
419
|
+
user_id=user_id,
|
|
420
|
+
images=list(images) if images else None,
|
|
421
|
+
audio=list(audio) if audio else None,
|
|
422
|
+
videos=list(videos) if videos else None,
|
|
423
|
+
files=list(files) if files else None,
|
|
424
|
+
headers=headers,
|
|
425
|
+
)
|
|
426
|
+
return map_task_result_to_team_run_output(task_result, team_id=self.team_id, user_id=user_id)
|
|
427
|
+
|
|
268
428
|
async def cancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
|
|
269
429
|
"""Cancel a running team execution.
|
|
270
430
|
|
|
@@ -277,7 +437,7 @@ class RemoteTeam(BaseRemote):
|
|
|
277
437
|
"""
|
|
278
438
|
headers = self._get_auth_headers(auth_token)
|
|
279
439
|
try:
|
|
280
|
-
await self.
|
|
440
|
+
await self.agentos_client.cancel_team_run( # type: ignore
|
|
281
441
|
team_id=self.team_id,
|
|
282
442
|
run_id=run_id,
|
|
283
443
|
headers=headers,
|
agno/team/team.py
CHANGED
|
@@ -5135,7 +5135,17 @@ class Team:
|
|
|
5135
5135
|
|
|
5136
5136
|
try:
|
|
5137
5137
|
sig = signature(value)
|
|
5138
|
-
|
|
5138
|
+
|
|
5139
|
+
# Build kwargs for the function
|
|
5140
|
+
kwargs: Dict[str, Any] = {}
|
|
5141
|
+
if "agent" in sig.parameters:
|
|
5142
|
+
kwargs["agent"] = self
|
|
5143
|
+
if "team" in sig.parameters:
|
|
5144
|
+
kwargs["team"] = self
|
|
5145
|
+
if "run_context" in sig.parameters:
|
|
5146
|
+
kwargs["run_context"] = run_context
|
|
5147
|
+
|
|
5148
|
+
resolved_value = value(**kwargs) if kwargs else value()
|
|
5139
5149
|
|
|
5140
5150
|
run_context.dependencies[key] = resolved_value
|
|
5141
5151
|
except Exception as e:
|
|
@@ -5156,7 +5166,17 @@ class Team:
|
|
|
5156
5166
|
|
|
5157
5167
|
try:
|
|
5158
5168
|
sig = signature(value)
|
|
5159
|
-
|
|
5169
|
+
|
|
5170
|
+
# Build kwargs for the function
|
|
5171
|
+
kwargs: Dict[str, Any] = {}
|
|
5172
|
+
if "agent" in sig.parameters:
|
|
5173
|
+
kwargs["agent"] = self
|
|
5174
|
+
if "team" in sig.parameters:
|
|
5175
|
+
kwargs["team"] = self
|
|
5176
|
+
if "run_context" in sig.parameters:
|
|
5177
|
+
kwargs["run_context"] = run_context
|
|
5178
|
+
|
|
5179
|
+
resolved_value = value(**kwargs) if kwargs else value()
|
|
5160
5180
|
|
|
5161
5181
|
if iscoroutine(resolved_value):
|
|
5162
5182
|
resolved_value = await resolved_value
|