signalwire-agents 0.1.6__py3-none-any.whl → 1.0.7__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 (140) hide show
  1. signalwire_agents/__init__.py +130 -4
  2. signalwire_agents/agent_server.py +438 -32
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +18 -0
  5. signalwire_agents/cli/build_search.py +1367 -0
  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/execution/__init__.py +10 -0
  13. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  14. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  15. signalwire_agents/cli/init_project.py +1225 -0
  16. signalwire_agents/cli/output/__init__.py +10 -0
  17. signalwire_agents/cli/output/output_formatter.py +255 -0
  18. signalwire_agents/cli/output/swml_dump.py +186 -0
  19. signalwire_agents/cli/simulation/__init__.py +10 -0
  20. signalwire_agents/cli/simulation/data_generation.py +374 -0
  21. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  22. signalwire_agents/cli/simulation/mock_env.py +282 -0
  23. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  24. signalwire_agents/cli/test_swaig.py +809 -0
  25. signalwire_agents/cli/types.py +81 -0
  26. signalwire_agents/core/__init__.py +2 -2
  27. signalwire_agents/core/agent/__init__.py +12 -0
  28. signalwire_agents/core/agent/config/__init__.py +12 -0
  29. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  30. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  31. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  32. signalwire_agents/core/agent/prompt/manager.py +306 -0
  33. signalwire_agents/core/agent/routing/__init__.py +9 -0
  34. signalwire_agents/core/agent/security/__init__.py +9 -0
  35. signalwire_agents/core/agent/swml/__init__.py +9 -0
  36. signalwire_agents/core/agent/tools/__init__.py +15 -0
  37. signalwire_agents/core/agent/tools/decorator.py +97 -0
  38. signalwire_agents/core/agent/tools/registry.py +210 -0
  39. signalwire_agents/core/agent_base.py +959 -2166
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +707 -0
  43. signalwire_agents/core/data_map.py +487 -0
  44. signalwire_agents/core/function_result.py +1150 -1
  45. signalwire_agents/core/logging_config.py +376 -0
  46. signalwire_agents/core/mixins/__init__.py +28 -0
  47. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  48. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  49. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  50. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  51. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  52. signalwire_agents/core/mixins/state_mixin.py +153 -0
  53. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  54. signalwire_agents/core/mixins/web_mixin.py +1134 -0
  55. signalwire_agents/core/security/session_manager.py +174 -86
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +200 -0
  58. signalwire_agents/core/skill_manager.py +244 -0
  59. signalwire_agents/core/swaig_function.py +33 -9
  60. signalwire_agents/core/swml_builder.py +212 -12
  61. signalwire_agents/core/swml_handler.py +43 -13
  62. signalwire_agents/core/swml_renderer.py +123 -297
  63. signalwire_agents/core/swml_service.py +277 -260
  64. signalwire_agents/prefabs/concierge.py +6 -2
  65. signalwire_agents/prefabs/info_gatherer.py +149 -33
  66. signalwire_agents/prefabs/receptionist.py +14 -22
  67. signalwire_agents/prefabs/survey.py +6 -2
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +137 -0
  70. signalwire_agents/search/document_processor.py +1223 -0
  71. signalwire_agents/search/index_builder.py +804 -0
  72. signalwire_agents/search/migration.py +418 -0
  73. signalwire_agents/search/models.py +30 -0
  74. signalwire_agents/search/pgvector_backend.py +752 -0
  75. signalwire_agents/search/query_processor.py +502 -0
  76. signalwire_agents/search/search_engine.py +1264 -0
  77. signalwire_agents/search/search_service.py +574 -0
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +23 -0
  80. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  81. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  82. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  83. signalwire_agents/skills/datasphere/README.md +210 -0
  84. signalwire_agents/skills/datasphere/__init__.py +12 -0
  85. signalwire_agents/skills/datasphere/skill.py +310 -0
  86. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  87. signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
  88. signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
  89. signalwire_agents/skills/datetime/README.md +132 -0
  90. signalwire_agents/skills/datetime/__init__.py +10 -0
  91. signalwire_agents/skills/datetime/skill.py +126 -0
  92. signalwire_agents/skills/joke/README.md +149 -0
  93. signalwire_agents/skills/joke/__init__.py +10 -0
  94. signalwire_agents/skills/joke/skill.py +109 -0
  95. signalwire_agents/skills/math/README.md +161 -0
  96. signalwire_agents/skills/math/__init__.py +10 -0
  97. signalwire_agents/skills/math/skill.py +105 -0
  98. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  99. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  100. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  101. signalwire_agents/skills/native_vector_search/README.md +210 -0
  102. signalwire_agents/skills/native_vector_search/__init__.py +10 -0
  103. signalwire_agents/skills/native_vector_search/skill.py +820 -0
  104. signalwire_agents/skills/play_background_file/README.md +218 -0
  105. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  106. signalwire_agents/skills/play_background_file/skill.py +242 -0
  107. signalwire_agents/skills/registry.py +459 -0
  108. signalwire_agents/skills/spider/README.md +236 -0
  109. signalwire_agents/skills/spider/__init__.py +13 -0
  110. signalwire_agents/skills/spider/skill.py +598 -0
  111. signalwire_agents/skills/swml_transfer/README.md +395 -0
  112. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  113. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  114. signalwire_agents/skills/weather_api/README.md +178 -0
  115. signalwire_agents/skills/weather_api/__init__.py +12 -0
  116. signalwire_agents/skills/weather_api/skill.py +191 -0
  117. signalwire_agents/skills/web_search/README.md +163 -0
  118. signalwire_agents/skills/web_search/__init__.py +10 -0
  119. signalwire_agents/skills/web_search/skill.py +739 -0
  120. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  121. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  122. signalwire_agents/skills/wikipedia_search/skill.py +210 -0
  123. signalwire_agents/utils/__init__.py +14 -0
  124. signalwire_agents/utils/schema_utils.py +111 -44
  125. signalwire_agents/web/__init__.py +17 -0
  126. signalwire_agents/web/web_service.py +559 -0
  127. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  128. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  129. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  130. signalwire_agents-1.0.7.dist-info/METADATA +992 -0
  131. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  132. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
  133. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  134. signalwire_agents/core/state/file_state_manager.py +0 -219
  135. signalwire_agents/core/state/state_manager.py +0 -101
  136. signalwire_agents-0.1.6.data/data/schema.json +0 -5611
  137. signalwire_agents-0.1.6.dist-info/METADATA +0 -199
  138. signalwire_agents-0.1.6.dist-info/RECORD +0 -34
  139. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  140. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,459 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ import os
