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.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/core/agent_base.py +265 -41
- signalwire_agents/core/function_result.py +1031 -1
- signalwire_agents/core/swml_builder.py +5 -1
- signalwire_agents/prefabs/info_gatherer.py +149 -33
- signalwire_agents/prefabs/receptionist.py +14 -22
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.8.dist-info}/METADATA +70 -2
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.8.dist-info}/RECORD +12 -12
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.8.dist-info}/WHEEL +1 -1
- {signalwire_agents-0.1.7.data → signalwire_agents-0.1.8.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.8.dist-info}/top_level.txt +0 -0
signalwire_agents/__init__.py
CHANGED
@@ -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.
|
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
|
-
#
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
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
|
-
|
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
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
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
|
-
|
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
|