Coverage for agentos/tools/skill_tool.py: 0%
93 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-07 00:45 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-07 00:45 +0800
1"""
2SkillTool — 将 marketplace skill 包装为 BaseTool,通过 Bridge 注册到 ToolAgent。
4每个 skill 的 run(**kwargs) 函数变成 Agent 可直接调用的工具。
5"""
7from __future__ import annotations
9import importlib
10import inspect
11import json
12import os
13import sys
14from collections.abc import Callable
16from agentos.tools.base import BaseTool, ToolResult
19class SkillTool(BaseTool):
20 """将一个 marketplace skill 的 run() 函数包装为 BaseTool。
22 skill 的 run(**kwargs) 接收任意关键字参数并返回字符串。
23 """
25 permission_level = "safe" # type: ignore
27 def __init__(
28 self,
29 skill_name: str,
30 skill_run: Callable[..., str],
31 description: str = "",
32 parameters: dict | None = None,
33 ):
34 self._skill_name = skill_name
35 self._run_fn = skill_run
36 self._description = description
37 self._parameters = parameters
39 @property
40 def name(self) -> str:
41 return self._skill_name
43 @property
44 def description(self) -> str:
45 return self._description or f"Execute the '{self._skill_name}' skill."
47 @property
48 def parameters(self) -> dict:
49 if self._parameters:
50 return self._parameters
51 # Default: accept arbitrary kwargs
52 return {
53 "type": "object",
54 "properties": {
55 "kwargs": {
56 "type": "string",
57 "description": "JSON string of keyword arguments to pass to the skill",
58 }
59 },
60 }
62 async def execute(self, input_data: dict) -> ToolResult:
63 try:
64 # If input has 'kwargs', parse as JSON
65 if "kwargs" in input_data:
66 parsed = json.loads(input_data["kwargs"])
67 else:
68 parsed = input_data
70 result = self._run_fn(**parsed)
71 return ToolResult(call_id=self.name, output=str(result))
72 except Exception as e:
73 return ToolResult(call_id=self.name, error=str(e))
76def discover_skills(skills_dir: str = None) -> list[SkillTool]:
77 """自动发现 marketplace/skills 下所有 skill 并包装为 SkillTool。
79 Args:
80 skills_dir: skills 目录路径。默认自动定位。
82 Returns:
83 SkillTool 实例列表。
84 """
85 if skills_dir is None:
86 # Auto-locate
87 agentos_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
88 skills_dir = os.path.join(agentos_dir, "marketplace", "skills")
90 if not os.path.isdir(skills_dir):
91 return []
93 tools: list[SkillTool] = []
95 for entry in sorted(os.listdir(skills_dir)):
96 skill_path = os.path.join(skills_dir, entry)
97 if not os.path.isdir(skill_path):
98 continue
100 skill_py = os.path.join(skill_path, f"{entry}.py")
101 if not os.path.isfile(skill_py):
102 continue
104 try:
105 # Import the skill module
106 spec = importlib.util.spec_from_file_location(
107 f"agentos_marketplace_skill_{entry}", skill_py
108 )
109 if spec is None or spec.loader is None:
110 continue
111 mod = importlib.util.module_from_spec(spec)
112 sys.modules[spec.name] = mod
113 spec.loader.exec_module(mod)
115 run_fn = getattr(mod, "run", None)
116 if run_fn is None or not callable(run_fn):
117 continue
119 # Get docstring as description
120 desc = (mod.__doc__ or f"Execute the '{entry}' skill.").strip().split("\n")[0]
122 # Try to get parameter schema from function signature
123 params = _infer_parameters(run_fn)
125 tool = SkillTool(
126 skill_name=f"skill_{entry.replace('-', '_')}",
127 skill_run=run_fn,
128 description=desc,
129 parameters=params,
130 )
131 tools.append(tool)
132 except Exception:
133 # Skip skills that fail to load
134 continue
136 return tools
139def _infer_parameters(fn: Callable) -> dict:
140 """从函数签名推断 JSON Schema 参数定义。"""
141 try:
142 sig = inspect.signature(fn)
143 except (ValueError, TypeError):
144 return {
145 "type": "object",
146 "properties": {"kwargs": {"type": "string", "description": "JSON string of arguments"}},
147 }
149 properties = {}
150 required = []
152 for name, param in sig.parameters.items():
153 if name in ("self", "cls"):
154 continue
155 param_type = "string"
156 if param.annotation is not inspect.Parameter.empty:
157 anno = param.annotation
158 if anno is str:
159 param_type = "string"
160 elif anno is int:
161 param_type = "integer"
162 elif anno is float:
163 param_type = "number"
164 elif anno is bool:
165 param_type = "boolean"
166 elif anno is list:
167 param_type = "array"
169 properties[name] = {"type": param_type, "description": f"Parameter: {name}"}
171 if param.default is inspect.Parameter.empty:
172 required.append(name)
174 return {
175 "type": "object",
176 "properties": properties,
177 "required": required,
178 }