signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -12,51 +12,72 @@ import importlib
|
|
|
12
12
|
import importlib.util
|
|
13
13
|
import inspect
|
|
14
14
|
import sys
|
|
15
|
-
from typing import Dict, List, Type, Optional
|
|
15
|
+
from typing import Dict, List, Type, Optional, Any
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from signalwire_agents.core.skill_base import SkillBase
|
|
19
19
|
from signalwire_agents.core.logging_config import get_logger
|
|
20
20
|
|
|
21
21
|
class SkillRegistry:
|
|
22
|
-
"""Global registry for
|
|
22
|
+
"""Global registry for on-demand skill loading"""
|
|
23
23
|
|
|
24
24
|
def __init__(self):
|
|
25
25
|
self._skills: Dict[str, Type[SkillBase]] = {}
|
|
26
|
+
self._external_paths: List[Path] = [] # Additional paths to search for skills
|
|
27
|
+
self._entry_points_loaded = False
|
|
26
28
|
self.logger = get_logger("skill_registry")
|
|
27
|
-
self._discovered = False
|
|
28
29
|
|
|
29
|
-
def
|
|
30
|
-
"""
|
|
31
|
-
if self.
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
#
|
|
30
|
+
def _load_skill_on_demand(self, skill_name: str) -> Optional[Type[SkillBase]]:
|
|
31
|
+
"""Load a skill on-demand by name"""
|
|
32
|
+
if skill_name in self._skills:
|
|
33
|
+
return self._skills[skill_name]
|
|
34
|
+
|
|
35
|
+
# First, ensure entry points are loaded
|
|
36
|
+
self._load_entry_points()
|
|
37
|
+
|
|
38
|
+
# Check if skill was loaded from entry points
|
|
39
|
+
if skill_name in self._skills:
|
|
40
|
+
return self._skills[skill_name]
|
|
41
|
+
|
|
42
|
+
# Search in built-in skills directory
|
|
35
43
|
skills_dir = Path(__file__).parent
|
|
44
|
+
skill_class = self._load_skill_from_path(skill_name, skills_dir)
|
|
45
|
+
if skill_class:
|
|
46
|
+
return skill_class
|
|
36
47
|
|
|
37
|
-
#
|
|
38
|
-
for
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
# Search in external paths
|
|
49
|
+
for external_path in self._external_paths:
|
|
50
|
+
skill_class = self._load_skill_from_path(skill_name, external_path)
|
|
51
|
+
if skill_class:
|
|
52
|
+
return skill_class
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
# Search in environment variable paths
|
|
55
|
+
env_paths = os.environ.get('SIGNALWIRE_SKILL_PATHS', '').split(':')
|
|
56
|
+
for path_str in env_paths:
|
|
57
|
+
if path_str:
|
|
58
|
+
skill_class = self._load_skill_from_path(skill_name, Path(path_str))
|
|
59
|
+
if skill_class:
|
|
60
|
+
return skill_class
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if not is_raw_mode:
|
|
47
|
-
self.logger.info(f"Discovered {len(self._skills)} skills")
|
|
62
|
+
self.logger.debug(f"Skill '{skill_name}' not found in any registered paths")
|
|
63
|
+
return None
|
|
48
64
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
65
|
+
def _load_skill_from_path(self, skill_name: str, base_path: Path) -> Optional[Type[SkillBase]]:
|
|
66
|
+
"""Try to load a skill from a specific base path"""
|
|
67
|
+
skill_dir = base_path / skill_name
|
|
51
68
|
skill_file = skill_dir / "skill.py"
|
|
69
|
+
|
|
52
70
|
if not skill_file.exists():
|
|
53
|
-
return
|
|
71
|
+
return None
|
|
54
72
|
|
|
55
73
|
try:
|
|
56
|
-
#
|
|
57
|
-
module_name = f"
|
|
74
|
+
# Create unique module name to avoid conflicts
|
|
75
|
+
module_name = f"signalwire_agents_external.{base_path.name}.{skill_name}.skill"
|
|
58
76
|
spec = importlib.util.spec_from_file_location(module_name, skill_file)
|
|
59
77
|
module = importlib.util.module_from_spec(spec)
|
|
78
|
+
|
|
79
|
+
# Add to sys.modules to handle relative imports
|
|
80
|
+
sys.modules[module_name] = module
|
|
60
81
|
spec.loader.exec_module(module)
|
|
61
82
|
|
|
62
83
|
# Find SkillBase subclasses in the module
|
|
@@ -64,15 +85,85 @@ class SkillRegistry:
|
|
|
64
85
|
if (inspect.isclass(obj) and
|
|
65
86
|
issubclass(obj, SkillBase) and
|
|
66
87
|
obj != SkillBase and
|
|
67
|
-
obj
|
|
88
|
+
hasattr(obj, 'SKILL_NAME') and
|
|
89
|
+
obj.SKILL_NAME == skill_name): # Match exact skill name
|
|
68
90
|
|
|
69
91
|
self.register_skill(obj)
|
|
92
|
+
return obj
|
|
93
|
+
|
|
94
|
+
self.logger.warning(f"No skill class found with name '{skill_name}' in {skill_file}")
|
|
95
|
+
return None
|
|
70
96
|
|
|
71
97
|
except Exception as e:
|
|
72
|
-
self.logger.error(f"Failed to load skill from {
|
|
98
|
+
self.logger.error(f"Failed to load skill '{skill_name}' from {skill_file}: {e}")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def discover_skills(self) -> None:
|
|
102
|
+
"""Deprecated: Skills are now loaded on-demand"""
|
|
103
|
+
# Keep this method for backwards compatibility but make it a no-op
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def _load_skill_from_directory(self, skill_dir: Path) -> None:
|
|
107
|
+
"""Deprecated: Skills are now loaded on-demand"""
|
|
108
|
+
# Keep this method for backwards compatibility but make it a no-op
|
|
109
|
+
pass
|
|
73
110
|
|
|
74
111
|
def register_skill(self, skill_class: Type[SkillBase]) -> None:
|
|
75
|
-
"""
|
|
112
|
+
"""
|
|
113
|
+
Register a skill class directly
|
|
114
|
+
|
|
115
|
+
This allows third-party code to register skill classes without
|
|
116
|
+
requiring them to be in a specific directory structure.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
skill_class: A class that inherits from SkillBase
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
from my_custom_skills import MyWeatherSkill
|
|
123
|
+
skill_registry.register_skill(MyWeatherSkill)
|
|
124
|
+
"""
|
|
125
|
+
if not issubclass(skill_class, SkillBase):
|
|
126
|
+
raise ValueError(f"{skill_class} must inherit from SkillBase")
|
|
127
|
+
|
|
128
|
+
if not hasattr(skill_class, 'SKILL_NAME') or skill_class.SKILL_NAME is None:
|
|
129
|
+
raise ValueError(f"{skill_class} must define SKILL_NAME")
|
|
130
|
+
|
|
131
|
+
# Validate that the skill has a proper parameter schema
|
|
132
|
+
if not hasattr(skill_class, 'get_parameter_schema') or not callable(getattr(skill_class, 'get_parameter_schema')):
|
|
133
|
+
raise ValueError(f"{skill_class.__name__} must have get_parameter_schema() classmethod")
|
|
134
|
+
|
|
135
|
+
# Try to call get_parameter_schema to ensure it's properly implemented
|
|
136
|
+
try:
|
|
137
|
+
schema = skill_class.get_parameter_schema()
|
|
138
|
+
if not isinstance(schema, dict):
|
|
139
|
+
raise ValueError(f"{skill_class.__name__}.get_parameter_schema() must return a dictionary, got {type(schema)}")
|
|
140
|
+
|
|
141
|
+
# Ensure it's not an empty schema (skills should at least have the base parameters)
|
|
142
|
+
if not schema:
|
|
143
|
+
raise ValueError(f"{skill_class.__name__}.get_parameter_schema() returned an empty dictionary. Skills should at least call super().get_parameter_schema()")
|
|
144
|
+
|
|
145
|
+
# Check if the skill has overridden the method (not just inherited base)
|
|
146
|
+
skill_method = getattr(skill_class, 'get_parameter_schema', None)
|
|
147
|
+
base_method = getattr(SkillBase, 'get_parameter_schema', None)
|
|
148
|
+
|
|
149
|
+
if skill_method and base_method:
|
|
150
|
+
# For class methods, check the underlying function
|
|
151
|
+
skill_func = skill_method.__func__ if hasattr(skill_method, '__func__') else skill_method
|
|
152
|
+
base_func = base_method.__func__ if hasattr(base_method, '__func__') else base_method
|
|
153
|
+
|
|
154
|
+
if skill_func is base_func:
|
|
155
|
+
# Get base schema to check if skill added any parameters
|
|
156
|
+
base_schema = SkillBase.get_parameter_schema()
|
|
157
|
+
if set(schema.keys()) == set(base_schema.keys()):
|
|
158
|
+
raise ValueError(f"{skill_class.__name__} must override get_parameter_schema() to define its specific parameters")
|
|
159
|
+
|
|
160
|
+
except AttributeError as e:
|
|
161
|
+
raise ValueError(f"{skill_class.__name__} must properly implement get_parameter_schema() classmethod")
|
|
162
|
+
except ValueError:
|
|
163
|
+
raise # Re-raise our validation errors
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise ValueError(f"{skill_class.__name__}.get_parameter_schema() failed: {e}")
|
|
166
|
+
|
|
76
167
|
if skill_class.SKILL_NAME in self._skills:
|
|
77
168
|
self.logger.warning(f"Skill '{skill_class.SKILL_NAME}' already registered")
|
|
78
169
|
return
|
|
@@ -81,24 +172,288 @@ class SkillRegistry:
|
|
|
81
172
|
self.logger.debug(f"Registered skill '{skill_class.SKILL_NAME}'")
|
|
82
173
|
|
|
83
174
|
def get_skill_class(self, skill_name: str) -> Optional[Type[SkillBase]]:
|
|
84
|
-
"""Get skill class by name"""
|
|
85
|
-
|
|
86
|
-
|
|
175
|
+
"""Get skill class by name, loading on-demand if needed"""
|
|
176
|
+
# First check if already loaded
|
|
177
|
+
if skill_name in self._skills:
|
|
178
|
+
return self._skills[skill_name]
|
|
179
|
+
|
|
180
|
+
# Try to load on-demand
|
|
181
|
+
return self._load_skill_on_demand(skill_name)
|
|
87
182
|
|
|
88
183
|
def list_skills(self) -> List[Dict[str, str]]:
|
|
89
|
-
"""List all
|
|
90
|
-
|
|
91
|
-
|
|
184
|
+
"""List all available skills by scanning directories (only when explicitly requested)"""
|
|
185
|
+
# Only scan when this method is explicitly called (e.g., for CLI tools)
|
|
186
|
+
skills_dir = Path(__file__).parent
|
|
187
|
+
available_skills = []
|
|
188
|
+
|
|
189
|
+
for item in skills_dir.iterdir():
|
|
190
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
191
|
+
skill_file = item / "skill.py"
|
|
192
|
+
if skill_file.exists():
|
|
193
|
+
# Try to load the skill to get its metadata
|
|
194
|
+
skill_class = self._load_skill_on_demand(item.name)
|
|
195
|
+
if skill_class:
|
|
196
|
+
available_skills.append({
|
|
197
|
+
"name": skill_class.SKILL_NAME,
|
|
198
|
+
"description": skill_class.SKILL_DESCRIPTION,
|
|
199
|
+
"version": skill_class.SKILL_VERSION,
|
|
200
|
+
"required_packages": skill_class.REQUIRED_PACKAGES,
|
|
201
|
+
"required_env_vars": skill_class.REQUIRED_ENV_VARS,
|
|
202
|
+
"supports_multiple_instances": skill_class.SUPPORTS_MULTIPLE_INSTANCES
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return available_skills
|
|
206
|
+
|
|
207
|
+
def get_all_skills_schema(self) -> Dict[str, Dict[str, Any]]:
|
|
208
|
+
"""
|
|
209
|
+
Get complete schema for all available skills including parameter metadata
|
|
210
|
+
|
|
211
|
+
This method scans all available skills and returns a comprehensive schema
|
|
212
|
+
that includes skill metadata and parameter definitions suitable for GUI
|
|
213
|
+
configuration or API documentation.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dict[str, Dict[str, Any]]: Complete skill schema where keys are skill names
|
|
217
|
+
and values contain:
|
|
218
|
+
- name: Skill name
|
|
219
|
+
- description: Skill description
|
|
220
|
+
- version: Skill version
|
|
221
|
+
- supports_multiple_instances: Whether multiple instances are allowed
|
|
222
|
+
- required_packages: List of required Python packages
|
|
223
|
+
- required_env_vars: List of required environment variables
|
|
224
|
+
- parameters: Parameter schema from get_parameter_schema()
|
|
225
|
+
- source: Where the skill was loaded from ('built-in', 'external', 'entry_point', 'registered')
|
|
226
|
+
|
|
227
|
+
Example:
|
|
92
228
|
{
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
229
|
+
"web_search": {
|
|
230
|
+
"name": "web_search",
|
|
231
|
+
"description": "Search the web for information",
|
|
232
|
+
"version": "1.0.0",
|
|
233
|
+
"supports_multiple_instances": True,
|
|
234
|
+
"required_packages": ["bs4", "requests"],
|
|
235
|
+
"required_env_vars": [],
|
|
236
|
+
"parameters": {
|
|
237
|
+
"api_key": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"description": "Google API key",
|
|
240
|
+
"required": True,
|
|
241
|
+
"hidden": True,
|
|
242
|
+
"env_var": "GOOGLE_SEARCH_API_KEY"
|
|
243
|
+
},
|
|
244
|
+
...
|
|
245
|
+
},
|
|
246
|
+
"source": "built-in"
|
|
247
|
+
}
|
|
99
248
|
}
|
|
100
|
-
|
|
101
|
-
|
|
249
|
+
"""
|
|
250
|
+
skills_schema = {}
|
|
251
|
+
|
|
252
|
+
# Load entry points first
|
|
253
|
+
self._load_entry_points()
|
|
254
|
+
|
|
255
|
+
# Helper function to add skill to schema
|
|
256
|
+
def add_skill_to_schema(skill_class, source):
|
|
257
|
+
try:
|
|
258
|
+
# Get parameter schema
|
|
259
|
+
try:
|
|
260
|
+
parameter_schema = skill_class.get_parameter_schema()
|
|
261
|
+
except AttributeError:
|
|
262
|
+
# Skill doesn't implement get_parameter_schema yet
|
|
263
|
+
parameter_schema = {}
|
|
264
|
+
|
|
265
|
+
skills_schema[skill_class.SKILL_NAME] = {
|
|
266
|
+
"name": skill_class.SKILL_NAME,
|
|
267
|
+
"description": skill_class.SKILL_DESCRIPTION,
|
|
268
|
+
"version": getattr(skill_class, 'SKILL_VERSION', '1.0.0'),
|
|
269
|
+
"supports_multiple_instances": getattr(skill_class, 'SUPPORTS_MULTIPLE_INSTANCES', False),
|
|
270
|
+
"required_packages": getattr(skill_class, 'REQUIRED_PACKAGES', []),
|
|
271
|
+
"required_env_vars": getattr(skill_class, 'REQUIRED_ENV_VARS', []),
|
|
272
|
+
"parameters": parameter_schema,
|
|
273
|
+
"source": source
|
|
274
|
+
}
|
|
275
|
+
except Exception as e:
|
|
276
|
+
self.logger.error(f"Failed to get schema for skill '{skill_class.SKILL_NAME}': {e}")
|
|
277
|
+
|
|
278
|
+
# Add already registered skills first (includes entry points)
|
|
279
|
+
for skill_name, skill_class in self._skills.items():
|
|
280
|
+
add_skill_to_schema(skill_class, 'registered')
|
|
281
|
+
|
|
282
|
+
# Scan built-in skills directory
|
|
283
|
+
skills_dir = Path(__file__).parent
|
|
284
|
+
for item in skills_dir.iterdir():
|
|
285
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
286
|
+
skill_file = item / "skill.py"
|
|
287
|
+
if skill_file.exists() and item.name not in skills_schema:
|
|
288
|
+
try:
|
|
289
|
+
skill_class = self._load_skill_on_demand(item.name)
|
|
290
|
+
if skill_class:
|
|
291
|
+
add_skill_to_schema(skill_class, 'built-in')
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.logger.error(f"Failed to load skill '{item.name}': {e}")
|
|
294
|
+
|
|
295
|
+
# Scan external directories
|
|
296
|
+
for external_path in self._external_paths:
|
|
297
|
+
if external_path.exists():
|
|
298
|
+
for item in external_path.iterdir():
|
|
299
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
300
|
+
skill_file = item / "skill.py"
|
|
301
|
+
if skill_file.exists() and item.name not in skills_schema:
|
|
302
|
+
try:
|
|
303
|
+
skill_class = self._load_skill_on_demand(item.name)
|
|
304
|
+
if skill_class:
|
|
305
|
+
add_skill_to_schema(skill_class, 'external')
|
|
306
|
+
except Exception as e:
|
|
307
|
+
self.logger.error(f"Failed to load skill '{item.name}': {e}")
|
|
308
|
+
|
|
309
|
+
# Scan environment variable paths
|
|
310
|
+
env_paths = os.environ.get('SIGNALWIRE_SKILL_PATHS', '').split(':')
|
|
311
|
+
for path_str in env_paths:
|
|
312
|
+
if path_str:
|
|
313
|
+
env_path = Path(path_str)
|
|
314
|
+
if env_path.exists():
|
|
315
|
+
for item in env_path.iterdir():
|
|
316
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
317
|
+
skill_file = item / "skill.py"
|
|
318
|
+
if skill_file.exists() and item.name not in skills_schema:
|
|
319
|
+
try:
|
|
320
|
+
skill_class = self._load_skill_on_demand(item.name)
|
|
321
|
+
if skill_class:
|
|
322
|
+
add_skill_to_schema(skill_class, 'external')
|
|
323
|
+
except Exception as e:
|
|
324
|
+
self.logger.error(f"Failed to load skill '{item.name}': {e}")
|
|
325
|
+
|
|
326
|
+
return skills_schema
|
|
327
|
+
|
|
328
|
+
def add_skill_directory(self, path: str) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Add a directory to search for skills
|
|
331
|
+
|
|
332
|
+
This allows third-party skill collections to be registered by path.
|
|
333
|
+
Skills in these directories should follow the same structure as built-in skills:
|
|
334
|
+
- Each skill in its own subdirectory
|
|
335
|
+
- skill.py file containing the skill class
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
path: Path to directory containing skill subdirectories
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
skill_registry.add_skill_directory('/opt/custom_skills')
|
|
342
|
+
# Now agent.add_skill('my_custom_skill') will search in this directory
|
|
343
|
+
"""
|
|
344
|
+
skill_path = Path(path)
|
|
345
|
+
if not skill_path.exists():
|
|
346
|
+
raise ValueError(f"Skill directory does not exist: {path}")
|
|
347
|
+
if not skill_path.is_dir():
|
|
348
|
+
raise ValueError(f"Path is not a directory: {path}")
|
|
349
|
+
|
|
350
|
+
if skill_path not in self._external_paths:
|
|
351
|
+
self._external_paths.append(skill_path)
|
|
352
|
+
self.logger.info(f"Added external skill directory: {path}")
|
|
353
|
+
|
|
354
|
+
def _load_entry_points(self) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Load skills from Python entry points
|
|
357
|
+
|
|
358
|
+
This allows installed packages to register skills via setup.py:
|
|
359
|
+
|
|
360
|
+
entry_points={
|
|
361
|
+
'signalwire_agents.skills': [
|
|
362
|
+
'weather = my_package.skills:WeatherSkill',
|
|
363
|
+
'stock = my_package.skills:StockSkill',
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
"""
|
|
367
|
+
if self._entry_points_loaded:
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
self._entry_points_loaded = True
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
import pkg_resources
|
|
374
|
+
|
|
375
|
+
for entry_point in pkg_resources.iter_entry_points('signalwire_agents.skills'):
|
|
376
|
+
try:
|
|
377
|
+
skill_class = entry_point.load()
|
|
378
|
+
if issubclass(skill_class, SkillBase):
|
|
379
|
+
self.register_skill(skill_class)
|
|
380
|
+
self.logger.info(f"Loaded skill '{skill_class.SKILL_NAME}' from entry point '{entry_point.name}'")
|
|
381
|
+
else:
|
|
382
|
+
self.logger.warning(f"Entry point '{entry_point.name}' does not provide a SkillBase subclass")
|
|
383
|
+
except Exception as e:
|
|
384
|
+
self.logger.error(f"Failed to load skill from entry point '{entry_point.name}': {e}")
|
|
385
|
+
|
|
386
|
+
except ImportError:
|
|
387
|
+
# pkg_resources not available, try importlib.metadata (Python 3.8+)
|
|
388
|
+
try:
|
|
389
|
+
from importlib import metadata
|
|
390
|
+
|
|
391
|
+
entry_points = metadata.entry_points()
|
|
392
|
+
if hasattr(entry_points, 'select'):
|
|
393
|
+
# Python 3.10+
|
|
394
|
+
skill_entries = entry_points.select(group='signalwire_agents.skills')
|
|
395
|
+
else:
|
|
396
|
+
# Python 3.8-3.9
|
|
397
|
+
skill_entries = entry_points.get('signalwire_agents.skills', [])
|
|
398
|
+
|
|
399
|
+
for entry_point in skill_entries:
|
|
400
|
+
try:
|
|
401
|
+
skill_class = entry_point.load()
|
|
402
|
+
if issubclass(skill_class, SkillBase):
|
|
403
|
+
self.register_skill(skill_class)
|
|
404
|
+
self.logger.info(f"Loaded skill '{skill_class.SKILL_NAME}' from entry point '{entry_point.name}'")
|
|
405
|
+
else:
|
|
406
|
+
self.logger.warning(f"Entry point '{entry_point.name}' does not provide a SkillBase subclass")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.logger.error(f"Failed to load skill from entry point '{entry_point.name}': {e}")
|
|
409
|
+
|
|
410
|
+
except ImportError:
|
|
411
|
+
# Neither pkg_resources nor importlib.metadata available
|
|
412
|
+
self.logger.debug("Entry point loading not available - install setuptools or use Python 3.8+")
|
|
413
|
+
|
|
414
|
+
def list_all_skill_sources(self) -> Dict[str, List[str]]:
|
|
415
|
+
"""
|
|
416
|
+
List all skill sources and the skills available from each
|
|
417
|
+
|
|
418
|
+
Returns a dictionary mapping source types to lists of skill names:
|
|
419
|
+
{
|
|
420
|
+
'built-in': ['datetime', 'math', ...],
|
|
421
|
+
'external_paths': ['custom_skill1', ...],
|
|
422
|
+
'entry_points': ['weather', ...],
|
|
423
|
+
'registered': ['my_skill', ...]
|
|
424
|
+
}
|
|
425
|
+
"""
|
|
426
|
+
sources = {
|
|
427
|
+
'built-in': [],
|
|
428
|
+
'external_paths': [],
|
|
429
|
+
'entry_points': [],
|
|
430
|
+
'registered': []
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Built-in skills
|
|
434
|
+
skills_dir = Path(__file__).parent
|
|
435
|
+
for item in skills_dir.iterdir():
|
|
436
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
437
|
+
skill_file = item / "skill.py"
|
|
438
|
+
if skill_file.exists():
|
|
439
|
+
sources['built-in'].append(item.name)
|
|
440
|
+
|
|
441
|
+
# External path skills
|
|
442
|
+
for external_path in self._external_paths:
|
|
443
|
+
if external_path.exists():
|
|
444
|
+
for item in external_path.iterdir():
|
|
445
|
+
if item.is_dir() and not item.name.startswith('__'):
|
|
446
|
+
skill_file = item / "skill.py"
|
|
447
|
+
if skill_file.exists():
|
|
448
|
+
sources['external_paths'].append(item.name)
|
|
449
|
+
|
|
450
|
+
# Already registered skills
|
|
451
|
+
for skill_name in self._skills:
|
|
452
|
+
# Determine source of registered skill
|
|
453
|
+
if skill_name not in sources['built-in']:
|
|
454
|
+
sources['registered'].append(skill_name)
|
|
455
|
+
|
|
456
|
+
return sources
|
|
102
457
|
|
|
103
458
|
# Global registry instance
|
|
104
459
|
skill_registry = SkillRegistry()
|