11
+ import importlib
12
+ import importlib.util
13
+ import inspect
14
+ import sys
15
+ from typing import Dict, List, Type, Optional, Any
16
+ from pathlib import Path
17
+
18
+ from signalwire_agents.core.skill_base import SkillBase
19
+ from signalwire_agents.core.logging_config import get_logger
20
+
21
+ class SkillRegistry:
22
+ """Global registry for on-demand skill loading"""
23
+
24
+ def __init__(self):
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
28
+ self.logger = get_logger("skill_registry")
29
+
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
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
47
+
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
53
+
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
61
+
62
+ self.logger.debug(f"Skill '{skill_name}' not found in any registered paths")
63
+ return None
64
+
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
68
+ skill_file = skill_dir / "skill.py"
69
+
70
+ if not skill_file.exists():
71
+ return None
72
+
73
+ try:
74
+ # Create unique module name to avoid conflicts
75
+ module_name = f"signalwire_agents_external.{base_path.name}.{skill_name}.skill"
76
+ spec = importlib.util.spec_from_file_location(module_name, skill_file)
77
+ module = importlib.util.module_from_spec(spec)
78
+
79
+ # Add to sys.modules to handle relative imports
80
+ sys.modules[module_name] = module
81
+ spec.loader.exec_module(module)
82
+
83
+ # Find SkillBase subclasses in the module
84
+ for name, obj in inspect.getmembers(module):
85
+ if (inspect.isclass(obj) and
86
+ issubclass(obj, SkillBase) and
87
+ obj != SkillBase and
88
+ hasattr(obj, 'SKILL_NAME') and
89
+ obj.SKILL_NAME == skill_name): # Match exact skill name
90
+
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
96
+
97
+ except Exception as 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
110
+
111
+ def register_skill(self, skill_class: Type[SkillBase]) -> None:
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
+
167
+ if skill_class.SKILL_NAME in self._skills:
168
+ self.logger.warning(f"Skill '{skill_class.SKILL_NAME}' already registered")
169
+ return
170
+
171
+ self._skills[skill_class.SKILL_NAME] = skill_class
172
+ self.logger.debug(f"Registered skill '{skill_class.SKILL_NAME}'")
173
+
174
+ def get_skill_class(self, skill_name: str) -> Optional[Type[SkillBase]]:
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)
182
+
183
+ def list_skills(self) -> List[Dict[str, str]]:
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:
228
+ {
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
+ }
248
+ }
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
457
+
458
+ # Global registry instance
459
+ skill_registry = SkillRegistry()
@@ -0,0 +1,236 @@
1
+ # Spider Skill
2
+
3
+ Fast web scraping and crawling capabilities for SignalWire AI Agents. Optimized for speed and token efficiency with sub-second response times.
4
+
5
+ ## Features
6
+
7
+ - **Single page scraping** - Extract content from any web page in under 500ms
8
+ - **Multi-page crawling** - Follow links and crawl entire sections of websites
9
+ - **Structured data extraction** - Extract specific data using CSS/XPath selectors
10
+ - **Multiple output formats** - Plain text, markdown, or structured JSON
11
+ - **Smart text truncation** - Intelligently truncate long content while preserving key information
12
+ - **Response caching** - Cache pages to avoid redundant requests
13
+ - **Configurable crawling** - Control depth, page limits, and URL patterns
14
+
15
+ ## Installation
16
+
17
+ ```python
18
+ # Basic usage with defaults (single page scraping)
19
+ agent.add_skill("spider")
20
+
21
+ # Custom configuration
22
+ agent.add_skill("spider", {
23
+ "delay": 0.5,
24
+ "max_pages": 10,
25
+ "max_depth": 2
26
+ })
27
+ ```
28
+
29
+ ## Configuration Parameters
30
+
31
+ | Parameter | Type | Default | Description |
32
+ |-----------|------|---------|-------------|
33
+ | `delay` | float | 0.1 | Seconds between requests |
34
+ | `concurrent_requests` | int | 5 | Number of parallel requests |
35
+ | `timeout` | int | 5 | Request timeout in seconds |
36
+ | `max_pages` | int | 1 | Maximum pages to crawl |
37
+ | `max_depth` | int | 0 | How many links deep to crawl |
38
+ | `extract_type` | string | "fast_text" | Default extraction method |
39
+ | `max_text_length` | int | 3000 | Maximum characters per page |
40
+ | `clean_text` | bool | True | Remove extra whitespace |
41
+ | `cache_enabled` | bool | True | Enable response caching |
42
+ | `follow_robots_txt` | bool | False | Respect robots.txt |
43
+ | `user_agent` | string | "Spider/1.0" | User agent string |
44
+ | `headers` | dict | {} | Additional HTTP headers |
45
+
46
+ ## Available Tools
47
+
48
+ ### scrape_url
49
+
50
+ Extract text content from a single web page.
51
+
52
+ **Parameters:**
53
+ - `url` (required): The URL to scrape
54
+ - `extract_type` (optional): "fast_text", "markdown", or "structured"
55
+ - `selectors` (optional): CSS/XPath selectors for specific elements
56
+
57
+ **Examples:**
58
+ ```
59
+ "Please get the content from https://example.com/article"
60
+ "Scrape the main text from https://docs.example.com in markdown format"
61
+ "Extract the product price from this page using the .price selector"
62
+ ```
63
+
64
+ ### crawl_site
65
+
66
+ Crawl multiple pages starting from a URL.
67
+
68
+ **Parameters:**
69
+ - `start_url` (required): Starting URL for the crawl
70
+ - `max_depth` (optional): How many links deep to crawl
71
+ - `follow_patterns` (optional): List of regex patterns for URLs to follow
72
+ - `max_pages` (optional): Maximum pages to crawl
73
+
74
+ **Examples:**
75
+ ```
76
+ "Crawl the documentation starting from /docs with depth 2"
77
+ "Get all blog posts from the site, following only /blog/ URLs"
78
+ "Crawl up to 20 pages from their support section"
79
+ ```
80
+
81
+ ### extract_structured_data
82
+
83
+ Extract specific data from a web page using selectors.
84
+
85
+ **Parameters:**
86
+ - `url` (required): The URL to scrape
87
+ - `selectors` (required): Dictionary mapping field names to CSS/XPath selectors
88
+
89
+ **Examples:**
90
+ ```
91
+ "Extract the title, price, and description from this product page"
92
+ "Get all the email addresses and phone numbers from the contact page"
93
+ ```
94
+
95
+ ## Usage Examples
96
+
97
+ ### Basic Single Page Scraping (Default)
98
+ ```python
99
+ agent.add_skill("spider")
100
+ # AI can now: "Get the content from https://example.com"
101
+ ```
102
+
103
+ ### Documentation Crawling
104
+ ```python
105
+ agent.add_skill("spider", {
106
+ "max_pages": 50,
107
+ "max_depth": 3,
108
+ "delay": 1.0,
109
+ "extract_type": "markdown"
110
+ })
111
+ # AI can now: "Crawl the API documentation and summarize the endpoints"
112
+ ```
113
+
114
+ ### Fast News Aggregation
115
+ ```python
116
+ agent.add_skill("spider", {
117
+ "concurrent_requests": 10,
118
+ "delay": 0.05,
119
+ "max_pages": 20,
120
+ "max_text_length": 1000,
121
+ "cache_enabled": True
122
+ })
123
+ # AI can now: "Get the latest articles from the news section"
124
+ ```
125
+
126
+ ### Respectful External Scraping
127
+ ```python
128
+ agent.add_skill("spider", {
129
+ "delay": 2.0,
130
+ "concurrent_requests": 1,
131
+ "follow_robots_txt": True,
132
+ "user_agent": "MyBot/1.0 (contact@example.com)"
133
+ })
134
+ # AI can now: "Carefully scrape competitor pricing data"
135
+ ```
136
+
137
+ ### Multiple Spider Instances
138
+ ```python
139
+ # Fast spider for internal sites
140
+ agent.add_skill("spider", {
141
+ "tool_name": "fast_spider",
142
+ "delay": 0.1,
143
+ "concurrent_requests": 10
144
+ })
145
+
146
+ # Slow spider for external sites
147
+ agent.add_skill("spider", {
148
+ "tool_name": "polite_spider",
149
+ "delay": 2.0,
150
+ "concurrent_requests": 1,
151
+ "follow_robots_txt": True
152
+ })
153
+ # AI can now use: fast_spider_scrape_url() and polite_spider_scrape_url()
154
+ ```
155
+
156
+ ## Output Examples
157
+
158
+ ### Fast Text Output (Default)
159
+ ```
160
+ Content from https://example.com/article (2,456 characters):
161
+
162
+ How to Build Better Web Applications
163
+ Published on January 15, 2024
164
+
165
+ In this comprehensive guide, we'll explore modern techniques for building
166
+ scalable and maintainable web applications...
167
+
168
+ Key Topics:
169
+ - Architecture patterns
170
+ - Performance optimization
171
+ - Security best practices
172
+ - Testing strategies
173
+
174
+ [...CONTENT TRUNCATED...]
175
+
176
+ For more information, visit our documentation portal.
177
+ ```
178
+
179
+ ### Crawl Summary Output
180
+ ```
181
+ Crawled 5 pages from docs.example.com:
182
+
183
+ 1. https://docs.example.com/ (depth: 0, 3,456 chars)
184
+ Summary: Welcome to our documentation. This guide covers...
185
+
186
+ 2. https://docs.example.com/quickstart (depth: 1, 2,890 chars)
187
+ Summary: Quick Start Guide. Get up and running in 5 minutes...
188
+
189
+ 3. https://docs.example.com/api (depth: 1, 4,567 chars)
190
+ Summary: API Reference. Complete documentation of all endpoints...
191
+
192
+ Total content: 15,234 characters across 5 pages
193
+ ```
194
+
195
+ ## Performance Characteristics
196
+
197
+ - **Single page scrape**: ~300-500ms
198
+ - **10-page crawl**: ~2-3 seconds
199
+ - **Text extraction**: <50ms per page
200
+ - **Caching**: Subsequent requests ~10ms
201
+
202
+ ## Best Practices
203
+
204
+ 1. **Start with defaults** - The skill is optimized for single page scraping out of the box
205
+ 2. **Use caching** - Enabled by default, saves time on repeated requests
206
+ 3. **Set appropriate delays** - Be respectful of external sites (2+ seconds)
207
+ 4. **Limit crawl scope** - Use `max_pages` and `max_depth` to control crawl size
208
+ 5. **Use URL patterns** - Filter crawls with `follow_patterns` for focused results
209
+ 6. **Monitor performance** - Check logs for timing and error information
210
+
211
+ ## Limitations
212
+
213
+ - No JavaScript rendering (for speed)
214
+ - Basic text extraction only
215
+ - No authentication support
216
+ - No form submission
217
+ - Limited to HTML content
218
+ - No file downloads
219
+
220
+ ## Error Handling
221
+
222
+ The skill handles common errors gracefully:
223
+ - **Timeouts**: Returns partial content with timeout notice
224
+ - **HTTP errors**: Reports status code and error message
225
+ - **Invalid URLs**: Clear error message
226
+ - **Rate limiting**: Respects 429 status codes
227
+ - **Network errors**: Returns descriptive error message
228
+
229
+ ## Contributing
230
+
231
+ To enhance this skill:
232
+ 1. Keep performance as the top priority
233
+ 2. Maintain backward compatibility
234
+ 3. Add tests for new features
235
+ 4. Update this documentation
236
+ 5. Consider token efficiency in outputs