swarmauri_agent_skill 0.11.0.dev1__tar.gz → 0.11.0.dev5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarmauri_agent_skill
3
- Version: 0.11.0.dev1
3
+ Version: 0.11.0.dev5
4
4
  Summary: Skill-aware agent wrapper for Swarmauri.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -16,10 +16,10 @@ Classifier: Programming Language :: Python :: 3 :: Only
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
- Requires-Dist: swarmauri_base
20
- Requires-Dist: swarmauri_core
21
- Requires-Dist: swarmauri_standard
22
- Requires-Dist: swarmauri_tool_skill_execution
19
+ Requires-Dist: swarmauri_base (>=0.11.0.dev6,<0.12)
20
+ Requires-Dist: swarmauri_core (>=0.11.0.dev6,<0.12)
21
+ Requires-Dist: swarmauri_standard (>=0.11.0.dev8,<0.12)
22
+ Requires-Dist: swarmauri_tool_skill_execution (>=0.11.0.dev6,<0.12)
23
23
  Description-Content-Type: text/markdown
24
24
 
25
25
  # Swarmauri Skill Agent
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "swarmauri_agent_skill"
3
- version = "0.11.0.dev1"
3
+ version = "0.11.0.dev5"
4
4
  description = "Skill-aware agent wrapper for Swarmauri."
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -18,10 +18,10 @@ classifiers = [
18
18
  ]
19
19
  authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
20
20
  dependencies = [
21
- "swarmauri_core",
22
- "swarmauri_base",
23
- "swarmauri_standard",
24
- "swarmauri_tool_skill_execution",
21
+ "swarmauri_core>=0.11.0.dev6,<0.12",
22
+ "swarmauri_base>=0.11.0.dev6,<0.12",
23
+ "swarmauri_standard>=0.11.0.dev8,<0.12",
24
+ "swarmauri_tool_skill_execution>=0.11.0.dev6,<0.12",
25
25
  ]
26
26
  keywords = ["swarmauri", "sdk", "standards", "skills", "agents"]
27
27
 
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from swarmauri_base.toolkits.ToolkitBase import ToolkitBase
4
+
3
5
  import asyncio
4
6
  import inspect
5
- from pathlib import Path
6
7
  from typing import (
7
8
  Any,
8
9
  Dict,
@@ -14,7 +15,7 @@ from typing import (
14
15
  Union,
15
16
  )
16
17
 
17
- from pydantic import ConfigDict, Field
18
+ from pydantic import ConfigDict, Field, PrivateAttr
18
19
 
19
20
  from swarmauri_base.ComponentBase import ComponentBase, SubclassUnion
20
21
  from swarmauri_base.agents.AgentBase import AgentBase
@@ -25,8 +26,10 @@ from swarmauri_base.agents.AgentSystemContextMixin import (
25
26
  from swarmauri_base.conversations.ConversationBase import ConversationBase
26
27
  from swarmauri_base.llms.LLMBase import LLMBase
27
28
  from swarmauri_base.skills.SkillBase import SkillBase
29
+ from swarmauri_base.skills.SkillMetadata import SkillMetadata
28
30
  from swarmauri_base.tools.ToolBase import ToolBase
29
31
  from swarmauri_core.messages.IMessage import IMessage
32
+ from swarmauri_core.skills.ISkillLoader import ISkillLoader
30
33
  from swarmauri_standard.conversations.MaxSystemContextConversation import (
31
34
  MaxSystemContextConversation,
32
35
  )
@@ -46,9 +49,13 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
46
49
  )
47
50
  system_context: SystemMessage | str = SystemMessage(content="")
48
51
  skills: List[SubclassUnion[SkillBase]] = Field(default_factory=list)
52
+ skill_metadata: List[SkillMetadata] = Field(default_factory=list)
53
+ skill_loader: ISkillLoader | None = None
54
+ _skill_roots: List[str] = PrivateAttr(default_factory=list)
49
55
  skill_execution_tool: SubclassUnion[ToolBase] = Field(
50
56
  default_factory=SkillExecutionTool
51
57
  )
58
+ skillset_toolkit: ToolkitBase | None = Field(default=None, exclude=True)
52
59
  turn_mode: Literal["single", "multi"] = "multi"
53
60
  execution_mode: Literal["sequential", "concurrent"] = "sequential"
54
61
  require_skill: bool = False
@@ -57,6 +64,10 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
57
64
 
58
65
  def model_post_init(self, __context: Any) -> None:
59
66
  self.skills = [self._hydrate_skill(item) for item in self.skills]
67
+ if not self.skill_metadata:
68
+ self.skill_metadata = [
69
+ self._metadata_for_skill(skill) for skill in self.skills
70
+ ]
60
71
 
61
72
  @staticmethod
62
73
  def _hydrate_skill(value: Any) -> Any:
@@ -120,38 +131,98 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
120
131
  )
