signalwire-agents 0.1.7__py3-none-any.whl → 0.1.9__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.9"
18
18
 
19
19
  # Import core classes for easier access
20
20
  from signalwire_agents.core.agent_base import AgentBase
@@ -23,4 +23,7 @@ from signalwire_agents.core.swml_service import SWMLService
23
23
  from signalwire_agents.core.swml_builder import SWMLBuilder
24
24
  from signalwire_agents.core.state import StateManager, FileStateManager
25
25
 
26
+ # Import skills to trigger discovery
27
+ import signalwire_agents.skills
28
+
26
29
  __all__ = ["AgentBase", "AgentServer", "SWMLService", "SWMLBuilder", "StateManager", "FileStateManager"]
@@ -75,10 +75,174 @@ from signalwire_agents.core.security.session_manager import SessionManager
75
75
  from signalwire_agents.core.state import StateManager, FileStateManager
76
76
  from signalwire_agents.core.swml_service import SWMLService
77
77
  from signalwire_agents.core.swml_handler import AIVerbHandler
78
+ from signalwire_agents.core.skill_manager import SkillManager
78
79
 
79
80
  # Create a logger
80
81
  logger = structlog.get_logger("agent_base")
81
82
 
83
+ class EphemeralAgentConfig:
84
+ """
85
+ An ephemeral configurator object that mimics AgentBase's configuration interface.
86
+
87
+ This allows dynamic configuration callbacks to use the same familiar methods
88
+ they would use during agent initialization, but for per-request configuration.
89
+ """
90
+
91
+ def __init__(self):
92
+ # Initialize all configuration containers
93
+ self._hints = []
94
+ self._languages = []
95
+ self._pronounce = []
96
+ self._params = {}
97
+ self._global_data = {}
98
+ self._prompt_sections = []
99
+ self._raw_prompt = None
100
+ self._post_prompt = None
101
+ self._function_includes = []
102
+ self._native_functions = []
103
+
104
+ # Mirror all the AgentBase configuration methods
105
+
106
+ def add_hint(self, hint: str) -> 'EphemeralAgentConfig':
107
+ """Add a simple string hint"""
108
+ if isinstance(hint, str) and hint:
109
+ self._hints.append(hint)
110
+ return self
111
+
112
+ def add_hints(self, hints: List[str]) -> 'EphemeralAgentConfig':
113
+ """Add multiple string hints"""
114
+ if hints and isinstance(hints, list):
115
+ for hint in hints:
116
+ if isinstance(hint, str) and hint:
117
+ self._hints.append(hint)
118
+ return self
119
+
120
+ def add_language(self, name: str, code: str, voice: str, **kwargs) -> 'EphemeralAgentConfig':
121
+ """Add a language configuration"""
122
+ language = {
123
+ "name": name,
124
+ "code": code,
125
+ "voice": voice
126
+ }
127
+
128
+ # Handle additional parameters
129
+ for key, value in kwargs.items():
130
+ if key in ["engine", "model", "speech_fillers", "function_fillers", "fillers"]:
131
+ language[key] = value
132
+
133
+ self._languages.append(language)
134
+ return self
135
+
136
+ def add_pronunciation(self, replace: str, with_text: str, ignore_case: bool = False) -> 'EphemeralAgentConfig':
137
+ """Add a pronunciation rule"""
138
+ if replace and with_text:
139
+ rule = {"replace": replace, "with": with_text}
140
+ if ignore_case:
141
+ rule["ignore_case"] = True
142
+ self._pronounce.append(rule)
143
+ return self
144
+
145
+ def set_param(self, key: str, value: Any) -> 'EphemeralAgentConfig':
146
+ """Set a single AI parameter"""
147
+ if key:
148
+ self._params[key] = value
149
+ return self
150
+
151
+ def set_params(self, params: Dict[str, Any]) -> 'EphemeralAgentConfig':
152
+ """Set multiple AI parameters"""
153
+ if params and isinstance(params, dict):
154
+ self._params.update(params)
155
+ return self
156
+
157
+ def set_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
158
+ """Set global data"""
159
+ if data and isinstance(data, dict):
160
+ self._global_data = data
161
+ return self
162
+
163
+ def update_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
164
+ """Update global data"""
165
+ if data and isinstance(data, dict):
166
+ self._global_data.update(data)
167
+ return self
168
+
169
+ def set_prompt_text(self, text: str) -> 'EphemeralAgentConfig':
170
+ """Set raw prompt text"""
171
+ self._raw_prompt = text
172
+ return self
173
+
174
+ def set_post_prompt(self, text: str) -> 'EphemeralAgentConfig':
175
+ """Set post-prompt text"""
176
+ self._post_prompt = text
177
+ return self
178
+
179
+ def prompt_add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None, **kwargs) -> 'EphemeralAgentConfig':
180
+ """Add a prompt section"""
181
+ section = {
182
+ "title": title,
183
+ "body": body
184
+ }
185
+ if bullets:
186
+ section["bullets"] = bullets
187
+
188
+ # Handle additional parameters
189
+ for key, value in kwargs.items():
190
+ if key in ["numbered", "numbered_bullets", "subsections"]:
191
+ section[key] = value
192
+
193
+ self._prompt_sections.append(section)
194
+ return self
195
+
196
+ def set_native_functions(self, function_names: List[str]) -> 'EphemeralAgentConfig':
197
+ """Set native functions"""
198
+ if function_names and isinstance(function_names, list):
199
+ self._native_functions = [name for name in function_names if isinstance(name, str)]
200
+ return self
201
+
202
+ def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'EphemeralAgentConfig':
203
+ """Add a function include"""
204
+ if url and functions and isinstance(functions, list):
205
+ include = {"url": url, "functions": functions}
206
+ if meta_data and isinstance(meta_data, dict):
207
+ include["meta_data"] = meta_data
208
+ self._function_includes.append(include)
209
+ return self
210
+
211
+ def extract_config(self) -> Dict[str, Any]:
212
+ """
213
+ Extract the configuration as a dictionary for applying to the real agent.
214
+
215
+ Returns:
216
+ Dictionary containing all the configuration changes
217
+ """
218
+ config = {}
219
+
220
+ if self._hints:
221
+ config["hints"] = self._hints
222
+ if self._languages:
223
+ config["languages"] = self._languages
224
+ if self._pronounce:
225
+ config["pronounce"] = self._pronounce
226
+ if self._params:
227
+ config["params"] = self._params
228
+ if self._global_data:
229
+ config["global_data"] = self._global_data
230
+ if self._function_includes:
231
+ config["function_includes"] = self._function_includes
232
+ if self._native_functions:
233
+ config["native_functions"] = self._native_functions
234
+
235
+ # Handle prompt sections - these should be applied to the agent's POM, not as raw config
236
+ # The calling code should use these to build the prompt properly
237
+ if self._prompt_sections:
238
+ config["_ephemeral_prompt_sections"] = self._prompt_sections
239
+ if self._raw_prompt:
240
+ config["_ephemeral_raw_prompt"] = self._raw_prompt
241
+ if self._post_prompt:
242
+ config["_ephemeral_post_prompt"] = self._post_prompt
243
+
244
+ return config
245
+
82
246
  class AgentBase(SWMLService):
