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.
Files changed (52) hide show
  1. agno/agent/agent.py +26 -1
  2. agno/agent/remote.py +233 -72
  3. agno/client/a2a/__init__.py +10 -0
  4. agno/client/a2a/client.py +554 -0
  5. agno/client/a2a/schemas.py +112 -0
  6. agno/client/a2a/utils.py +369 -0
  7. agno/db/migrations/utils.py +19 -0
  8. agno/db/migrations/v1_to_v2.py +54 -16
  9. agno/db/migrations/versions/v2_3_0.py +92 -53
  10. agno/db/postgres/async_postgres.py +162 -40
  11. agno/db/postgres/postgres.py +181 -31
  12. agno/db/postgres/utils.py +6 -2
  13. agno/knowledge/chunking/document.py +3 -2
  14. agno/knowledge/chunking/markdown.py +8 -3
  15. agno/knowledge/chunking/recursive.py +2 -2
  16. agno/models/openai/chat.py +1 -1
  17. agno/models/openai/responses.py +14 -7
  18. agno/os/middleware/jwt.py +66 -27
  19. agno/os/routers/agents/router.py +2 -2
  20. agno/os/routers/knowledge/knowledge.py +3 -3
  21. agno/os/routers/teams/router.py +2 -2
  22. agno/os/routers/workflows/router.py +2 -2
  23. agno/reasoning/deepseek.py +11 -1
  24. agno/reasoning/gemini.py +6 -2
  25. agno/reasoning/groq.py +8 -3
  26. agno/reasoning/openai.py +2 -0
  27. agno/remote/base.py +105 -8
  28. agno/skills/__init__.py +17 -0
  29. agno/skills/agent_skills.py +370 -0
  30. agno/skills/errors.py +32 -0
  31. agno/skills/loaders/__init__.py +4 -0
  32. agno/skills/loaders/base.py +27 -0
  33. agno/skills/loaders/local.py +216 -0
  34. agno/skills/skill.py +65 -0
  35. agno/skills/utils.py +107 -0
  36. agno/skills/validator.py +277 -0
  37. agno/team/remote.py +219 -59
  38. agno/team/team.py +22 -2
  39. agno/tools/mcp/mcp.py +299 -17
  40. agno/tools/mcp/multi_mcp.py +269 -14
  41. agno/utils/mcp.py +49 -8
  42. agno/utils/string.py +43 -1
  43. agno/workflow/condition.py +4 -2
  44. agno/workflow/loop.py +20 -1
  45. agno/workflow/remote.py +172 -32
  46. agno/workflow/router.py +4 -1
  47. agno/workflow/steps.py +4 -0
  48. {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/METADATA +13 -14
  49. {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/RECORD +52 -38
  50. {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
  51. {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
  52. {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 AgentOSRunner for local or remote execution.
33
+ """Initialize RemoteTeam for remote execution.
33
34
 
34
- For remote execution, provide base_url and team_id.
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 AgentOS instance (e.g., "http://localhost:7777")
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
- """Get the team config from remote (always fetches fresh)."""
52
- return await self.client.aget_team(self.team_id)
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
- """Get the team config from remote, cached with TTL."""
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
- current_time = time.time()
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
- # Check if cache is valid
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.client.get_team(self.team_id) # type: ignore
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
- """Force refresh the cached team config."""
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
- config: TeamResponse = self.client.get_team(self.team_id) # type: ignore
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
- if self._team_config is not None:
83
- return self._team_config.name
84
- return None
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
- if self._team_config is not None:
89
- return self._team_config.description
90
- return None
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 self._team_config is not None and self._team_config.db_id is not None:
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.client,
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.client,
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.client,
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
- if stream:
223
- # Handle streaming response
224
- return self.get_client().run_team_stream( # type: ignore
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
- session_id=session_id,
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
- return self.get_client().run_team( # type: ignore
247
- team_id=self.team_id,
248
- message=serialized_input,
249
- session_id=session_id,
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.get_client().cancel_team_run(
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
- resolved_value = value(agent=self) if "agent" in sig.parameters else value()
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
- resolved_value = value(team=self) if "team" in sig.parameters else value()
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