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.
Files changed (46) hide show
  1. signalwire_agents/__init__.py +43 -4
  2. signalwire_agents/agent_server.py +268 -15
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/build_search.py +457 -0
  5. signalwire_agents/cli/test_swaig.py +2609 -0
  6. signalwire_agents/core/agent_base.py +691 -82
  7. signalwire_agents/core/contexts.py +289 -0
  8. signalwire_agents/core/data_map.py +499 -0
  9. signalwire_agents/core/function_result.py +57 -10
  10. signalwire_agents/core/logging_config.py +232 -0
  11. signalwire_agents/core/skill_base.py +27 -37
  12. signalwire_agents/core/skill_manager.py +89 -23
  13. signalwire_agents/core/swaig_function.py +13 -1
  14. signalwire_agents/core/swml_handler.py +37 -13
  15. signalwire_agents/core/swml_service.py +37 -28
  16. signalwire_agents/search/__init__.py +131 -0
  17. signalwire_agents/search/document_processor.py +764 -0
  18. signalwire_agents/search/index_builder.py +534 -0
  19. signalwire_agents/search/query_processor.py +371 -0
  20. signalwire_agents/search/search_engine.py +383 -0
  21. signalwire_agents/search/search_service.py +251 -0
  22. signalwire_agents/skills/datasphere/__init__.py +12 -0
  23. signalwire_agents/skills/datasphere/skill.py +229 -0
  24. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  25. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  26. signalwire_agents/skills/datetime/skill.py +9 -5
  27. signalwire_agents/skills/joke/__init__.py +1 -0
  28. signalwire_agents/skills/joke/skill.py +88 -0
  29. signalwire_agents/skills/math/skill.py +9 -6
  30. signalwire_agents/skills/native_vector_search/__init__.py +1 -0
  31. signalwire_agents/skills/native_vector_search/skill.py +352 -0
  32. signalwire_agents/skills/registry.py +10 -4
  33. signalwire_agents/skills/web_search/skill.py +57 -21
  34. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  35. signalwire_agents/skills/wikipedia/skill.py +180 -0
  36. signalwire_agents/utils/__init__.py +14 -0
  37. signalwire_agents/utils/schema_utils.py +111 -44
  38. signalwire_agents-0.1.12.dist-info/METADATA +863 -0
  39. signalwire_agents-0.1.12.dist-info/RECORD +67 -0
  40. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
  41. signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
  42. signalwire_agents-0.1.10.dist-info/METADATA +0 -319
  43. signalwire_agents-0.1.10.dist-info/RECORD +0 -44
  44. {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
  45. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
  46. {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
- def define_tool_with_swaig_fields(
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[skill_name] = skill_instance
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, skill_name: str) -> bool:
111
- """Unload a skill and cleanup"""
112
- if skill_name not in self.loaded_skills:
113
- self.logger.warning(f"Skill '{skill_name}' is not loaded")
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[skill_name]
120
- self.logger.info(f"Successfully unloaded skill '{skill_name}'")
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 '{skill_name}': {e}")
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 names of currently loaded skills"""
159
+ """List instance keys of currently loaded skills"""
128
160
  return list(self.loaded_skills.keys())
129
161
 
130
- def has_skill(self, skill_name: str) -> bool:
131
- """Check if skill is currently loaded"""
132
- return skill_name in self.loaded_skills
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, skill_name: str) -> Optional[SkillBase]:
135
- """Get a loaded skill instance by name"""
136
- return self.loaded_skills.get(skill_name)
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 required fields
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
- # Validate prompt structure if present
101
- if "prompt" in config:
102
- prompt = config["prompt"]
103
- if not isinstance(prompt, dict):
104
- errors.append("'prompt' must be an object")
105
- elif "text" not in prompt and "pom" not in prompt:
106
- errors.append("'prompt' must contain either 'text' or 'pom'")
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 POM)
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
- else:
145
- raise ValueError("Either prompt_text or prompt_pom must be provided")
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: