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.
- signalwire_agents/__init__.py +130 -4
- signalwire_agents/agent_server.py +438 -32
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +18 -0
- signalwire_agents/cli/build_search.py +1367 -0
- 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/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 +1225 -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 +809 -0
- 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 +959 -2166
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +707 -0
- signalwire_agents/core/data_map.py +487 -0
- signalwire_agents/core/function_result.py +1150 -1
- signalwire_agents/core/logging_config.py +376 -0
- 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 +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -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 +1134 -0
- signalwire_agents/core/security/session_manager.py +174 -86
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +200 -0
- signalwire_agents/core/skill_manager.py +244 -0
- signalwire_agents/core/swaig_function.py +33 -9
- signalwire_agents/core/swml_builder.py +212 -12
- signalwire_agents/core/swml_handler.py +43 -13
- signalwire_agents/core/swml_renderer.py +123 -297
- signalwire_agents/core/swml_service.py +277 -260
- signalwire_agents/prefabs/concierge.py +6 -2
- signalwire_agents/prefabs/info_gatherer.py +149 -33
- signalwire_agents/prefabs/receptionist.py +14 -22
- signalwire_agents/prefabs/survey.py +6 -2
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +137 -0
- signalwire_agents/search/document_processor.py +1223 -0
- signalwire_agents/search/index_builder.py +804 -0
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +752 -0
- signalwire_agents/search/query_processor.py +502 -0
- signalwire_agents/search/search_engine.py +1264 -0
- signalwire_agents/search/search_service.py +574 -0
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +23 -0
- 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/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +310 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +10 -0
- signalwire_agents/skills/datetime/skill.py +126 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +10 -0
- signalwire_agents/skills/joke/skill.py +109 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +10 -0
- signalwire_agents/skills/math/skill.py +105 -0
- 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 +10 -0
- signalwire_agents/skills/native_vector_search/skill.py +820 -0
- 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 +459 -0
- 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 +10 -0
- signalwire_agents/skills/web_search/skill.py +739 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/wikipedia_search/skill.py +210 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
- signalwire_agents-1.0.7.dist-info/METADATA +992 -0
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
- signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents-0.1.6.data/data/schema.json +0 -5611
- signalwire_agents-0.1.6.dist-info/METADATA +0 -199
- signalwire_agents-0.1.6.dist-info/RECORD +0 -34
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {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
|