121
132
  return results
122
133
 
134
+ @classmethod
135
+ def from_skill_roots(
136
+ cls,
137
+ llm: LLMBase,
138
+ roots: Iterable[str],
139
+ loader_cls: Any,
140
+ **kwargs: Any,
141
+ ) -> "SkillAgent":
142
+ """Discover metadata now and activate only selected skills later."""
143
+ root_list = list(roots)
144
+ metadata = loader_cls.discover(root_list)
145
+ agent = cls(llm=llm, skill_metadata=metadata, **kwargs)
146
+ agent._skill_roots = [str(root) for root in root_list]
147
+ if isinstance(loader_cls, type) and issubclass(loader_cls, SkillBase):
148
+ # Shipped filesystem/local skill classes expose classmethod loader
149
+ # APIs but cannot be instantiated without skill fields.
150
+ agent.skill_loader = loader_cls
151
+ elif isinstance(loader_cls, type):
152
+ agent.skill_loader = loader_cls()
153
+ else:
154
+ agent.skill_loader = loader_cls
155
+ return agent
156
+
157
+ @staticmethod
158
+ def _metadata_for_skill(skill: SkillBase) -> SkillMetadata:
159
+ return SkillMetadata(
160
+ name=skill.name,
161
+ description=skill.description,
162
+ license=skill.license,
163
+ compatibility=skill.compatibility,
164
+ metadata=skill.metadata,
165
+ source="loaded",
166
+ location=str(getattr(skill, "root_path", "")),
167
+ )
168
+
169
+ def _activate_metadata(
170
+ self, selected_metadata: Sequence[SkillMetadata]
171
+ ) -> List[SkillBase]:
172
+ if not self.skill_loader:
173
+ return [
174
+ skill
175
+ for skill in self.skills
176
+ if skill.name in {item.name for item in selected_metadata}
177
+ ]
178
+ loaded = {skill.name: skill for skill in self.skills}
179
+ for metadata in selected_metadata:
180
+ if metadata.name in loaded:
181
+ continue
182
+ if hasattr(self.skill_loader, "from_name"):
183
+ skill = self.skill_loader.from_name(
184
+ metadata.name, roots=self._skill_roots
185
+ )
186
+ else:
187
+ skill = self.skill_loader.load(
188
+ metadata.location, skill_name=metadata.name
189
+ )
190
+ loaded[metadata.name] = skill
191
+ self.skills.append(skill)
192
+ return [loaded[item.name] for item in selected_metadata]
193
+
123
194
  def select_skills(
124
195
  self,
125
196
  input_data: Union[str, IMessage],
126
197
  skill_names: Optional[Sequence[str]] = None,
127
198
  ) -> List[SkillBase]:
199
+ catalog = self.skill_metadata
128
200
  if skill_names:
