signalwire-agents 0.1.10__py3-none-any.whl → 0.1.12__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 +43 -4
- signalwire_agents/agent_server.py +268 -15
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +457 -0
- signalwire_agents/cli/test_swaig.py +2609 -0
- signalwire_agents/core/agent_base.py +691 -82
- signalwire_agents/core/contexts.py +289 -0
- signalwire_agents/core/data_map.py +499 -0
- signalwire_agents/core/function_result.py +57 -10
- signalwire_agents/core/logging_config.py +232 -0
- signalwire_agents/core/skill_base.py +27 -37
- signalwire_agents/core/skill_manager.py +89 -23
- signalwire_agents/core/swaig_function.py +13 -1
- signalwire_agents/core/swml_handler.py +37 -13
- signalwire_agents/core/swml_service.py +37 -28
- signalwire_agents/search/__init__.py +131 -0
- signalwire_agents/search/document_processor.py +764 -0
- signalwire_agents/search/index_builder.py +534 -0
- signalwire_agents/search/query_processor.py +371 -0
- signalwire_agents/search/search_engine.py +383 -0
- signalwire_agents/search/search_service.py +251 -0
- signalwire_agents/skills/datasphere/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +229 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
- signalwire_agents/skills/datetime/skill.py +9 -5
- signalwire_agents/skills/joke/__init__.py +1 -0
- signalwire_agents/skills/joke/skill.py +88 -0
- signalwire_agents/skills/math/skill.py +9 -6
- signalwire_agents/skills/native_vector_search/__init__.py +1 -0
- signalwire_agents/skills/native_vector_search/skill.py +352 -0
- signalwire_agents/skills/registry.py +10 -4
- signalwire_agents/skills/web_search/skill.py +57 -21
- signalwire_agents/skills/wikipedia/__init__.py +9 -0
- signalwire_agents/skills/wikipedia/skill.py +180 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents-0.1.12.dist-info/METADATA +863 -0
- signalwire_agents-0.1.12.dist-info/RECORD +67 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
- signalwire_agents-0.1.10.dist-info/METADATA +0 -319
- signalwire_agents-0.1.10.dist-info/RECORD +0 -44
- {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,232 @@
|
|
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
|
+
"""
|
11
|
+
Central logging configuration for SignalWire Agents SDK
|
12
|
+
|
13
|
+
This module provides a single point of control for all logging across the SDK
|
14
|
+
and applications built with it. All components should use get_logger() instead
|
15
|
+
of direct logging module usage or print() statements.
|
16
|
+
|
17
|
+
The StructuredLoggerWrapper provides backward compatibility with existing
|
18
|
+
structured logging calls (e.g., log.info("message", key=value)) while using
|
19
|
+
standard Python logging underneath. This allows the entire codebase to work
|
20
|
+
without changes while providing centralized logging control.
|
21
|
+
"""
|
22
|
+
|
23
|
+
import logging
|
24
|
+
import os
|
25
|
+
import sys
|
26
|
+
from typing import Optional, Any, Dict
|
27
|
+
|
28
|
+
# Global flag to ensure configuration only happens once
|
29
|
+
_logging_configured = False
|
30
|
+
|
31
|
+
|
32
|
+
class StructuredLoggerWrapper:
|
33
|
+
"""
|
34
|
+
A wrapper that provides structured logging interface while using standard Python logging
|
35
|
+
|
36
|
+
This allows existing structured logging calls to work without changes while
|
37
|
+
giving us centralized control over logging behavior.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, logger: logging.Logger):
|
41
|
+
self._logger = logger
|
42
|
+
|
43
|
+
def _format_structured_message(self, message: str, **kwargs) -> str:
|
44
|
+
"""Format a message with structured keyword arguments"""
|
45
|
+
if not kwargs:
|
46
|
+
return message
|
47
|
+
|
48
|
+
# Convert kwargs to readable string format
|
49
|
+
parts = []
|
50
|
+
for key, value in kwargs.items():
|
51
|
+
# Handle different value types appropriately
|
52
|
+
if isinstance(value, str):
|
53
|
+
parts.append(f"{key}={value}")
|
54
|
+
elif isinstance(value, (list, dict)):
|
55
|
+
parts.append(f"{key}={str(value)}")
|
56
|
+
else:
|
57
|
+
parts.append(f"{key}={value}")
|
58
|
+
|
59
|
+
if parts:
|
60
|
+
return f"{message} ({', '.join(parts)})"
|
61
|
+
else:
|
62
|
+
return message
|
63
|
+
|
64
|
+
def debug(self, message: str, **kwargs) -> None:
|
65
|
+
"""Log debug message with optional structured data"""
|
66
|
+
formatted = self._format_structured_message(message, **kwargs)
|
67
|
+
self._logger.debug(formatted)
|
68
|
+
|
69
|
+
def info(self, message: str, **kwargs) -> None:
|
70
|
+
"""Log info message with optional structured data"""
|
71
|
+
formatted = self._format_structured_message(message, **kwargs)
|
72
|
+
self._logger.info(formatted)
|
73
|
+
|
74
|
+
def warning(self, message: str, **kwargs) -> None:
|
75
|
+
"""Log warning message with optional structured data"""
|
76
|
+
formatted = self._format_structured_message(message, **kwargs)
|
77
|
+
self._logger.warning(formatted)
|
78
|
+
|
79
|
+
def error(self, message: str, **kwargs) -> None:
|
80
|
+
"""Log error message with optional structured data"""
|
81
|
+
formatted = self._format_structured_message(message, **kwargs)
|
82
|
+
self._logger.error(formatted)
|
83
|
+
|
84
|
+
def critical(self, message: str, **kwargs) -> None:
|
85
|
+
"""Log critical message with optional structured data"""
|
86
|
+
formatted = self._format_structured_message(message, **kwargs)
|
87
|
+
self._logger.critical(formatted)
|
88
|
+
|
89
|
+
# Also support the 'warn' alias
|
90
|
+
warn = warning
|
91
|
+
|
92
|
+
# Support direct access to underlying logger attributes if needed
|
93
|
+
def __getattr__(self, name: str) -> Any:
|
94
|
+
"""Delegate any unknown attributes to the underlying logger"""
|
95
|
+
return getattr(self._logger, name)
|
96
|
+
|
97
|
+
|
98
|
+
def get_execution_mode() -> str:
|
99
|
+
"""
|
100
|
+
Determine the execution mode based on environment variables
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
'cgi' if running in CGI mode
|
104
|
+
'lambda' if running in AWS Lambda
|
105
|
+
'server' for normal server mode
|
106
|
+
"""
|
107
|
+
if os.getenv('GATEWAY_INTERFACE'):
|
108
|
+
return 'cgi'
|
109
|
+
if os.getenv('AWS_LAMBDA_FUNCTION_NAME') or os.getenv('LAMBDA_TASK_ROOT'):
|
110
|
+
return 'lambda'
|
111
|
+
return 'server'
|
112
|
+
|
113
|
+
|
114
|
+
def configure_logging():
|
115
|
+
"""
|
116
|
+
Configure logging system once, globally, based on environment variables
|
117
|
+
|
118
|
+
Environment Variables:
|
119
|
+
SIGNALWIRE_LOG_MODE: off, stderr, default, auto
|
120
|
+
SIGNALWIRE_LOG_LEVEL: debug, info, warning, error, critical
|
121
|
+
"""
|
122
|
+
global _logging_configured
|
123
|
+
|
124
|
+
if _logging_configured:
|
125
|
+
return
|
126
|
+
|
127
|
+
# Get configuration from environment
|
128
|
+
log_mode = os.getenv('SIGNALWIRE_LOG_MODE', '').lower()
|
129
|
+
log_level = os.getenv('SIGNALWIRE_LOG_LEVEL', 'info').lower()
|
130
|
+
|
131
|
+
# Determine log mode if auto or not specified
|
132
|
+
if not log_mode or log_mode == 'auto':
|
133
|
+
execution_mode = get_execution_mode()
|
134
|
+
if execution_mode == 'cgi':
|
135
|
+
log_mode = 'off'
|
136
|
+
else:
|
137
|
+
log_mode = 'default'
|
138
|
+
|
139
|
+
# Configure based on mode
|
140
|
+
if log_mode == 'off':
|
141
|
+
_configure_off_mode()
|
142
|
+
elif log_mode == 'stderr':
|
143
|
+
_configure_stderr_mode(log_level)
|
144
|
+
else: # default mode
|
145
|
+
_configure_default_mode(log_level)
|
146
|
+
|
147
|
+
_logging_configured = True
|
148
|
+
|
149
|
+
|
150
|
+
def _configure_off_mode():
|
151
|
+
"""Suppress all logging output"""
|
152
|
+
# Redirect to devnull
|
153
|
+
null_file = open(os.devnull, 'w')
|
154
|
+
|
155
|
+
# Clear existing handlers and configure to devnull
|
156
|
+
logging.getLogger().handlers.clear()
|
157
|
+
logging.basicConfig(
|
158
|
+
stream=null_file,
|
159
|
+
level=logging.CRITICAL,
|
160
|
+
format=''
|
161
|
+
)
|
162
|
+
|
163
|
+
# Set all known loggers to CRITICAL to prevent any output
|
164
|
+
logger_names = [
|
165
|
+
'', 'signalwire_agents', 'skill_registry', 'swml_service',
|
166
|
+
'agent_base', 'AgentServer', 'uvicorn', 'fastapi'
|
167
|
+
]
|
168
|
+
for name in logger_names:
|
169
|
+
logging.getLogger(name).setLevel(logging.CRITICAL)
|
170
|
+
|
171
|
+
# Configure structlog if available
|
172
|
+
try:
|
173
|
+
import structlog
|
174
|
+
structlog.configure(
|
175
|
+
processors=[],
|
176
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
|
177
|
+
logger_factory=structlog.PrintLoggerFactory(file=null_file),
|
178
|
+
cache_logger_on_first_use=True,
|
179
|
+
)
|
180
|
+
except ImportError:
|
181
|
+
pass
|
182
|
+
|
183
|
+
|
184
|
+
def _configure_stderr_mode(log_level: str):
|
185
|
+
"""Configure logging to stderr"""
|
186
|
+
# Clear existing handlers
|
187
|
+
logging.getLogger().handlers.clear()
|
188
|
+
|
189
|
+
# Convert log level
|
190
|
+
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
191
|
+
|
192
|
+
# Configure to stderr
|
193
|
+
logging.basicConfig(
|
194
|
+
stream=sys.stderr,
|
195
|
+
level=numeric_level,
|
196
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
197
|
+
)
|
198
|
+
|
199
|
+
|
200
|
+
def _configure_default_mode(log_level: str):
|
201
|
+
"""Configure standard logging behavior"""
|
202
|
+
# Convert log level
|
203
|
+
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
204
|
+
|
205
|
+
# Configure standard logging
|
206
|
+
logging.basicConfig(
|
207
|
+
level=numeric_level,
|
208
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
209
|
+
)
|
210
|
+
|
211
|
+
|
212
|
+
def get_logger(name: str) -> StructuredLoggerWrapper:
|
213
|
+
"""
|
214
|
+
Get a logger instance for the specified name with structured logging support
|
215
|
+
|
216
|
+
This is the single entry point for all logging in the SDK.
|
217
|
+
All modules should use this instead of direct logging module usage.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
name: Logger name, typically __name__
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
StructuredLoggerWrapper that supports both regular and structured logging
|
224
|
+
"""
|
225
|
+
# Ensure logging is configured
|
226
|
+
configure_logging()
|
227
|
+
|
228
|
+
# Get the standard Python logger
|
229
|
+
python_logger = logging.getLogger(name)
|
230
|
+
|
231
|
+
# Wrap it with our structured logging interface
|
232
|
+
return StructuredLoggerWrapper(python_logger)
|
@@ -24,6 +24,9 @@ class SkillBase(ABC):
|
|
24
24
|
REQUIRED_PACKAGES: List[str] = [] # Python packages needed
|
25
25
|
REQUIRED_ENV_VARS: List[str] = [] # Environment variables needed
|
26
26
|
|
27
|
+
# Multiple instance support
|
28
|
+
SUPPORTS_MULTIPLE_INSTANCES: bool = False # Set to True to allow multiple instances
|
29
|
+
|
27
30
|
def __init__(self, agent: 'AgentBase', params: Optional[Dict[str, Any]] = None):
|
28
31
|
if self.SKILL_NAME is None:
|
29
32
|
raise ValueError(f"{self.__class__.__name__} must define SKILL_NAME")
|
@@ -50,42 +53,7 @@ class SkillBase(ABC):
|
|
50
53
|
"""Register SWAIG tools with the agent"""
|
51
54
|
pass
|
52
55
|
|
53
|
-
|
54
|
-
self,
|
55
|
-
name: str,
|
56
|
-
description: str,
|
57
|
-
parameters: Dict[str, Any],
|
58
|
-
handler,
|
59
|
-
**additional_kwargs
|
60
|
-
):
|
61
|
-
"""
|
62
|
-
Helper method to define a tool with swaig_fields merged in
|
63
|
-
|
64
|
-
Args:
|
65
|
-
name: Function name
|
66
|
-
description: Function description
|
67
|
-
parameters: Function parameters schema
|
68
|
-
handler: Function handler
|
69
|
-
**additional_kwargs: Additional keyword arguments for define_tool
|
70
|
-
|
71
|
-
This method automatically merges the swaig_fields from skill params
|
72
|
-
into the tool definition, allowing the skill loader to customize
|
73
|
-
SWAIG function properties.
|
74
|
-
"""
|
75
|
-
# Start with the additional kwargs passed to this method
|
76
|
-
tool_kwargs = additional_kwargs.copy()
|
77
|
-
|
78
|
-
# Merge in the swaig_fields from params (swaig_fields take precedence)
|
79
|
-
tool_kwargs.update(self.swaig_fields)
|
80
|
-
|
81
|
-
# Call the agent's define_tool with all parameters
|
82
|
-
self.agent.define_tool(
|
83
|
-
name=name,
|
84
|
-
description=description,
|
85
|
-
parameters=parameters,
|
86
|
-
handler=handler,
|
87
|
-
**tool_kwargs
|
88
|
-
)
|
56
|
+
|
89
57
|
|
90
58
|
def get_hints(self) -> List[str]:
|
91
59
|
"""Return speech recognition hints for this skill"""
|
@@ -124,4 +92,26 @@ class SkillBase(ABC):
|
|
124
92
|
if missing:
|
125
93
|
self.logger.error(f"Missing required packages: {missing}")
|
126
94
|
return False
|
127
|
-
return True
|
95
|
+
return True
|
96
|
+
|
97
|
+
def get_instance_key(self) -> str:
|
98
|
+
"""
|
99
|
+
Get the key used to track this skill instance
|
100
|
+
|
101
|
+
For skills that support multiple instances (SUPPORTS_MULTIPLE_INSTANCES = True),
|
102
|
+
this method can be overridden to provide a unique key for each instance.
|
103
|
+
|
104
|
+
Default implementation:
|
105
|
+
- If SUPPORTS_MULTIPLE_INSTANCES is False: returns SKILL_NAME
|
106
|
+
- If SUPPORTS_MULTIPLE_INSTANCES is True: returns SKILL_NAME + "_" + tool_name
|
107
|
+
(where tool_name comes from params['tool_name'] or defaults to the skill name)
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
str: Unique key for this skill instance
|
111
|
+
"""
|
112
|
+
if not self.SUPPORTS_MULTIPLE_INSTANCES:
|
113
|
+
return self.SKILL_NAME
|
114
|
+
|
115
|
+
# For multi-instance skills, create key from skill name + tool name
|
116
|
+
tool_name = self.params.get('tool_name', self.SKILL_NAME)
|
117
|
+
return f"{self.SKILL_NAME}_{tool_name}"
|
@@ -31,10 +31,6 @@ class SkillManager:
|
|
31
31
|
Returns:
|
32
32
|
tuple: (success, error_message) - error_message is empty string if successful
|
33
33
|
"""
|
34
|
-
if skill_name in self.loaded_skills:
|
35
|
-
self.logger.warning(f"Skill '{skill_name}' is already loaded")
|
36
|
-
return True, ""
|
37
|
-
|
38
34
|
# Get skill class from registry if not provided
|
39
35
|
if skill_class is None:
|
40
36
|
try:
|
@@ -50,8 +46,21 @@ class SkillManager:
|
|
50
46
|
return False, error_msg
|
51
47
|
|
52
48
|
try:
|
53
|
-
# Create skill instance with parameters
|
49
|
+
# Create skill instance with parameters to get the instance key
|
54
50
|
skill_instance = skill_class(self.agent, params)
|
51
|
+
instance_key = skill_instance.get_instance_key()
|
52
|
+
|
53
|
+
# Check if this instance is already loaded
|
54
|
+
if instance_key in self.loaded_skills:
|
55
|
+
# For single-instance skills, this is an error
|
56
|
+
if not skill_instance.SUPPORTS_MULTIPLE_INSTANCES:
|
57
|
+
error_msg = f"Skill '{skill_name}' is already loaded and does not support multiple instances"
|
58
|
+
self.logger.error(error_msg)
|
59
|
+
return False, error_msg
|
60
|
+
else:
|
61
|
+
# For multi-instance skills, just warn and return success
|
62
|
+
self.logger.warning(f"Skill instance '{instance_key}' is already loaded")
|
63
|
+
return True, ""
|
55
64
|
|
56
65
|
# Validate environment variables with specific error details
|
57
66
|
import os
|
@@ -97,9 +106,9 @@ class SkillManager:
|
|
97
106
|
for section in prompt_sections:
|
98
107
|
self.agent.prompt_add_section(**section)
|
99
108
|
|
100
|
-
# Store loaded skill
|
101
|
-
self.loaded_skills[
|
102
|
-
self.logger.info(f"Successfully loaded skill '{skill_name}'")
|
109
|
+
# Store loaded skill using instance key
|
110
|
+
self.loaded_skills[instance_key] = skill_instance
|
111
|
+
self.logger.info(f"Successfully loaded skill instance '{instance_key}' (skill: '{skill_name}')")
|
103
112
|
return True, ""
|
104
113
|
|
105
114
|
except Exception as e:
|
@@ -107,30 +116,87 @@ class SkillManager:
|
|
107
116
|
self.logger.error(error_msg)
|
108
117
|
return False, error_msg
|
109
118
|
|
110
|
-
def unload_skill(self,
|
111
|
-
"""
|
112
|
-
|
113
|
-
|
119
|
+
def unload_skill(self, skill_identifier: str) -> bool:
|
120
|
+
"""
|
121
|
+
Unload a skill and cleanup
|
122
|
+
|
123
|
+
Args:
|
124
|
+
skill_identifier: Either a skill name or an instance key
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
bool: True if successfully unloaded, False otherwise
|
128
|
+
"""
|
129
|
+
# Try to find the skill by identifier (could be skill name or instance key)
|
130
|
+
skill_instance = None
|
131
|
+
instance_key = None
|
132
|
+
|
133
|
+
# First try as direct instance key
|
134
|
+
if skill_identifier in self.loaded_skills:
|
135
|
+
instance_key = skill_identifier
|
136
|
+
skill_instance = self.loaded_skills[skill_identifier]
|
137
|
+
else:
|
138
|
+
# Try to find by skill name (for backwards compatibility)
|
139
|
+
for key, instance in self.loaded_skills.items():
|
140
|
+
if instance.SKILL_NAME == skill_identifier:
|
141
|
+
instance_key = key
|
142
|
+
skill_instance = instance
|
143
|
+
break
|
144
|
+
|
145
|
+
if skill_instance is None:
|
146
|
+
self.logger.warning(f"Skill '{skill_identifier}' is not loaded")
|
114
147
|
return False
|
115
148
|
|
116
149
|
try:
|
117
|
-
skill_instance = self.loaded_skills[skill_name]
|
118
150
|
skill_instance.cleanup()
|
119
|
-
del self.loaded_skills[
|
120
|
-
self.logger.info(f"Successfully unloaded skill '{
|
151
|
+
del self.loaded_skills[instance_key]
|
152
|
+
self.logger.info(f"Successfully unloaded skill instance '{instance_key}'")
|
121
153
|
return True
|
122
154
|
except Exception as e:
|
123
|
-
self.logger.error(f"Error unloading skill '{
|
155
|
+
self.logger.error(f"Error unloading skill '{skill_identifier}': {e}")
|
124
156
|
return False
|
125
157
|
|
126
158
|
def list_loaded_skills(self) -> List[str]:
|
127
|
-
"""List
|
159
|
+
"""List instance keys of currently loaded skills"""
|
128
160
|
return list(self.loaded_skills.keys())
|
129
161
|
|
130
|
-
def has_skill(self,
|
131
|
-
"""
|
132
|
-
|
162
|
+
def has_skill(self, skill_identifier: str) -> bool:
|
163
|
+
"""
|
164
|
+
Check if skill is currently loaded
|
165
|
+
|
166
|
+
Args:
|
167
|
+
skill_identifier: Either a skill name or an instance key
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
bool: True if loaded, False otherwise
|
171
|
+
"""
|
172
|
+
# First try as direct instance key
|
173
|
+
if skill_identifier in self.loaded_skills:
|
174
|
+
return True
|
175
|
+
|
176
|
+
# Try to find by skill name (for backwards compatibility)
|
177
|
+
for instance in self.loaded_skills.values():
|
178
|
+
if instance.SKILL_NAME == skill_identifier:
|
179
|
+
return True
|
180
|
+
|
181
|
+
return False
|
133
182
|
|
134
|
-
def get_skill(self,
|
135
|
-
"""
|
136
|
-
|
183
|
+
def get_skill(self, skill_identifier: str) -> Optional[SkillBase]:
|
184
|
+
"""
|
185
|
+
Get a loaded skill instance by identifier
|
186
|
+
|
187
|
+
Args:
|
188
|
+
skill_identifier: Either a skill name or an instance key
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
SkillBase: The skill instance if found, None otherwise
|
192
|
+
"""
|
193
|
+
# First try as direct instance key
|
194
|
+
if skill_identifier in self.loaded_skills:
|
195
|
+
return self.loaded_skills[skill_identifier]
|
196
|
+
|
197
|
+
# Try to find by skill name (for backwards compatibility)
|
198
|
+
for instance in self.loaded_skills.values():
|
199
|
+
if instance.SKILL_NAME == skill_identifier:
|
200
|
+
return instance
|
201
|
+
|
202
|
+
return None
|
@@ -27,7 +27,9 @@ class SWAIGFunction:
|
|
27
27
|
description: str,
|
28
28
|
parameters: Dict[str, Dict] = None,
|
29
29
|
secure: bool = False,
|
30
|
-
fillers: Optional[Dict[str, List[str]]] = None
|
30
|
+
fillers: Optional[Dict[str, List[str]]] = None,
|
31
|
+
webhook_url: Optional[str] = None,
|
32
|
+
**extra_swaig_fields
|
31
33
|
):
|
32
34
|
"""
|
33
35
|
Initialize a new SWAIG function
|
@@ -39,6 +41,8 @@ class SWAIGFunction:
|
|
39
41
|
parameters: Dictionary of parameters, keys are parameter names, values are param definitions
|
40
42
|
secure: Whether this function requires token validation
|
41
43
|
fillers: Optional dictionary of filler phrases by language code
|
44
|
+
webhook_url: Optional external webhook URL to use instead of local handling
|
45
|
+
**extra_swaig_fields: Additional SWAIG fields to include in function definition
|
42
46
|
"""
|
43
47
|
self.name = name
|
44
48
|
self.handler = handler
|
@@ -46,6 +50,11 @@ class SWAIGFunction:
|
|
46
50
|
self.parameters = parameters or {}
|
47
51
|
self.secure = secure
|
48
52
|
self.fillers = fillers
|
53
|
+
self.webhook_url = webhook_url
|
54
|
+
self.extra_swaig_fields = extra_swaig_fields
|
55
|
+
|
56
|
+
# Mark as external if webhook_url is provided
|
57
|
+
self.is_external = webhook_url is not None
|
49
58
|
|
50
59
|
def _ensure_parameter_structure(self) -> Dict:
|
51
60
|
"""
|
@@ -165,6 +174,9 @@ class SWAIGFunction:
|
|
165
174
|
# Add fillers if provided
|
166
175
|
if self.fillers and len(self.fillers) > 0:
|
167
176
|
function_def["fillers"] = self.fillers
|
177
|
+
|
178
|
+
# Add any extra SWAIG fields
|
179
|
+
function_def.update(self.extra_swaig_fields)
|
168
180
|
|
169
181
|
return function_def
|
170
182
|
|
@@ -93,17 +93,33 @@ class AIVerbHandler(SWMLVerbHandler):
|
|
93
93
|
"""
|
94
94
|
errors = []
|
95
95
|
|
96
|
-
# Check
|
96
|
+
# Check that prompt is present
|
97
97
|
if "prompt" not in config:
|
98
98
|
errors.append("Missing required field 'prompt'")
|
99
|
+
return False, errors
|
99
100
|
|
100
|
-
|
101
|
-
if
|
102
|
-
prompt
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
101
|
+
prompt = config["prompt"]
|
102
|
+
if not isinstance(prompt, dict):
|
103
|
+
errors.append("'prompt' must be an object")
|
104
|
+
return False, errors
|
105
|
+
|
106
|
+
# Check that prompt contains one of: text, pom, or contexts
|
107
|
+
has_text = "text" in prompt
|
108
|
+
has_pom = "pom" in prompt
|
109
|
+
has_contexts = "contexts" in prompt
|
110
|
+
|
111
|
+
options_count = sum([has_text, has_pom, has_contexts])
|
112
|
+
|
113
|
+
if options_count == 0:
|
114
|
+
errors.append("'prompt' must contain one of: 'text', 'pom', or 'contexts'")
|
115
|
+
elif options_count > 1:
|
116
|
+
errors.append("'prompt' can only contain one of: 'text', 'pom', or 'contexts'")
|
117
|
+
|
118
|
+
# Validate contexts structure if present
|
119
|
+
if has_contexts:
|
120
|
+
contexts = prompt["contexts"]
|
121
|
+
if not isinstance(contexts, dict):
|
122
|
+
errors.append("'prompt.contexts' must be an object")
|
107
123
|
|
108
124
|
# Validate SWAIG structure if present
|
109
125
|
if "SWAIG" in config:
|
@@ -116,6 +132,7 @@ class AIVerbHandler(SWMLVerbHandler):
|
|
116
132
|
def build_config(self,
|
117
133
|
prompt_text: Optional[str] = None,
|
118
134
|
prompt_pom: Optional[List[Dict[str, Any]]] = None,
|
135
|
+
contexts: Optional[Dict[str, Any]] = None,
|
119
136
|
post_prompt: Optional[str] = None,
|
120
137
|
post_prompt_url: Optional[str] = None,
|
121
138
|
swaig: Optional[Dict[str, Any]] = None,
|
@@ -124,8 +141,9 @@ class AIVerbHandler(SWMLVerbHandler):
|
|
124
141
|
Build a configuration for the AI verb
|
125
142
|
|
126
143
|
Args:
|
127
|
-
prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
|
128
|
-
prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
|
144
|
+
prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom and contexts)
|
145
|
+
prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text and contexts)
|
146
|
+
contexts: Contexts and steps configuration (mutually exclusive with prompt_text and prompt_pom)
|
129
147
|
post_prompt: Optional post-prompt text
|
130
148
|
post_prompt_url: Optional URL for post-prompt processing
|
131
149
|
swaig: Optional SWAIG configuration
|
@@ -136,13 +154,19 @@ class AIVerbHandler(SWMLVerbHandler):
|
|
136
154
|
"""
|
137
155
|
config = {}
|
138
156
|
|
139
|
-
# Add prompt (either text or
|
157
|
+
# Add prompt (either text, POM, or contexts - mutually exclusive)
|
158
|
+
prompt_options_count = sum(x is not None for x in [prompt_text, prompt_pom, contexts])
|
159
|
+
if prompt_options_count == 0:
|
160
|
+
raise ValueError("One of prompt_text, prompt_pom, or contexts must be provided")
|
161
|
+
elif prompt_options_count > 1:
|
162
|
+
raise ValueError("prompt_text, prompt_pom, and contexts are mutually exclusive")
|
163
|
+
|
140
164
|
if prompt_text is not None:
|
141
165
|
config["prompt"] = {"text": prompt_text}
|
142
166
|
elif prompt_pom is not None:
|
143
167
|
config["prompt"] = {"pom": prompt_pom}
|
144
|
-
|
145
|
-
|
168
|
+
elif contexts is not None:
|
169
|
+
config["prompt"] = {"contexts": contexts}
|
146
170
|
|
147
171
|
# Add post-prompt if provided
|
148
172
|
if post_prompt is not None:
|