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,200 @@
|
|
|
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
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import List, Dict, Any, TYPE_CHECKING, Optional
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from signalwire_agents.core.agent_base import AgentBase
|
|
16
|
+
|
|
17
|
+
class SkillBase(ABC):
|
|
18
|
+
"""Abstract base class for all agent skills"""
|
|
19
|
+
|
|
20
|
+
# Subclasses must define these
|
|
21
|
+
SKILL_NAME: str = None # Required: unique identifier
|
|
22
|
+
SKILL_DESCRIPTION: str = None # Required: human-readable description
|
|
23
|
+
SKILL_VERSION: str = "1.0.0" # Semantic version
|
|
24
|
+
REQUIRED_PACKAGES: List[str] = [] # Python packages needed
|
|
25
|
+
REQUIRED_ENV_VARS: List[str] = [] # Environment variables needed
|
|
26
|
+
|
|
27
|
+
# Multiple instance support
|
|
28
|
+
SUPPORTS_MULTIPLE_INSTANCES: bool = False # Set to True to allow multiple instances
|
|
29
|
+
|
|
30
|
+
def __init__(self, agent: 'AgentBase', params: Optional[Dict[str, Any]] = None):
|
|
31
|
+
if self.SKILL_NAME is None:
|
|
32
|
+
raise ValueError(f"{self.__class__.__name__} must define SKILL_NAME")
|
|
33
|
+
if self.SKILL_DESCRIPTION is None:
|
|
34
|
+
raise ValueError(f"{self.__class__.__name__} must define SKILL_DESCRIPTION")
|
|
35
|
+
|
|
36
|
+
self.agent = agent
|
|
37
|
+
self.params = params or {}
|
|
38
|
+
self.logger = logging.getLogger(f"skill.{self.SKILL_NAME}")
|
|
39
|
+
|
|
40
|
+
# Extract swaig_fields from params for merging into tool definitions
|
|
41
|
+
self.swaig_fields = self.params.pop('swaig_fields', {})
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def setup(self) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Setup the skill (validate env vars, initialize APIs, etc.)
|
|
47
|
+
Returns True if setup successful, False otherwise
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def register_tools(self) -> None:
|
|
53
|
+
"""Register SWAIG tools with the agent"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def define_tool(self, **kwargs) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Wrapper method that automatically includes swaig_fields when defining tools.
|
|
59
|
+
|
|
60
|
+
This method delegates to self.agent.define_tool() but automatically merges
|
|
61
|
+
any swaig_fields configured for this skill. Skills should use this method
|
|
62
|
+
instead of calling self.agent.define_tool() directly.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
**kwargs: All arguments supported by agent.define_tool()
|
|
66
|
+
(name, description, parameters, handler, etc.)
|
|
67
|
+
"""
|
|
68
|
+
# Merge swaig_fields with any explicitly passed fields
|
|
69
|
+
# Explicit fields take precedence over swaig_fields
|
|
70
|
+
merged_kwargs = dict(self.swaig_fields)
|
|
71
|
+
merged_kwargs.update(kwargs)
|
|
72
|
+
|
|
73
|
+
# Call the agent's define_tool with merged arguments
|
|
74
|
+
return self.agent.define_tool(**merged_kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_hints(self) -> List[str]:
|
|
79
|
+
"""Return speech recognition hints for this skill"""
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
def get_global_data(self) -> Dict[str, Any]:
|
|
83
|
+
"""Return data to add to agent's global context"""
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
|
87
|
+
"""Return prompt sections to add to agent"""
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
def cleanup(self) -> None:
|
|
91
|
+
"""Cleanup when skill is removed or agent shuts down"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def validate_env_vars(self) -> bool:
|
|
95
|
+
"""Check if all required environment variables are set"""
|
|
96
|
+
import os
|
|
97
|
+
missing = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)]
|
|
98
|
+
if missing:
|
|
99
|
+
self.logger.error(f"Missing required environment variables: {missing}")
|
|
100
|
+
return False
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
def validate_packages(self) -> bool:
|
|
104
|
+
"""Check if all required packages are available"""
|
|
105
|
+
import importlib
|
|
106
|
+
missing = []
|
|
107
|
+
for package in self.REQUIRED_PACKAGES:
|
|
108
|
+
try:
|
|
109
|
+
importlib.import_module(package)
|
|
110
|
+
except ImportError:
|
|
111
|
+
missing.append(package)
|
|
112
|
+
if missing:
|
|
113
|
+
self.logger.error(f"Missing required packages: {missing}")
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
def get_instance_key(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Get the key used to track this skill instance
|
|
120
|
+
|
|
121
|
+
For skills that support multiple instances (SUPPORTS_MULTIPLE_INSTANCES = True),
|
|
122
|
+
this method can be overridden to provide a unique key for each instance.
|
|
123
|
+
|
|
124
|
+
Default implementation:
|
|
125
|
+
- If SUPPORTS_MULTIPLE_INSTANCES is False: returns SKILL_NAME
|
|
126
|
+
- If SUPPORTS_MULTIPLE_INSTANCES is True: returns SKILL_NAME + "_" + tool_name
|
|
127
|
+
(where tool_name comes from params['tool_name'] or defaults to the skill name)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
str: Unique key for this skill instance
|
|
131
|
+
"""
|
|
132
|
+
if not self.SUPPORTS_MULTIPLE_INSTANCES:
|
|
133
|
+
return self.SKILL_NAME
|
|
134
|
+
|
|
135
|
+
# For multi-instance skills, create key from skill name + tool name
|
|
136
|
+
tool_name = self.params.get('tool_name', self.SKILL_NAME)
|
|
137
|
+
return f"{self.SKILL_NAME}_{tool_name}"
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
|
141
|
+
"""
|
|
142
|
+
Get the parameter schema for this skill
|
|
143
|
+
|
|
144
|
+
This method returns metadata about all parameters the skill accepts,
|
|
145
|
+
including their types, descriptions, default values, and whether they
|
|
146
|
+
are required or should be hidden (e.g., API keys).
|
|
147
|
+
|
|
148
|
+
The base implementation provides common parameters available to all skills.
|
|
149
|
+
Subclasses should override this method and merge their specific parameters
|
|
150
|
+
with the base schema.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dict[str, Dict[str, Any]]: Parameter schema where keys are parameter names
|
|
154
|
+
and values are dictionaries containing:
|
|
155
|
+
- type: Parameter type ("string", "integer", "number", "boolean", "object", "array")
|
|
156
|
+
- description: Human-readable description
|
|
157
|
+
- default: Default value if not provided (optional)
|
|
158
|
+
- required: Whether the parameter is required (default: False)
|
|
159
|
+
- hidden: Whether to hide this field in UIs (for secrets/keys)
|
|
160
|
+
- env_var: Environment variable that can provide this value (optional)
|
|
161
|
+
- enum: List of allowed values (optional)
|
|
162
|
+
- min/max: Minimum/maximum values for numeric types (optional)
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
{
|
|
166
|
+
"tool_name": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "Name for the tool when using multiple instances",
|
|
169
|
+
"default": "my_skill",
|
|
170
|
+
"required": False
|
|
171
|
+
},
|
|
172
|
+
"api_key": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "API key for the service",
|
|
175
|
+
"required": True,
|
|
176
|
+
"hidden": True,
|
|
177
|
+
"env_var": "MY_API_KEY"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
"""
|
|
181
|
+
schema = {}
|
|
182
|
+
|
|
183
|
+
# Add swaig_fields parameter (available to all skills)
|
|
184
|
+
schema["swaig_fields"] = {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"description": "Additional SWAIG function metadata to merge into tool definitions",
|
|
187
|
+
"default": {},
|
|
188
|
+
"required": False
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add tool_name for multi-instance skills
|
|
192
|
+
if cls.SUPPORTS_MULTIPLE_INSTANCES:
|
|
193
|
+
schema["tool_name"] = {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Custom name for this skill instance (for multiple instances)",
|
|
196
|
+
"default": cls.SKILL_NAME,
|
|
197
|
+
"required": False
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return schema
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
from typing import Dict, List, Type, Any, Optional
|
|
11
|
+
from signalwire_agents.core.logging_config import get_logger
|
|
12
|
+
from signalwire_agents.core.skill_base import SkillBase
|
|
13
|
+
|
|
14
|
+
class SkillManager:
|
|
15
|
+
"""Manages loading and lifecycle of agent skills"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, agent):
|
|
18
|
+
self.agent = agent
|
|
19
|
+
self.loaded_skills: Dict[str, SkillBase] = {}
|
|
20
|
+
self.logger = get_logger("skill_manager")
|
|
21
|
+
|
|
22
|
+
def load_skill(self, skill_name: str, skill_class: Type[SkillBase] = None, params: Optional[Dict[str, Any]] = None) -> tuple[bool, str]:
|
|
23
|
+
"""
|
|
24
|
+
Load and setup a skill by name
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
skill_name: Name of the skill to load
|
|
28
|
+
skill_class: Optional skill class (if not provided, will try to find it)
|
|
29
|
+
params: Optional parameters to pass to the skill
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
tuple: (success, error_message) - error_message is empty string if successful
|
|
33
|
+
"""
|
|
34
|
+
# Get skill class from registry if not provided
|
|
35
|
+
if skill_class is None:
|
|
36
|
+
try:
|
|
37
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
38
|
+
skill_class = skill_registry.get_skill_class(skill_name)
|
|
39
|
+
if skill_class is None:
|
|
40
|
+
error_msg = f"Skill '{skill_name}' not found in registry"
|
|
41
|
+
self.logger.error(error_msg)
|
|
42
|
+
return False, error_msg
|
|
43
|
+
except ImportError:
|
|
44
|
+
error_msg = f"Skills registry not available. Cannot load skill '{skill_name}'"
|
|
45
|
+
self.logger.error(error_msg)
|
|
46
|
+
return False, error_msg
|
|
47
|
+
|
|
48
|
+
# Validate that the skill has a proper parameter schema
|
|
49
|
+
if not hasattr(skill_class, 'get_parameter_schema') or not callable(getattr(skill_class, 'get_parameter_schema')):
|
|
50
|
+
error_msg = f"Skill '{skill_name}' must have get_parameter_schema() classmethod"
|
|
51
|
+
self.logger.error(error_msg)
|
|
52
|
+
return False, error_msg
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Validate the parameter schema
|
|
56
|
+
schema = skill_class.get_parameter_schema()
|
|
57
|
+
if not isinstance(schema, dict):
|
|
58
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() must return a dictionary"
|
|
59
|
+
self.logger.error(error_msg)
|
|
60
|
+
return False, error_msg
|
|
61
|
+
|
|
62
|
+
# Ensure it's not an empty schema
|
|
63
|
+
if not schema:
|
|
64
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() returned empty dictionary"
|
|
65
|
+
self.logger.error(error_msg)
|
|
66
|
+
return False, error_msg
|
|
67
|
+
|
|
68
|
+
# Check if the skill has overridden the method
|
|
69
|
+
from signalwire_agents.core.skill_base import SkillBase
|
|
70
|
+
skill_method = getattr(skill_class, 'get_parameter_schema', None)
|
|
71
|
+
base_method = getattr(SkillBase, 'get_parameter_schema', None)
|
|
72
|
+
|
|
73
|
+
if skill_method and base_method:
|
|
74
|
+
# For class methods, check the underlying function
|
|
75
|
+
skill_func = skill_method.__func__ if hasattr(skill_method, '__func__') else skill_method
|
|
76
|
+
base_func = base_method.__func__ if hasattr(base_method, '__func__') else base_method
|
|
77
|
+
|
|
78
|
+
if skill_func is base_func:
|
|
79
|
+
# Get base schema to check if skill added any parameters
|
|
80
|
+
base_schema = SkillBase.get_parameter_schema()
|
|
81
|
+
if set(schema.keys()) == set(base_schema.keys()):
|
|
82
|
+
error_msg = f"Skill '{skill_name}' must override get_parameter_schema() to define its specific parameters"
|
|
83
|
+
self.logger.error(error_msg)
|
|
84
|
+
return False, error_msg
|
|
85
|
+
|
|
86
|
+
except AttributeError as e:
|
|
87
|
+
error_msg = f"Skill '{skill_name}' must properly implement get_parameter_schema() classmethod"
|
|
88
|
+
self.logger.error(error_msg)
|
|
89
|
+
return False, error_msg
|
|
90
|
+
except Exception as e:
|
|
91
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() failed: {e}"
|
|
92
|
+
self.logger.error(error_msg)
|
|
93
|
+
return False, error_msg
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Create skill instance with parameters to get the instance key
|
|
97
|
+
skill_instance = skill_class(self.agent, params)
|
|
98
|
+
instance_key = skill_instance.get_instance_key()
|
|
99
|
+
|
|
100
|
+
# Check if this instance is already loaded
|
|
101
|
+
if instance_key in self.loaded_skills:
|
|
102
|
+
# For single-instance skills, this is an error
|
|
103
|
+
if not skill_instance.SUPPORTS_MULTIPLE_INSTANCES:
|
|
104
|
+
error_msg = f"Skill '{skill_name}' is already loaded and does not support multiple instances"
|
|
105
|
+
self.logger.error(error_msg)
|
|
106
|
+
return False, error_msg
|
|
107
|
+
else:
|
|
108
|
+
# For multi-instance skills, just warn and return success
|
|
109
|
+
self.logger.warning(f"Skill instance '{instance_key}' is already loaded")
|
|
110
|
+
return True, ""
|
|
111
|
+
|
|
112
|
+
# Validate environment variables with specific error details
|
|
113
|
+
import os
|
|
114
|
+
missing_env_vars = [var for var in skill_instance.REQUIRED_ENV_VARS if not os.getenv(var)]
|
|
115
|
+
if missing_env_vars:
|
|
116
|
+
error_msg = f"Missing required environment variables: {missing_env_vars}"
|
|
117
|
+
self.logger.error(error_msg)
|
|
118
|
+
return False, error_msg
|
|
119
|
+
|
|
120
|
+
# Validate packages with specific error details
|
|
121
|
+
import importlib
|
|
122
|
+
missing_packages = []
|
|
123
|
+
for package in skill_instance.REQUIRED_PACKAGES:
|
|
124
|
+
try:
|
|
125
|
+
importlib.import_module(package)
|
|
126
|
+
except ImportError:
|
|
127
|
+
missing_packages.append(package)
|
|
128
|
+
if missing_packages:
|
|
129
|
+
error_msg = f"Missing required packages: {missing_packages}"
|
|
130
|
+
self.logger.error(error_msg)
|
|
131
|
+
return False, error_msg
|
|
132
|
+
|
|
133
|
+
# Setup the skill
|
|
134
|
+
if not skill_instance.setup():
|
|
135
|
+
error_msg = f"Failed to setup skill '{skill_name}'"
|
|
136
|
+
self.logger.error(error_msg)
|
|
137
|
+
return False, error_msg
|
|
138
|
+
|
|
139
|
+
# Register tools with agent
|
|
140
|
+
skill_instance.register_tools()
|
|
141
|
+
|
|
142
|
+
# Add hints and global data to agent
|
|
143
|
+
hints = skill_instance.get_hints()
|
|
144
|
+
if hints:
|
|
145
|
+
self.agent.add_hints(hints)
|
|
146
|
+
|
|
147
|
+
global_data = skill_instance.get_global_data()
|
|
148
|
+
if global_data:
|
|
149
|
+
self.agent.update_global_data(global_data)
|
|
150
|
+
|
|
151
|
+
# Add prompt sections
|
|
152
|
+
prompt_sections = skill_instance.get_prompt_sections()
|
|
153
|
+
for section in prompt_sections:
|
|
154
|
+
self.agent.prompt_add_section(**section)
|
|
155
|
+
|
|
156
|
+
# Store loaded skill using instance key
|
|
157
|
+
self.loaded_skills[instance_key] = skill_instance
|
|
158
|
+
self.logger.info(f"Successfully loaded skill instance '{instance_key}' (skill: '{skill_name}')")
|
|
159
|
+
return True, ""
|
|
160
|
+
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
# Check if this is a duplicate tool registration (expected during agent cloning)
|
|
163
|
+
if "already exists" in str(e):
|
|
164
|
+
debug_msg = f"Skill '{skill_name}' already loaded, skipping duplicate registration"
|
|
165
|
+
self.logger.debug(debug_msg)
|
|
166
|
+
return True, "" # Not an error, skill is already available
|
|
167
|
+
else:
|
|
168
|
+
error_msg = f"Error loading skill '{skill_name}': {e}"
|
|
169
|
+
self.logger.error(error_msg)
|
|
170
|
+
return False, error_msg
|
|
171
|
+
except Exception as e:
|
|
172
|
+
error_msg = f"Error loading skill '{skill_name}': {e}"
|
|
173
|
+
self.logger.error(error_msg)
|
|
174
|
+
return False, error_msg
|
|
175
|
+
|
|
176
|
+
def unload_skill(self, skill_identifier: str) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Unload a skill and cleanup
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
skill_identifier: Either a skill name or an instance key
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
bool: True if successfully unloaded, False otherwise
|
|
185
|
+
"""
|
|
186
|
+
# Try to find the skill by identifier (could be skill name or instance key)
|
|
187
|
+
skill_instance = None
|
|
188
|
+
instance_key = None
|
|
189
|
+
|
|
190
|
+
# First try as direct instance key
|
|
191
|
+
if skill_identifier in self.loaded_skills:
|
|
192
|
+
instance_key = skill_identifier
|
|
193
|
+
skill_instance = self.loaded_skills[skill_identifier]
|
|
194
|
+
|
|
195
|
+
if skill_instance is None:
|
|
196
|
+
self.logger.warning(f"Skill '{skill_identifier}' is not loaded")
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
skill_instance.cleanup()
|
|
201
|
+
del self.loaded_skills[instance_key]
|
|
202
|
+
self.logger.info(f"Successfully unloaded skill instance '{instance_key}'")
|
|
203
|
+
return True
|
|
204
|
+
except Exception as e:
|
|
205
|
+
self.logger.error(f"Error unloading skill '{skill_identifier}': {e}")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def list_loaded_skills(self) -> List[str]:
|
|
209
|
+
"""List instance keys of currently loaded skills"""
|
|
210
|
+
return list(self.loaded_skills.keys())
|
|
211
|
+
|
|
212
|
+
def has_skill(self, skill_identifier: str) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Check if skill is currently loaded
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
skill_identifier: Either a skill name or an instance key
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
bool: True if loaded, False otherwise
|
|
221
|
+
"""
|
|
222
|
+
# First try as direct instance key
|
|
223
|
+
if skill_identifier in self.loaded_skills:
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def get_skill(self, skill_identifier: str) -> Optional[SkillBase]:
|
|
229
|
+
"""
|
|
230
|
+
Get a loaded skill instance by identifier
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
skill_identifier: Either a skill name or an instance key
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
SkillBase: The skill instance if found, None otherwise
|
|
237
|
+
"""
|
|
238
|
+
# First try as direct instance key
|
|
239
|
+
if skill_identifier in self.loaded_skills:
|
|
240
|
+
return self.loaded_skills[skill_identifier]
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
@@ -15,6 +15,8 @@ from typing import Dict, Any, Optional, Callable, List, Type, Union
|
|
|
15
15
|
import inspect
|
|
16
16
|
import logging
|
|
17
17
|
|
|
18
|
+
# Import here to avoid circular imports
|
|
19
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
18
20
|
|
|
19
21
|
class SWAIGFunction:
|
|
20
22
|
"""
|
|
@@ -27,7 +29,12 @@ class SWAIGFunction:
|
|
|
27
29
|
description: str,
|
|
28
30
|
parameters: Dict[str, Dict] = None,
|
|
29
31
|
secure: bool = False,
|
|
30
|
-
fillers: Optional[Dict[str, List[str]]] = None
|
|
32
|
+
fillers: Optional[Dict[str, List[str]]] = None,
|
|
33
|
+
wait_file: Optional[str] = None,
|
|
34
|
+
wait_file_loops: Optional[int] = None,
|
|
35
|
+
webhook_url: Optional[str] = None,
|
|
36
|
+
required: Optional[List[str]] = None,
|
|
37
|
+
**extra_swaig_fields
|
|
31
38
|
):
|
|
32
39
|
"""
|
|
33
40
|
Initialize a new SWAIG function
|
|
@@ -38,14 +45,27 @@ class SWAIGFunction:
|
|
|
38
45
|
description: Human-readable description of the function
|
|
39
46
|
parameters: Dictionary of parameters, keys are parameter names, values are param definitions
|
|
40
47
|
secure: Whether this function requires token validation
|
|
41
|
-
fillers: Optional dictionary of filler phrases by language code
|
|
48
|
+
fillers: Optional dictionary of filler phrases by language code (deprecated, use wait_file)
|
|
49
|
+
wait_file: Optional URL to audio file to play while function executes
|
|
50
|
+
wait_file_loops: Optional number of times to loop the wait_file
|
|
51
|
+
webhook_url: Optional external webhook URL to use instead of local handling
|
|
52
|
+
required: Optional list of required parameter names
|
|
53
|
+
**extra_swaig_fields: Additional SWAIG fields to include in function definition
|
|
42
54
|
"""
|
|
43
55
|
self.name = name
|
|
44
56
|
self.handler = handler
|
|
45
57
|
self.description = description
|
|
46
58
|
self.parameters = parameters or {}
|
|
47
59
|
self.secure = secure
|
|
48
|
-
self.fillers = fillers
|
|
60
|
+
self.fillers = fillers # Text phrases to say while processing
|
|
61
|
+
self.wait_file = wait_file # URL to audio/video file to play while waiting
|
|
62
|
+
self.wait_file_loops = wait_file_loops
|
|
63
|
+
self.webhook_url = webhook_url
|
|
64
|
+
self.required = required or []
|
|
65
|
+
self.extra_swaig_fields = extra_swaig_fields
|
|
66
|
+
|
|
67
|
+
# Mark as external if webhook_url is provided
|
|
68
|
+
self.is_external = webhook_url is not None
|
|
49
69
|
|
|
50
70
|
def _ensure_parameter_structure(self) -> Dict:
|
|
51
71
|
"""
|
|
@@ -62,11 +82,17 @@ class SWAIGFunction:
|
|
|
62
82
|
return self.parameters
|
|
63
83
|
|
|
64
84
|
# Otherwise, wrap the parameters in the expected structure
|
|
65
|
-
|
|
85
|
+
result = {
|
|
66
86
|
"type": "object",
|
|
67
87
|
"properties": self.parameters
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
# Add required fields if specified
|
|
91
|
+
if self.required:
|
|
92
|
+
result["required"] = self.required
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
70
96
|
def __call__(self, *args, **kwargs):
|
|
71
97
|
"""
|
|
72
98
|
Call the underlying handler function
|
|
@@ -92,9 +118,6 @@ class SWAIGFunction:
|
|
|
92
118
|
# Call the handler with both args and raw_data
|
|
93
119
|
result = self.handler(args, raw_data)
|
|
94
120
|
|
|
95
|
-
# Import here to avoid circular imports
|
|
96
|
-
from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
97
|
-
|
|
98
121
|
# Handle different result types - everything must end up as a SwaigFunctionResult
|
|
99
122
|
if isinstance(result, SwaigFunctionResult):
|
|
100
123
|
# Already a SwaigFunctionResult - just convert to dict
|
|
@@ -165,8 +188,9 @@ class SWAIGFunction:
|
|
|
165
188
|
# Add fillers if provided
|
|
166
189
|
if self.fillers and len(self.fillers) > 0:
|
|
167
190
|
function_def["fillers"] = self.fillers
|
|
191
|
+
|
|
192
|
+
# Add any extra SWAIG fields
|
|
193
|
+
function_def.update(self.extra_swaig_fields)
|
|
168
194
|
|
|
169
195
|
return function_def
|
|
170
196
|
|
|
171
|
-
# Add an alias for backward compatibility
|
|
172
|
-
SwaigFunction = SWAIGFunction
|