signalwire-agents 0.1.7__py3-none-any.whl → 0.1.8__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.
@@ -14,7 +14,7 @@ SignalWire AI Agents SDK
14
14
  A package for building AI agents using SignalWire's AI and SWML capabilities.
15
15
  """
16
16
 
17
- __version__ = "0.1.7"
17
+ __version__ = "0.1.8"
18
18
 
19
19
  # Import core classes for easier access
20
20
  from signalwire_agents.core.agent_base import AgentBase
@@ -79,6 +79,169 @@ from signalwire_agents.core.swml_handler import AIVerbHandler
79
79
  # Create a logger
80
80
  logger = structlog.get_logger("agent_base")
81
81
 
82
+ class EphemeralAgentConfig:
83
+ """
84
+ An ephemeral configurator object that mimics AgentBase's configuration interface.
85
+
86
+ This allows dynamic configuration callbacks to use the same familiar methods
87
+ they would use during agent initialization, but for per-request configuration.
88
+ """
89
+
90
+ def __init__(self):
91
+ # Initialize all configuration containers
92
+ self._hints = []
93
+ self._languages = []
94
+ self._pronounce = []
95
+ self._params = {}
96
+ self._global_data = {}
97
+ self._prompt_sections = []
98
+ self._raw_prompt = None
99
+ self._post_prompt = None
100
+ self._function_includes = []
101
+ self._native_functions = []
102
+
103
+ # Mirror all the AgentBase configuration methods
104
+
105
+ def add_hint(self, hint: str) -> 'EphemeralAgentConfig':
106
+ """Add a simple string hint"""
107
+ if isinstance(hint, str) and hint:
108
+ self._hints.append(hint)
109
+ return self
110
+
111
+ def add_hints(self, hints: List[str]) -> 'EphemeralAgentConfig':
112
+ """Add multiple string hints"""
113
+ if hints and isinstance(hints, list):
114
+ for hint in hints:
115
+ if isinstance(hint, str) and hint:
116
+ self._hints.append(hint)
117
+ return self
118
+
119
+ def add_language(self, name: str, code: str, voice: str, **kwargs) -> 'EphemeralAgentConfig':
120
+ """Add a language configuration"""
121
+ language = {
122
+ "name": name,
123
+ "code": code,
124
+ "voice": voice
125
+ }
126
+
127
+ # Handle additional parameters
128
+ for key, value in kwargs.items():
129
+ if key in ["engine", "model", "speech_fillers", "function_fillers", "fillers"]:
130
+ language[key] = value
131
+
132
+ self._languages.append(language)
133
+ return self
134
+
135
+ def add_pronunciation(self, replace: str, with_text: str, ignore_case: bool = False) -> 'EphemeralAgentConfig':
136
+ """Add a pronunciation rule"""
137
+ if replace and with_text:
138
+ rule = {"replace": replace, "with": with_text}
139
+ if ignore_case:
140
+ rule["ignore_case"] = True
141
+ self._pronounce.append(rule)
142
+ return self
143
+
144
+ def set_param(self, key: str, value: Any) -> 'EphemeralAgentConfig':
145
+ """Set a single AI parameter"""
146
+ if key:
147
+ self._params[key] = value
148
+ return self
149
+
150
+ def set_params(self, params: Dict[str, Any]) -> 'EphemeralAgentConfig':
151
+ """Set multiple AI parameters"""
152
+ if params and isinstance(params, dict):
153
+ self._params.update(params)
154
+ return self
155
+
156
+ def set_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
157
+ """Set global data"""
158
+ if data and isinstance(data, dict):
159
+ self._global_data = data
160
+ return self
161
+
162
+ def update_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
163
+ """Update global data"""
164
+ if data and isinstance(data, dict):
165
+ self._global_data.update(data)
166
+ return self
167
+
168
+ def set_prompt_text(self, text: str) -> 'EphemeralAgentConfig':
169
+ """Set raw prompt text"""
170
+ self._raw_prompt = text
171
+ return self
172
+
173
+ def set_post_prompt(self, text: str) -> 'EphemeralAgentConfig':
174
+ """Set post-prompt text"""
175
+ self._post_prompt = text
176
+ return self
177
+
178
+ def prompt_add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None, **kwargs) -> 'EphemeralAgentConfig':
179
+ """Add a prompt section"""
180
+ section = {
181
+ "title": title,
182
+ "body": body
183
+ }
184
+ if bullets:
185
+ section["bullets"] = bullets
186
+
187
+ # Handle additional parameters
188
+ for key, value in kwargs.items():
189
+ if key in ["numbered", "numbered_bullets", "subsections"]:
190
+ section[key] = value
191
+
192
+ self._prompt_sections.append(section)
193
+ return self
194
+
195
+ def set_native_functions(self, function_names: List[str]) -> 'EphemeralAgentConfig':
196
+ """Set native functions"""
197
+ if function_names and isinstance(function_names, list):
198
+ self._native_functions = [name for name in function_names if isinstance(name, str)]
199
+ return self
200
+
201
+ def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'EphemeralAgentConfig':
202
+ """Add a function include"""
203
+ if url and functions and isinstance(functions, list):
204
+ include = {"url": url, "functions": functions}
205
+ if meta_data and isinstance(meta_data, dict):
206
+ include["meta_data"] = meta_data
207
+ self._function_includes.append(include)
208
+ return self
209
+
210
+ def extract_config(self) -> Dict[str, Any]:
211
+ """
212
+ Extract the configuration as a dictionary for applying to the real agent.
213
+
214
+ Returns:
215
+ Dictionary containing all the configuration changes
216
+ """
217
+ config = {}
218
+
219
+ if self._hints:
220
+ config["hints"] = self._hints
221
+ if self._languages:
222
+ config["languages"] = self._languages
223
+ if self._pronounce:
224
+ config["pronounce"] = self._pronounce
225
+ if self._params:
226
+ config["params"] = self._params
227
+ if self._global_data:
228
+ config["global_data"] = self._global_data
229
+ if self._function_includes:
230
+ config["function_includes"] = self._function_includes
231
+ if self._native_functions:
232
+ config["native_functions"] = self._native_functions
233
+
234
+ # Handle prompt sections - these should be applied to the agent's POM, not as raw config
235
+ # The calling code should use these to build the prompt properly
236
+ if self._prompt_sections:
237
+ config["_ephemeral_prompt_sections"] = self._prompt_sections
238
+ if self._raw_prompt:
239
+ config["_ephemeral_raw_prompt"] = self._raw_prompt
240
+ if self._post_prompt:
241
+ config["_ephemeral_post_prompt"] = self._post_prompt
242
+
243
+ return config
244
+
82
245
  class AgentBase(SWMLService):
83
246
  """