83
247
  """
84
248
  Base class for all SignalWire AI Agents.
@@ -238,6 +402,12 @@ class AgentBase(SWMLService):
238
402
  self._params = {}
239
403
  self._global_data = {}
240
404
  self._function_includes = []
405
+
406
+ # Dynamic configuration callback
407
+ self._dynamic_config_callback = None
408
+
409
+ # Initialize skill manager
410
+ self.skill_manager = SkillManager(self)
241
411
 
242
412
  def _process_prompt_sections(self):
243
413
  """
@@ -1211,36 +1381,23 @@ class AgentBase(SWMLService):
1211
1381
  # Add the AI verb to the document
1212
1382
  self.add_verb("ai", ai_config)
1213
1383
 
1214
- # Apply any modifications from the callback
1384
+ # Apply any modifications from the callback to agent state
1215
1385
  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
1386
+ # Handle global_data modifications by updating the AI config directly
1387
+ if "global_data" in modifications:
1388
+ if modifications["global_data"]:
1389
+ # Merge the modification global_data with existing global_data
1390
+ ai_config["global_data"] = {**ai_config.get("global_data", {}), **modifications["global_data"]}
1391
+
1392
+ # Handle other modifications by updating the AI config
1393
+ for key, value in modifications.items():
1394
+ if key != "global_data": # global_data handled above
1395
+ ai_config[key] = value
1396
+
1397
+ # Clear and rebuild the document with the modified AI config
1233
1398
  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)
