agno 2.3.20__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 (58) 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/eval/agent_as_judge.py +24 -14
  14. agno/knowledge/chunking/document.py +3 -2
  15. agno/knowledge/chunking/markdown.py +8 -3
  16. agno/knowledge/chunking/recursive.py +2 -2
  17. agno/knowledge/embedder/mistral.py +1 -1
  18. agno/models/openai/chat.py +1 -1
  19. agno/models/openai/responses.py +14 -7
  20. agno/os/middleware/jwt.py +66 -27
  21. agno/os/routers/agents/router.py +2 -2
  22. agno/os/routers/evals/evals.py +0 -9
  23. agno/os/routers/evals/utils.py +6 -6
  24. agno/os/routers/knowledge/knowledge.py +3 -3
  25. agno/os/routers/teams/router.py +2 -2
  26. agno/os/routers/workflows/router.py +2 -2
  27. agno/reasoning/deepseek.py +11 -1
  28. agno/reasoning/gemini.py +6 -2
  29. agno/reasoning/groq.py +8 -3
  30. agno/reasoning/openai.py +2 -0
  31. agno/remote/base.py +105 -8
  32. agno/run/agent.py +19 -19
  33. agno/run/team.py +19 -19
  34. agno/skills/__init__.py +17 -0
  35. agno/skills/agent_skills.py +370 -0
  36. agno/skills/errors.py +32 -0
  37. agno/skills/loaders/__init__.py +4 -0
  38. agno/skills/loaders/base.py +27 -0
  39. agno/skills/loaders/local.py +216 -0
  40. agno/skills/skill.py +65 -0
  41. agno/skills/utils.py +107 -0
  42. agno/skills/validator.py +277 -0
  43. agno/team/remote.py +219 -59
  44. agno/team/team.py +22 -2
  45. agno/tools/mcp/mcp.py +299 -17
  46. agno/tools/mcp/multi_mcp.py +269 -14
  47. agno/utils/mcp.py +49 -8
  48. agno/utils/string.py +43 -1
  49. agno/workflow/condition.py +4 -2
  50. agno/workflow/loop.py +20 -1
  51. agno/workflow/remote.py +172 -32
  52. agno/workflow/router.py +4 -1
  53. agno/workflow/steps.py +4 -0
  54. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/METADATA +59 -130
  55. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/RECORD +58 -44
  56. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
  57. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
  58. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/top_level.txt +0 -0
agno/run/agent.py CHANGED
@@ -55,8 +55,11 @@ class RunInput:
55
55
  return self.input_content.model_dump_json(exclude_none=True)
56
56
  elif isinstance(self.input_content, Message):
57
57
  return json.dumps(self.input_content.to_dict())
58
- elif isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], Message):
59
- return json.dumps([m.to_dict() for m in self.input_content])
58
+ elif isinstance(self.input_content, list):
59
+ try:
60
+ return json.dumps(self.to_dict().get("input_content"))
61
+ except Exception:
62
+ return str(self.input_content)
60
63
  else:
61
64
  return str(self.input_content)
62
65
 
@@ -71,22 +74,15 @@ class RunInput:
71
74
  result["input_content"] = self.input_content.model_dump(exclude_none=True)
72
75
  elif isinstance(self.input_content, Message):
73
76
  result["input_content"] = self.input_content.to_dict()
74
-
75
- # Handle input_content provided as a list of Message objects
76
- elif (
77
- isinstance(self.input_content, list)
78
- and self.input_content
79
- and isinstance(self.input_content[0], Message)
80
- ):
81
- result["input_content"] = [m.to_dict() for m in self.input_content]
82
-
83
- # Handle input_content provided as a list of dicts
84
- elif (
85
- isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], dict)
86
- ):
87
- for content in self.input_content:
88
- # Handle media input
89
- if isinstance(content, dict):
77
+ elif isinstance(self.input_content, list):
78
+ serialized_items: List[Any] = []
79
+ for item in self.input_content:
80
+ if isinstance(item, Message):
81
+ serialized_items.append(item.to_dict())
82
+ elif isinstance(item, BaseModel):
83
+ serialized_items.append(item.model_dump(exclude_none=True))
84
+ elif isinstance(item, dict):
85
+ content = dict(item)
90
86
  if content.get("images"):
