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.
- 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/eval/agent_as_judge.py +24 -14
- agno/knowledge/chunking/document.py +3 -2
- agno/knowledge/chunking/markdown.py +8 -3
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/embedder/mistral.py +1 -1
- 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/evals/evals.py +0 -9
- agno/os/routers/evals/utils.py +6 -6
- 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/run/agent.py +19 -19
- agno/run/team.py +19 -19
- 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.20.dist-info → agno-2.3.22.dist-info}/METADATA +59 -130
- {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/RECORD +58 -44
- {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
- {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
- {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)
|
|
59
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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)
|
|
55
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
agno/skills/__init__.py
ADDED
|
@@ -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,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
|