1399
+ self.add_answer_verb()
1400
+ self.add_verb("ai", ai_config)
1244
1401
 
1245
1402
  # Return the rendered document as a string
1246
1403
  return self.render_document()
@@ -2108,16 +2265,15 @@ class AgentBase(SWMLService):
2108
2265
 
2109
2266
  # Allow subclasses to inspect/modify the request
2110
2267
  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))
2268
+ try:
2269
+ modifications = self.on_swml_request(body, callback_path, request)
2270
+ if modifications:
2271
+ req_log.debug("request_modifications_applied")
2272
+ except Exception as e:
2273
+ req_log.error("error_in_request_modifier", error=str(e))
2118
2274
 
2119
2275
  # Render SWML
2120
- swml = self._render_swml(call_id)
2276
+ swml = self._render_swml(call_id, modifications)
2121
2277
  req_log.debug("swml_rendered", swml_size=len(swml))
2122
2278
 
2123
2279
  # Return as JSON
@@ -2176,13 +2332,15 @@ class AgentBase(SWMLService):
2176
2332
 
2177
2333
  # Allow subclasses to inspect/modify the request
2178
2334
  modifications = None
2179
- if body:
2180
- modifications = self.on_swml_request(body)
2335
+ try:
2336
+ modifications = self.on_swml_request(body, None, request)
2181
2337
  if modifications:
2182
2338
  req_log.debug("request_modifications_applied")
2339
+ except Exception as e:
2340
+ req_log.error("error_in_request_modifier", error=str(e))
2183
2341
 
2184
2342
  # Render SWML
2185
- swml = self._render_swml(call_id)
2343
+ swml = self._render_swml(call_id, modifications)
2186
2344
  req_log.debug("swml_rendered", swml_size=len(swml))
2187
2345
 
2188
2346
  # Return as JSON
@@ -2492,24 +2650,66 @@ class AgentBase(SWMLService):
2492
2650
  """
2493
2651
  # First try to call on_swml_request if it exists (backward compatibility)
2494
2652
  if hasattr(self, 'on_swml_request') and callable(getattr(self, 'on_swml_request')):
2495
- return self.on_swml_request(request_data, callback_path)
2653
+ return self.on_swml_request(request_data, callback_path, None)
2496
2654
 
2497
2655
  # If no on_swml_request or it returned None, we'll proceed with default rendering
2498
2656
  # We're not returning any modifications here because _render_swml will be called
2499
2657
  # to generate the complete SWML document
2500
2658
  return None
2501
2659
 
2502
- def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
2660
+ def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None, request: Optional[Request] = None) -> Optional[dict]:
2503
2661
  """
2504
2662
  Customization point for subclasses to modify SWML based on request data
2505
2663
 
2506
2664
  Args:
2507
2665
  request_data: Optional dictionary containing the parsed POST body
2508
2666
  callback_path: Optional callback path
2667
+ request: Optional FastAPI Request object for accessing query params, headers, etc.
2509
2668
 
2510
2669
  Returns:
2511
2670
  Optional dict with modifications to apply to the SWML document