91
87
  content["images"] = [
92
88
  img.to_dict() if isinstance(img, Image) else img for img in content["images"]
@@ -103,7 +99,11 @@ class RunInput:
103
99
  content["files"] = [
104
100
  file.to_dict() if isinstance(file, File) else file for file in content["files"]
105
101
  ]
106
- result["input_content"] = self.input_content
102
+ serialized_items.append(content)
103
+ else:
104
+ serialized_items.append(item)
105
+
106
+ result["input_content"] = serialized_items
107
107
  else:
108
108
  result["input_content"] = self.input_content
109
109
 
agno/run/team.py CHANGED
@@ -51,8 +51,11 @@ class TeamRunInput:
51
51
  return self.input_content.model_dump_json(exclude_none=True)
52
52
  elif isinstance(self.input_content, Message):
53
53
  return json.dumps(self.input_content.to_dict())
54
- elif isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], Message):
55
- return json.dumps([m.to_dict() for m in self.input_content])
54
+ elif isinstance(self.input_content, list):
55
+ try:
56
+ return json.dumps(self.to_dict().get("input_content"))
57
+ except Exception:
58
+ return str(self.input_content)
56
59
  else:
57
60
  return str(self.input_content)
58
61
 
@@ -67,22 +70,15 @@ class TeamRunInput:
67
70
  result["input_content"] = self.input_content.model_dump(exclude_none=True)
68
71
  elif isinstance(self.input_content, Message):
69
72
  result["input_content"] = self.input_content.to_dict()
70
-
71
- # Handle input_content provided as a list of Message objects
72
- elif (
73
- isinstance(self.input_content, list)
74
- and self.input_content
75
- and isinstance(self.input_content[0], Message)
76
- ):
77
- result["input_content"] = [m.to_dict() for m in self.input_content]
78
-
79
- # Handle input_content provided as a list of dicts
80
- elif (
81
- isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], dict)
82
- ):
83
- for content in self.input_content:
84
- # Handle media input
85
- if isinstance(content, dict):
73
+ elif isinstance(self.input_content, list):
74
+ serialized_items: List[Any] = []
75
+ for item in self.input_content:
76
+ if isinstance(item, Message):
77
+ serialized_items.append(item.to_dict())
78
+ elif isinstance(item, BaseModel):
79
+ serialized_items.append(item.model_dump(exclude_none=True))
80
+ elif isinstance(item, dict):
81
+ content = dict(item)
86
82
  if content.get("images"):