84
247
  Base class for all SignalWire AI Agents.
@@ -238,6 +401,9 @@ class AgentBase(SWMLService):
238
401
  self._params = {}
239
402
  self._global_data = {}
240
403
  self._function_includes = []
404
+
405
+ # Dynamic configuration callback
406
+ self._dynamic_config_callback = None
241
407
 
242
408
  def _process_prompt_sections(self):
243
409
  """
@@ -1211,36 +1377,23 @@ class AgentBase(SWMLService):
1211
1377
  # Add the AI verb to the document
1212
1378
  self.add_verb("ai", ai_config)
1213
1379
 
1214
- # Apply any modifications from the callback
1380
+ # Apply any modifications from the callback to agent state
1215
1381
  if modifications and isinstance(modifications, dict):
1216
- # We need a way to apply modifications to the document
1217
- # Get the current document
1218
- document = self.get_document()
1219
-
1220
- # Simple recursive update function
1221
- def update_dict(target, source):
1222
- for key, value in source.items():
1223
- if isinstance(value, dict) and key in target and isinstance(target[key], dict):
1224
- update_dict(target[key], value)
1225
- else:
1226
- target[key] = value
1227
-
1228
- # Apply modifications to the document
1229
- update_dict(document, modifications)
1230
-
1231
- # Since we can't directly set the document in SWMLService,
1232
- # we'll need to reset and rebuild if there are modifications
1382
+ # Handle global_data modifications by updating the AI config directly
1383
+ if "global_data" in modifications:
1384
+ if modifications["global_data"]:
1385
+ # Merge the modification global_data with existing global_data
1386
+ ai_config["global_data"] = {**ai_config.get("global_data", {}), **modifications["global_data"]}
1387
+
1388
+ # Handle other modifications by updating the AI config
1389
+ for key, value in modifications.items():
1390
+ if key != "global_data": # global_data handled above
1391
+ ai_config[key] = value
1392
+
1393
+ # Clear and rebuild the document with the modified AI config
1233
1394
  self.reset_document()
1234
-
1235
- # Add the modified document's sections
1236
- for section_name, section_content in document["sections"].items():
1237
- if section_name != "main": # Main section is created by default
1238
- self.add_section(section_name)
1239
-
1240
- # Add each verb to the section
1241
- for verb_obj in section_content:
1242
- for verb_name, verb_config in verb_obj.items():
1243
- self.add_verb_to_section(section_name, verb_name, verb_config)
1395
+ self.add_answer_verb()
1396
+ self.add_verb("ai", ai_config)
1244
1397
 
1245
1398
  # Return the rendered document as a string
1246
1399
  return self.render_document()
@@ -2108,16 +2261,15 @@ class AgentBase(SWMLService):
2108
2261
 
2109
2262
  # Allow subclasses to inspect/modify the request
2110
2263
  modifications = None
2111
- if body:
2112
- try:
2113
- modifications = self.on_swml_request(body, callback_path)
2114
- if modifications:
2115
- req_log.debug("request_modifications_applied")
2116
- except Exception as e:
2117
- req_log.error("error_in_request_modifier", error=str(e))
2264
+ try:
2265
+ modifications = self.on_swml_request(body, callback_path, request)
2266
+ if modifications:
2267
+ req_log.debug("request_modifications_applied")
2268
+ except Exception as e:
2269
+ req_log.error("error_in_request_modifier", error=str(e))
2118
2270
 
2119
2271
  # Render SWML
2120
- swml = self._render_swml(call_id)
2272
+ swml = self._render_swml(call_id, modifications)
2121
2273
  req_log.debug("swml_rendered", swml_size=len(swml))
2122
2274
 
2123
2275
  # Return as JSON
@@ -2176,13 +2328,15 @@ class AgentBase(SWMLService):
2176
2328
 
2177
2329
  # Allow subclasses to inspect/modify the request
2178
2330
  modifications = None
2179
- if body:
2180
- modifications = self.on_swml_request(body)
2331
+ try:
2332
+ modifications = self.on_swml_request(body, None, request)
2181
2333
  if modifications:
2182
2334
  req_log.debug("request_modifications_applied")
2335
+ except Exception as e:
2336
+ req_log.error("error_in_request_modifier", error=str(e))
2183
2337
 
2184
2338
  # Render SWML
2185
- swml = self._render_swml(call_id)
2339
+ swml = self._render_swml(call_id, modifications)
2186
2340
  req_log.debug("swml_rendered", swml_size=len(swml))
2187
2341
 
2188
2342
  # Return as JSON
@@ -2492,24 +2646,66 @@ class AgentBase(SWMLService):
2492
2646
  """
