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.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {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 discovering and managing skills"""
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 discover_skills(self) -> None:
30
- """Discover skills from the skills directory"""
31
- if self._discovered:
32
- return
33
-
34
- # Get the skills directory path
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
- # Scan for skill directories
38
- for item in skills_dir.iterdir():
39
- if item.is_dir() and not item.name.startswith('__'):
40
- self._load_skill_from_directory(item)
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
- self._discovered = True
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
- # Check if we're in raw mode (used by swaig-test --raw) and suppress logging
45
- is_raw_mode = "--raw" in sys.argv
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 _load_skill_from_directory(self, skill_dir: Path) -> None:
50
- """Load a skill from a directory"""
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
- # Import the skill module
57
- module_name = f"signalwire_agents.skills.{skill_dir.name}.skill"
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.SKILL_NAME is not None):
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 {skill_dir}: {e}")
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
- """Register a skill class"""
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
- self.discover_skills() # Ensure skills are discovered
86
- return self._skills.get(skill_name)
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 registered skills with metadata"""
90
- self.discover_skills()
91
- return [
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
- "name": skill_class.SKILL_NAME,
94
- "description": skill_class.SKILL_DESCRIPTION,
95
- "version": skill_class.SKILL_VERSION,
96
- "required_packages": skill_class.REQUIRED_PACKAGES,
97
- "required_env_vars": skill_class.REQUIRED_ENV_VARS,
98
- "supports_multiple_instances": skill_class.SUPPORTS_MULTIPLE_INSTANCES
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
- for skill_class in self._skills.values()
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()