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.
- signalwire_agents/__init__.py +4 -1
- signalwire_agents/core/agent_base.py +305 -41
- signalwire_agents/core/function_result.py +1031 -1
- signalwire_agents/core/skill_base.py +87 -0
- signalwire_agents/core/skill_manager.py +136 -0
- 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/skills/__init__.py +14 -0
- signalwire_agents/skills/datetime/__init__.py +1 -0
- signalwire_agents/skills/datetime/skill.py +109 -0
- signalwire_agents/skills/math/__init__.py +1 -0
- signalwire_agents/skills/math/skill.py +88 -0
- signalwire_agents/skills/registry.py +98 -0
- signalwire_agents/skills/web_search/__init__.py +1 -0
- signalwire_agents/skills/web_search/skill.py +240 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/METADATA +113 -2
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/RECORD +22 -12
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/WHEEL +1 -1
- {signalwire_agents-0.1.7.data → signalwire_agents-0.1.9.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.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.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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
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
|
-
|
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)
|