2493
2647
  # First try to call on_swml_request if it exists (backward compatibility)
2494
2648
  if hasattr(self, 'on_swml_request') and callable(getattr(self, 'on_swml_request')):
2495
- return self.on_swml_request(request_data, callback_path)
2649
+ return self.on_swml_request(request_data, callback_path, None)
2496
2650
 
2497
2651
  # If no on_swml_request or it returned None, we'll proceed with default rendering
2498
2652
  # We're not returning any modifications here because _render_swml will be called
2499
2653
  # to generate the complete SWML document
2500
2654
  return None
2501
2655
 
2502
- def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
2656
+ def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None, request: Optional[Request] = None) -> Optional[dict]:
2503
2657
  """
2504
2658
  Customization point for subclasses to modify SWML based on request data
2505
2659
 
2506
2660
  Args:
2507
2661
  request_data: Optional dictionary containing the parsed POST body
2508
2662
  callback_path: Optional callback path
2663
+ request: Optional FastAPI Request object for accessing query params, headers, etc.
2509
2664
 
2510
2665
  Returns:
2511
2666
  Optional dict with modifications to apply to the SWML document
2512
2667
  """
2668
+ # Handle dynamic configuration callback if set
2669
+ if self._dynamic_config_callback and request:
2670
+ try:
2671
+ # Extract request data
2672
+ query_params = dict(request.query_params)
2673
+ body_params = request_data or {}
2674
+ headers = dict(request.headers)
2675
+
2676
+ # Create ephemeral configurator
2677
+ agent_config = EphemeralAgentConfig()
2678
+
2679
+ # Call the user's configuration callback
2680
+ self._dynamic_config_callback(query_params, body_params, headers, agent_config)
2681
+
2682
+ # Extract the configuration
2683
+ config = agent_config.extract_config()
2684
+ if config:
2685
+ # Handle ephemeral prompt sections by applying them to this agent instance
2686
+ if "_ephemeral_prompt_sections" in config:
2687
+ for section in config["_ephemeral_prompt_sections"]:
2688
+ self.prompt_add_section(
2689
+ section["title"],
2690
+ section.get("body", ""),
2691
+ section.get("bullets"),
2692
+ **{k: v for k, v in section.items() if k not in ["title", "body", "bullets"]}
2693
+ )
2694
+ del config["_ephemeral_prompt_sections"]
2695
+
2696
+ if "_ephemeral_raw_prompt" in config:
2697
+ self._raw_prompt = config["_ephemeral_raw_prompt"]
2698
+ del config["_ephemeral_raw_prompt"]
2699
+
2700
+ if "_ephemeral_post_prompt" in config:
2701
+ self._post_prompt = config["_ephemeral_post_prompt"]
2702
+ del config["_ephemeral_post_prompt"]
2703
+
2704
+ return config
2705
+
2706
+ except Exception as e:
2707
+ self.log.error("dynamic_config_error", error=str(e))
2708
+
2513
2709
  # Default implementation does nothing