2512
2671
  """
2672
+ # Handle dynamic configuration callback if set
2673
+ if self._dynamic_config_callback and request:
2674
+ try:
2675
+ # Extract request data
2676
+ query_params = dict(request.query_params)
2677
+ body_params = request_data or {}
2678
+ headers = dict(request.headers)
2679
+
2680
+ # Create ephemeral configurator
2681
+ agent_config = EphemeralAgentConfig()
2682
+
2683
+ # Call the user's configuration callback
2684
+ self._dynamic_config_callback(query_params, body_params, headers, agent_config)
2685
+
2686
+ # Extract the configuration
2687
+ config = agent_config.extract_config()
2688
+ if config:
2689
+ # Handle ephemeral prompt sections by applying them to this agent instance
2690
+ if "_ephemeral_prompt_sections" in config:
2691
+ for section in config["_ephemeral_prompt_sections"]:
2692
+ self.prompt_add_section(
2693
+ section["title"],
2694
+ section.get("body", ""),
2695
+ section.get("bullets"),
2696
+ **{k: v for k, v in section.items() if k not in ["title", "body", "bullets"]}
2697
+ )
2698
+ del config["_ephemeral_prompt_sections"]
2699
+
2700
+ if "_ephemeral_raw_prompt" in config:
2701
+ self._raw_prompt = config["_ephemeral_raw_prompt"]
2702
+ del config["_ephemeral_raw_prompt"]
2703
+
2704
+ if "_ephemeral_post_prompt" in config:
2705
+ self._post_prompt = config["_ephemeral_post_prompt"]
2706
+ del config["_ephemeral_post_prompt"]
2707
+
2708
+ return config
2709
+
2710
+ except Exception as e:
2711
+ self.log.error("dynamic_config_error", error=str(e))
2712
+
2513
2713
  # Default implementation does nothing
2514
2714
  return None
2515
2715
 
@@ -2542,6 +2742,34 @@ class AgentBase(SWMLService):
2542
2742
  self._routing_callbacks = {}
2543
2743
  self._routing_callbacks[normalized_path] = callback_fn
2544
2744
 
2745
+ def set_dynamic_config_callback(self, callback: Callable[[dict, dict, dict, EphemeralAgentConfig], None]) -> 'AgentBase':
2746
+ """
2747
+ Set a callback function for dynamic agent configuration
2748
+
2749
+ This callback receives an EphemeralAgentConfig object that provides the same
2750
+ configuration methods as AgentBase, allowing you to dynamically configure
2751
+ the agent's voice, prompt, parameters, etc. based on request data.
2752
+
2753
+ Args:
2754
+ callback: Function that takes (query_params, body_params, headers, agent_config)
2755
+ and configures the agent_config object using familiar methods like:
2756
+ - agent_config.add_language(...)
2757
+ - agent_config.prompt_add_section(...)
2758
+ - agent_config.set_params(...)
2759
+ - agent_config.set_global_data(...)
2760
+
2761
+ Example:
2762
+ def my_config(query_params, body_params, headers, agent):
2763
+ if query_params.get('tier') == 'premium':
2764
+ agent.add_language("English", "en-US", "premium_voice")
2765
+ agent.set_params({"end_of_speech_timeout": 500})
2766
+ agent.set_global_data({"tier": query_params.get('tier', 'standard')})
2767
+
2768
+ my_agent.set_dynamic_config_callback(my_config)
2769
+ """
2770
+ self._dynamic_config_callback = callback
2771
+ return self
2772
+
2545
2773
  def manual_set_proxy_url(self, proxy_url: str) -> 'AgentBase':
2546
2774
  """
2547
2775
  Manually set the proxy URL base for webhook callbacks
@@ -2568,3 +2796,39 @@ class AgentBase(SWMLService):
2568
2796
  self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
2569
2797
 
2570
2798
  return self
2799
+
2800
+ # ----------------------------------------------------------------------
2801
+ # Skill Management Methods
2802
+ # ----------------------------------------------------------------------
2803
+
2804
+ def add_skill(self, skill_name: str, params: Optional[Dict[str, Any]] = None) -> 'AgentBase':
2805
+ """
2806
+ Add a skill to this agent
2807
+
2808
+ Args:
2809
+ skill_name: Name of the skill to add
2810
+ params: Optional parameters to pass to the skill for configuration
2811
+
2812
+ Returns:
2813
+ Self for method chaining
2814
+
2815
+ Raises:
2816
+ ValueError: If skill not found or failed to load with detailed error message
2817
+ """
2818
+ success, error_message = self.skill_manager.load_skill(skill_name, params=params)
2819
+ if not success:
2820
+ raise ValueError(f"Failed to load skill '{skill_name}': {error_message}")
2821
+ return self
2822
+
2823
+ def remove_skill(self, skill_name: str) -> 'AgentBase':
2824
+ """Remove a skill from this agent"""
2825
+ self.skill_manager.unload_skill(skill_name)
2826
+ return self
2827
+
2828
+ def list_skills(self) -> List[str]:
2829
+ """List currently loaded skills"""
2830
+ return self.skill_manager.list_loaded_skills()
2831
+
2832
+ def has_skill(self, skill_name: str) -> bool:
2833
+ """Check if skill is loaded"""
2834
+ return self.skill_manager.has_skill(skill_name)