87
83
  content["images"] = [
88
84
  img.to_dict() if isinstance(img, Image) else img for img in content["images"]
@@ -99,7 +95,11 @@ class TeamRunInput:
99
95
  content["files"] = [
100
96
  file.to_dict() if isinstance(file, File) else file for file in content["files"]
101
97
  ]
102
- result["input_content"] = self.input_content
98
+ serialized_items.append(content)
99
+ else:
100
+ serialized_items.append(item)
101
+
102
+ result["input_content"] = serialized_items
103
103
  else:
104
104
  result["input_content"] = self.input_content
105
105
 
@@ -0,0 +1,17 @@
1
+ from agno.skills.agent_skills import Skills
2
+ from agno.skills.errors import SkillError, SkillParseError, SkillValidationError
3
+ from agno.skills.loaders import LocalSkills, SkillLoader
4
+ from agno.skills.skill import Skill
5
+ from agno.skills.validator import validate_metadata, validate_skill_directory
6
+
7
+ __all__ = [
8
+ "Skills",
9
+ "LocalSkills",
10
+ "SkillLoader",
11
+ "Skill",
12
+ "SkillError",
13
+ "SkillParseError",
14
+ "SkillValidationError",
15
+ "validate_metadata",
16
+ "validate_skill_directory",
17
+ ]
@@ -0,0 +1,370 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+ from agno.skills.errors import SkillValidationError
7
+ from agno.skills.loaders.base import SkillLoader
8
+ from agno.skills.skill import Skill
9
+ from agno.skills.utils import is_safe_path, read_file_safe, run_script
10
+ from agno.tools.function import Function
11
+ from agno.utils.log import log_debug, log_warning
12
+
13
+
14
+ class Skills:
15
+ """Orchestrates skill loading and provides tools for agents to access skills.
16
+
17
+ The Skills class is responsible for:
18
+ 1. Loading skills from various sources (loaders)
19
+ 2. Providing methods to access loaded skills
20
+ 3. Generating tools for agents to use skills
21
+ 4. Creating system prompt snippets with available skills metadata
22
+
23
+ Args:
24
+ loaders: List of SkillLoader instances to load skills from.
25
+ """
26
+
27
+ def __init__(self, loaders: List[SkillLoader]):
28
+ self.loaders = loaders
29
+ self._skills: Dict[str, Skill] = {}
30
+ self._load_skills()
31
+
32
+ def _load_skills(self) -> None:
33
+ """Load skills from all loaders.
34
+
35
+ Raises:
36
+ SkillValidationError: If any skill fails validation.
37
+ """
38
+ for loader in self.loaders:
39
+ try:
40
+ skills = loader.load()
41
+ for skill in skills:
42
+ if skill.name in self._skills:
43
+ log_warning(f"Duplicate skill name '{skill.name}', overwriting with newer version")
44
+ self._skills[skill.name] = skill
45
+ except SkillValidationError:
46
+ raise # Re-raise validation errors as hard failures
47
+ except Exception as e:
48
+ log_warning(f"Error loading skills from {loader}: {e}")
49
+
50
+ log_debug(f"Loaded {len(self._skills)} total skills")
51
+
52
+ def reload(self) -> None:
53
+ """Reload skills from all loaders, clearing existing skills.
54
+
55
+ Raises:
56
+ SkillValidationError: If any skill fails validation.
57
+ """
58
+ self._skills.clear()
59
+ self._load_skills()
60
+
61
+ def get_skill(self, name: str) -> Optional[Skill]:
62
+ """Get a skill by name.
63
+
64
+ Args:
65
+ name: The name of the skill to retrieve.
66
+
67
+ Returns:
68
+ The Skill object if found, None otherwise.
69
+ """
70
+ return self._skills.get(name)
71
+
72
+ def get_all_skills(self) -> List[Skill]:
73
+ """Get all loaded skills.
74
+
75
+ Returns:
76
+ A list of all loaded Skill objects.
77
+ """
78
+ return list(self._skills.values())
79
+
80
+ def get_skill_names(self) -> List[str]:
81
+ """Get the names of all loaded skills.
82
+
83
+ Returns:
84
+ A list of skill names.
85
+ """
86
+ return list(self._skills.keys())
87
+
88
+ def get_system_prompt_snippet(self) -> str:
89
+ """Generate a system prompt snippet with available skills metadata.
90
+
91
+ This creates an XML-formatted snippet that provides the agent with
92
+ information about available skills without including the full instructions.
93
+
94
+ Returns:
95
+ An XML-formatted string with skills metadata.
96
+ """
97
+ if not self._skills:
98
+ return ""
99
+
100
+ lines = [
101
+ "<skills_system>",
102
+ "",
103
+ "## What are Skills?",
104
+ "Skills are packages of domain expertise that extend your capabilities. Each skill contains:",
105
+ "- **Instructions**: Detailed guidance on when and how to apply the skill",
106
+ "- **Scripts**: Executable code templates you can use or adapt",
107
+ "- **References**: Supporting documentation (guides, cheatsheets, examples)",
108
+ "",
109
+ "## Progressive Discovery",
110
+ "Skill information is designed to be loaded on-demand to keep your context focused:",
111
+ "1. **Browse**: Review the skill summaries below to understand what's available",
112
+ "2. **Load**: When a task matches a skill, use `get_skill_instructions` to load full guidance",
113
+ "3. **Reference**: Use `get_skill_reference` to access specific documentation as needed",
114
+ "4. **Scripts**: Use `get_skill_script` to read or execute scripts from a skill",
115
+ "",
116
+ "This approach ensures you only load detailed instructions when actually needed.",
117
+ "",
118
+ "## Available Skills",
119
+ ]
120
+ for skill in self._skills.values():
121
+ lines.append("<skill>")
122
+ lines.append(f" <name>{skill.name}</name>")
123
+ lines.append(f" <description>{skill.description}</description>")
124
+ if skill.scripts:
125
+ script_names = [s["name"] if isinstance(s, dict) else s for s in skill.scripts]
126
+ lines.append(f" <scripts>{', '.join(script_names)}</scripts>")
127
+ if skill.references:
128
+ ref_names = [r["name"] if isinstance(r, dict) else r for r in skill.references]
129
+ lines.append(f" <references>{', '.join(ref_names)}</references>")
130
+ lines.append("</skill>")
131
+ lines.append("")
132
+ lines.append("</skills_system>")
133
+
134
+ return "\n".join(lines)
135
+
136
+ def get_tools(self) -> List[Function]:
137
+ """Get the tools for accessing skills.
138
+
139
+ Returns:
140
+ A list of Function objects that agents can use to access skills.
141
+ """
142
+ tools: List[Function] = []
143
+
144
+ # Tool: get_skill_instructions
145
+ tools.append(
146
+ Function(
147
+ name="get_skill_instructions",
148
+ description="Load the full instructions for a skill. Use this when you need to follow a skill's guidance.",
149
+ entrypoint=self._get_skill_instructions,
150
+ )
151
+ )
152
+
153
+ # Tool: get_skill_reference
154
+ tools.append(
155
+ Function(
156
+ name="get_skill_reference",
157
+ description="Load a reference document from a skill's references. Use this to access detailed documentation.",
158
+ entrypoint=self._get_skill_reference,
159
+ )
160
+ )
161
+
162
+ # Tool: get_skill_script
163
+ tools.append(
164
+ Function(
165
+ name="get_skill_script",
166
+ description="Read or execute a script from a skill. Set execute=True to run the script and get output, or execute=False (default) to read the script content.",
167
+ entrypoint=self._get_skill_script,
168
+ )
169
+ )
170
+
171
+ return tools
172
+
173
+ def _get_skill_instructions(self, skill_name: str) -> str:
174
+ """Load the full instructions for a skill.
175
+
176
+ Args:
177
+ skill_name: The name of the skill to get instructions for.
178
+
179
+ Returns:
180
+ A JSON string with the skill's instructions and metadata.
181
+ """
182
+ skill = self.get_skill(skill_name)
183
+ if skill is None:
184
+ available = ", ".join(self.get_skill_names())
185
+ return json.dumps(
186
+ {
187
+ "error": f"Skill '{skill_name}' not found",
188
+ "available_skills": available,
189
+ }
190
+ )
191
+
192
+ return json.dumps(
193
+ {
194
+ "skill_name": skill.name,
195
+ "description": skill.description,
196
+ "instructions": skill.instructions,
197
+ "available_scripts": skill.scripts,
198
+ "available_references": skill.references,
199
+ }
200
+ )
201
+
202
+ def _get_skill_reference(self, skill_name: str, reference_path: str) -> str:
203
+ """Load a reference document from a skill.
204
+
205
+ Args:
206
+ skill_name: The name of the skill.
207
+ reference_path: The filename of the reference document.
208
+
209
+ Returns:
210
+ A JSON string with the reference content.
211
+ """
212
+ skill = self.get_skill(skill_name)
213
+ if skill is None:
214
+ available = ", ".join(self.get_skill_names())
215
+ return json.dumps(
216
+ {
217
+ "error": f"Skill '{skill_name}' not found",
218
+ "available_skills": available,
219
+ }
220
+ )
221
+
222
+ if reference_path not in skill.references:
223
+ return json.dumps(
224
+ {
225
+ "error": f"Reference '{reference_path}' not found in skill '{skill_name}'",
226
+ "available_references": skill.references,
227
+ }
228
+ )
229
+
230
+ # Validate path to prevent path traversal attacks
231
+ refs_dir = Path(skill.source_path) / "references"
232
+ if not is_safe_path(refs_dir, reference_path):
233
+ return json.dumps(
234
+ {
235
+ "error": f"Invalid reference path: '{reference_path}'",
236
+ "skill_name": skill_name,
237
+ }
238
+ )
239
+
240
+ # Load the reference file
241
+ ref_file = refs_dir / reference_path
242
+ try:
243
+ content = read_file_safe(ref_file)
244
+ return json.dumps(
245
+ {
246
+ "skill_name": skill_name,
247
+ "reference_path": reference_path,
248
+ "content": content,
249
+ }
250
+ )
251
+ except Exception as e:
252
+ return json.dumps(
253
+ {
254
+ "error": f"Error reading reference file: {e}",
255
+ "skill_name": skill_name,
256
+ "reference_path": reference_path,
257
+ }
258
+ )
259
+
260
+ def _get_skill_script(
261
+ self,
262
+ skill_name: str,
263
+ script_path: str,
264
+ execute: bool = False,
265
+ args: Optional[List[str]] = None,
266
+ timeout: int = 30,
267
+ ) -> str:
268
+ """Read or execute a script from a skill.
269
+
270
+ Args:
271
+ skill_name: The name of the skill.
272
+ script_path: The filename of the script.
273
+ execute: If True, execute the script. If False (default), return content.
274
+ args: Optional list of arguments to pass to the script (only used if execute=True).
275
+ timeout: Maximum execution time in seconds (default: 30, only used if execute=True).
276
+
277
+ Returns:
278
+ A JSON string with either the script content or execution results.
279
+ """
280
+ skill = self.get_skill(skill_name)
281
+ if skill is None:
282
+ available = ", ".join(self.get_skill_names())
283
+ return json.dumps(
284
+ {
285
+ "error": f"Skill '{skill_name}' not found",
286
+ "available_skills": available,
287
+ }
288
+ )
289
+
290
+ if script_path not in skill.scripts:
291
+ return json.dumps(
292
+ {
293
+ "error": f"Script '{script_path}' not found in skill '{skill_name}'",
294
+ "available_scripts": skill.scripts,
295
+ }
296
+ )
297
+
298
+ # Validate path to prevent path traversal attacks
299
+ scripts_dir = Path(skill.source_path) / "scripts"
300
+ if not is_safe_path(scripts_dir, script_path):
301
+ return json.dumps(
302
+ {
303
+ "error": f"Invalid script path: '{script_path}'",
304
+ "skill_name": skill_name,
305
+ }
306
+ )
307
+
308
+ script_file = scripts_dir / script_path
309
+
310
+ if not execute:
311
+ # Read mode: return script content
312
+ try:
313
+ content = read_file_safe(script_file)
314
+ return json.dumps(
315
+ {
316
+ "skill_name": skill_name,
317
+ "script_path": script_path,
318
+ "content": content,
319
+ }
320
+ )
321
+ except Exception as e:
322
+ return json.dumps(
323
+ {
324
+ "error": f"Error reading script file: {e}",
325
+ "skill_name": skill_name,
326
+ "script_path": script_path,
327
+ }
328
+ )
329
+
330
+ # Execute mode: run the script
331
+ try:
332
+ result = run_script(
333
+ script_path=script_file,
334
+ args=args,
335
+ timeout=timeout,
336
+ cwd=Path(skill.source_path),
337
+ )
338
+ return json.dumps(
339
+ {
340
+ "skill_name": skill_name,
341
+ "script_path": script_path,
342
+ "stdout": result.stdout,
343
+ "stderr": result.stderr,
344
+ "returncode": result.returncode,
345
+ }
346
+ )
347
+ except subprocess.TimeoutExpired:
348
+ return json.dumps(
349
+ {
350
+ "error": f"Script execution timed out after {timeout} seconds",
351
+ "skill_name": skill_name,
352
+ "script_path": script_path,
353
+ }
354
+ )
355
+ except FileNotFoundError as e:
356
+ return json.dumps(
357
+ {
358
+ "error": f"Interpreter or script not found: {e}",
359
+ "skill_name": skill_name,
360
+ "script_path": script_path,
361
+ }
362
+ )
363
+ except Exception as e:
364
+ return json.dumps(
365
+ {
366
+ "error": f"Error executing script: {e}",
367
+ "skill_name": skill_name,
368
+ "script_path": script_path,
369
+ }
370
+ )
agno/skills/errors.py ADDED
@@ -0,0 +1,32 @@
1
+ """Skill-related exceptions."""
2
+
3
+ from typing import List, Optional
4
+
5
+
6
+ class SkillError(Exception):
7
+ """Base exception for all skill-related errors."""
8
+
9
+ pass
10
+
11
+
12
+ class SkillParseError(SkillError):
13
+ """Raised when SKILL.md parsing fails."""
14
+
15
+ pass
16
+
17
+
18
+ class SkillValidationError(SkillError):
19
+ """Raised when skill validation fails.
20
+
21
+ Attributes:
22
+ errors: List of validation error messages.
23
+ """
24
+
25
+ def __init__(self, message: str, errors: Optional[List[str]] = None):
26
+ super().__init__(message)
27
+ self.errors = errors if errors is not None else [message]
28
+
29
+ def __str__(self) -> str:
30
+ if len(self.errors) == 1:
31
+ return self.errors[0]
32
+ return f"{len(self.errors)} validation errors: {'; '.join(self.errors)}"
@@ -0,0 +1,4 @@
1
+ from agno.skills.loaders.base import SkillLoader
2
+ from agno.skills.loaders.local import LocalSkills
3
+
4
+ __all__ = ["SkillLoader", "LocalSkills"]
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+ from agno.skills.skill import Skill
5
+
6
+
7
+ class SkillLoader(ABC):
8
+ """Abstract base class for skill loaders.
9
+
10
+ Skill loaders are responsible for loading skills from various sources
11
+ (local filesystem, GitHub, URLs, etc.) and returning them as Skill objects.
12
+
13
+ Subclasses must implement the `load()` method to define how skills
14
+ are loaded from their specific source.
15
+ """
16
+
17
+ @abstractmethod
18
+ def load(self) -> List[Skill]:
19
+ """Load skills from the source.
20
+
21
+ Returns:
22
+ A list of Skill objects loaded from the source.
23
+
24
+ Raises:
25
+ SkillLoadError: If there's an error loading skills from the source.
26
+ """
27
+ pass