2514
2710
  return None
2515
2711
 
@@ -2542,6 +2738,34 @@ class AgentBase(SWMLService):
2542
2738
  self._routing_callbacks = {}
2543
2739
  self._routing_callbacks[normalized_path] = callback_fn
2544
2740
 
2741
+ def set_dynamic_config_callback(self, callback: Callable[[dict, dict, dict, EphemeralAgentConfig], None]) -> 'AgentBase':
2742
+ """
2743
+ Set a callback function for dynamic agent configuration
2744
+
2745
+ This callback receives an EphemeralAgentConfig object that provides the same
2746
+ configuration methods as AgentBase, allowing you to dynamically configure
2747
+ the agent's voice, prompt, parameters, etc. based on request data.
2748
+
2749
+ Args:
2750
+ callback: Function that takes (query_params, body_params, headers, agent_config)
2751
+ and configures the agent_config object using familiar methods like:
2752
+ - agent_config.add_language(...)
2753
+ - agent_config.prompt_add_section(...)
2754
+ - agent_config.set_params(...)
2755
+ - agent_config.set_global_data(...)
2756
+
2757
+ Example:
2758
+ def my_config(query_params, body_params, headers, agent):
2759
+ if query_params.get('tier') == 'premium':
2760
+ agent.add_language("English", "en-US", "premium_voice")
2761
+ agent.set_params({"end_of_speech_timeout": 500})
2762
+ agent.set_global_data({"tier": query_params.get('tier', 'standard')})
2763
+
2764
+ my_agent.set_dynamic_config_callback(my_config)
2765
+ """
2766
+ self._dynamic_config_callback = callback
2767
+ return self
2768
+
2545
2769
  def manual_set_proxy_url(self, proxy_url: str) -> 'AgentBase':
2546
2770
  """
2547
2771
  Manually set the proxy URL base for webhook callbacks