129
- selected = [
130
- skill
131
- for skill in self.skills
132
- if skill.name in set(skill_names)
201
+ requested = set(skill_names)
202
+ selected_metadata = [
203
+ item for item in catalog if item.name in requested
133
204
  ]
134
205
  missing = sorted(
135
- set(skill_names) - {skill.name for skill in selected}
206
+ requested - {item.name for item in selected_metadata}
136
207
  )
137
208
  if missing:
138
209
  raise ValueError(
139
210
  f"Unknown skill name(s): {', '.join(missing)}"
140
211
  )
141
- return selected
212
+ return self._activate_metadata(selected_metadata)
142
213
 
143
- if len(self.skills) == 1:
144
- return list(self.skills)
214
+ if len(catalog) == 1:
215
+ return self._activate_metadata(catalog)
145
216
 
146
217
  input_text = self._input_text(input_data).lower()
147
- selected = [
148
- skill
149
- for skill in self.skills
150
- if self._skill_matches_input(skill, input_text)
218
+ selected_metadata = [
219
+ item
220
+ for item in catalog
221
+ if self._skill_matches_input(item, input_text)
151
222
  ]
152
- if not selected and self.require_skill:
223
+ if not selected_metadata and self.require_skill:
153
224
  raise ValueError("No matching skill found for input")
154
- return selected
225
+ return self._activate_metadata(selected_metadata)
155
226
 
156
227
  def _exec_one(
157
228
  self,
@@ -248,9 +319,26 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
248
319
  selected_skills = list(skills if skills is not None else self.skills)
249
320
  self.skill_execution_tool.skills = selected_skills
250
321
  toolkit = Toolkit()
322
+ if self.skillset_toolkit is not None:
323
+ toolkit.add_tools(self.skillset_toolkit.available_tools())
251
324
  toolkit.add_tool(self.skill_execution_tool)
252
325
  return toolkit
253
326
 
327
+ def load_skill_resource(
328
+ self, skill_name: str, relative_path: str
329
+ ) -> bytes:
330
+ """Load one selected skill resource without eager context injection."""
331
+ for skill in self.skills:
332
+ if skill.name == skill_name:
333
+ reader = getattr(skill, "read_resource", None)
334
+ if reader is None:
335
+ raise ValueError(
336
+ f"Skill '{skill_name}' does not support "
337
+ "filesystem resources"
338
+ )
339
+ return reader(relative_path)
340
+ raise ValueError(f"Unknown skill name: {skill_name}")
341
+
254
342
  def _llm_kwargs(
255
343
  self,
256
344
  selected_skills: Iterable[SkillBase],
@@ -285,7 +373,8 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
285
373
  (
286
374
  "Available tool: SkillExecutionTool. Use this single "
287
375
  "tool "
288
- )(
376
+ ),
377
+ (
289
378
  "to run skill-local argv command arrays when execution " # noqa: E501
290
379
  "is "
291
380
  "needed."
@@ -299,39 +388,16 @@ class SkillAgent(AgentSystemContextMixin, AgentConversationMixin, AgentBase):
299
388
  def _build_resource_context(cls, skill: SkillBase) -> str:
300
389
  lines = ["## Skill Resources"]
301
390
  has_resources = False
302
- for field_name in SkillBase._RESOURCE_FIELDS:
303
- values = list(getattr(skill, field_name))
391
+ for field_name in ("references", "scripts", "assets"):
392
+ values = list(getattr(skill, field_name, []))
304
393
  if not values:
305
394
  continue
306
395
  has_resources = True
307
396
  lines.append(f"- {field_name}: {', '.join(values)}")
308
- if field_name in {"agents", "references"}:
309
- for value in values:
310
- content = cls._read_resource_content(skill, value)
311
- if content:
312
- lines.append(f"\n### {field_name}/{value}\n{content}")
313
397
  if not has_resources:
314
398
  lines.append("- none")
315
399
  return "\n".join(lines)
316
400
 
317
- @staticmethod
318
- def _read_resource_content(skill: SkillBase, resource_path: str) -> str:
319
- root_path = getattr(skill, "root_path", None)
320
- if not root_path:
321
- return ""
322
- root = Path(root_path).expanduser().resolve()
323
- path = (root / resource_path).resolve()
324
- try:
325
- path.relative_to(root)
326
- except ValueError:
327
- return ""
328
- if (
329
- path.suffix.lower() not in {".md", ".yaml", ".yml"}
330
- or not path.is_file()
331
- ):
332
- return ""
333
- return path.read_text(encoding="utf-8").strip()
334
-
335
401
  @classmethod
336
402
  def _skill_matches_input(cls, skill: SkillBase, input_text: str) -> bool:
337
403
  candidates = [skill.name, skill.description]