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,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
- return {
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