signalwire-agents 0.1.1__py3-none-any.whl → 0.1.5__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 (34) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +1 -1
  3. signalwire_agents/core/__init__.py +29 -0
  4. signalwire_agents/core/agent_base.py +2541 -0
  5. signalwire_agents/core/function_result.py +123 -0
  6. signalwire_agents/core/pom_builder.py +204 -0
  7. signalwire_agents/core/security/__init__.py +9 -0
  8. signalwire_agents/core/security/session_manager.py +179 -0
  9. signalwire_agents/core/state/__init__.py +17 -0
  10. signalwire_agents/core/state/file_state_manager.py +219 -0
  11. signalwire_agents/core/state/state_manager.py +101 -0
  12. signalwire_agents/core/swaig_function.py +172 -0
  13. signalwire_agents/core/swml_builder.py +214 -0
  14. signalwire_agents/core/swml_handler.py +227 -0
  15. signalwire_agents/core/swml_renderer.py +368 -0
  16. signalwire_agents/core/swml_service.py +1057 -0
  17. signalwire_agents/prefabs/__init__.py +26 -0
  18. signalwire_agents/prefabs/concierge.py +267 -0
  19. signalwire_agents/prefabs/faq_bot.py +305 -0
  20. signalwire_agents/prefabs/info_gatherer.py +263 -0
  21. signalwire_agents/prefabs/receptionist.py +295 -0
  22. signalwire_agents/prefabs/survey.py +378 -0
  23. signalwire_agents/utils/__init__.py +9 -0
  24. signalwire_agents/utils/pom_utils.py +9 -0
  25. signalwire_agents/utils/schema_utils.py +357 -0
  26. signalwire_agents/utils/token_generators.py +9 -0
  27. signalwire_agents/utils/validators.py +9 -0
  28. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.5.dist-info}/METADATA +1 -1
  29. signalwire_agents-0.1.5.dist-info/RECORD +34 -0
  30. signalwire_agents-0.1.1.dist-info/RECORD +0 -9
  31. {signalwire_agents-0.1.1.data → signalwire_agents-0.1.5.data}/data/schema.json +0 -0
  32. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.5.dist-info}/WHEEL +0 -0
  33. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.5.dist-info}/licenses/LICENSE +0 -0
  34. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,227 @@
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
+ SWML Verb Handlers - Interface and implementations for SWML verb handling
12
+
13
+ This module defines the base interface for SWML verb handlers and provides
14
+ implementations for specific verbs that require special handling.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from typing import Dict, List, Any, Optional, Tuple
19
+
20
+
21
+ class SWMLVerbHandler(ABC):
22
+ """
23
+ Base interface for SWML verb handlers
24
+
25
+ This abstract class defines the interface that all SWML verb handlers
26
+ must implement. Verb handlers provide specialized logic for complex
27
+ SWML verbs that cannot be handled generically.
28
+ """
29
+
30
+ @abstractmethod
31
+ def get_verb_name(self) -> str:
32
+ """
33
+ Get the name of the verb this handler handles
34
+
35
+ Returns:
36
+ The verb name as a string
37
+ """
38
+ pass
39
+
40
+ @abstractmethod
41
+ def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
42
+ """
43
+ Validate the configuration for this verb
44
+
45
+ Args:
46
+ config: The configuration dictionary for this verb
47
+
48
+ Returns:
49
+ (is_valid, error_messages) tuple
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ def build_config(self, **kwargs) -> Dict[str, Any]:
55
+ """
56
+ Build a configuration for this verb from the provided arguments
57
+
58
+ Args:
59
+ **kwargs: Keyword arguments specific to this verb
60
+
61
+ Returns:
62
+ Configuration dictionary
63
+ """
64
+ pass
65
+
66
+
67
+ class AIVerbHandler(SWMLVerbHandler):
68
+ """
69
+ Handler for the SWML 'ai' verb
70
+
71
+ The 'ai' verb is complex and requires specialized handling, particularly
72
+ for managing prompts, SWAIG functions, and AI configurations.
73
+ """
74
+
75
+ def get_verb_name(self) -> str:
76
+ """
77
+ Get the name of the verb this handler handles
78
+
79
+ Returns:
80
+ "ai" as the verb name
81
+ """
82
+ return "ai"
83
+
84
+ def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
85
+ """
86
+ Validate the configuration for the AI verb
87
+
88
+ Args:
89
+ config: The configuration dictionary for the AI verb
90
+
91
+ Returns:
92
+ (is_valid, error_messages) tuple
93
+ """
94
+ errors = []
95
+
96
+ # Check required fields
97
+ if "prompt" not in config:
98
+ errors.append("Missing required field 'prompt'")
99
+
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'")
107
+
108
+ # Validate SWAIG structure if present
109
+ if "SWAIG" in config:
110
+ swaig = config["SWAIG"]
111
+ if not isinstance(swaig, dict):
112
+ errors.append("'SWAIG' must be an object")
113
+
114
+ return len(errors) == 0, errors
115
+
116
+ def build_config(self,
117
+ prompt_text: Optional[str] = None,
118
+ prompt_pom: Optional[List[Dict[str, Any]]] = None,
119
+ post_prompt: Optional[str] = None,
120
+ post_prompt_url: Optional[str] = None,
121
+ swaig: Optional[Dict[str, Any]] = None,
122
+ **kwargs) -> Dict[str, Any]:
123
+ """
124
+ Build a configuration for the AI verb
125
+
126
+ 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)
129
+ post_prompt: Optional post-prompt text
130
+ post_prompt_url: Optional URL for post-prompt processing
131
+ swaig: Optional SWAIG configuration
132
+ **kwargs: Additional AI parameters
133
+
134
+ Returns:
135
+ AI verb configuration dictionary
136
+ """
137
+ config = {}
138
+
139
+ # Add prompt (either text or POM)
140
+ if prompt_text is not None:
141
+ config["prompt"] = {"text": prompt_text}
142
+ elif prompt_pom is not None:
143
+ config["prompt"] = {"pom": prompt_pom}
144
+ else:
145
+ raise ValueError("Either prompt_text or prompt_pom must be provided")
146
+
147
+ # Add post-prompt if provided
148
+ if post_prompt is not None:
149
+ config["post_prompt"] = {"text": post_prompt}
150
+
151
+ # Add post-prompt URL if provided
152
+ if post_prompt_url is not None:
153
+ config["post_prompt_url"] = post_prompt_url
154
+
155
+ # Add SWAIG if provided
156
+ if swaig is not None:
157
+ config["SWAIG"] = swaig
158
+
159
+ # Add any additional parameters
160
+ if "params" not in config:
161
+ config["params"] = {}
162
+
163
+ for key, value in kwargs.items():
164
+ # Special handling for certain parameters
165
+ if key == "languages":
166
+ config["languages"] = value
167
+ elif key == "hints":
168
+ config["hints"] = value
169
+ elif key == "pronounce":
170
+ config["pronounce"] = value
171
+ elif key == "global_data":
172
+ config["global_data"] = value
173
+ else:
174
+ # Add to params object
175
+ config["params"][key] = value
176
+
177
+ return config
178
+
179
+
180
+ class VerbHandlerRegistry:
181
+ """
182
+ Registry for SWML verb handlers
183
+
184
+ This class maintains a registry of handlers for special SWML verbs
185
+ and provides methods for accessing and using them.
186
+ """
187
+
188
+ def __init__(self):
189
+ """Initialize the registry with default handlers"""
190
+ self._handlers = {}
191
+
192
+ # Register default handlers
193
+ self.register_handler(AIVerbHandler())
194
+
195
+ def register_handler(self, handler: SWMLVerbHandler) -> None:
196
+ """
197
+ Register a new verb handler
198
+
199
+ Args:
200
+ handler: The handler to register
201
+ """
202
+ verb_name = handler.get_verb_name()
203
+ self._handlers[verb_name] = handler
204
+
205
+ def get_handler(self, verb_name: str) -> Optional[SWMLVerbHandler]:
206
+ """
207
+ Get the handler for a specific verb
208
+
209
+ Args:
210
+ verb_name: The name of the verb
211
+
212
+ Returns:
213
+ The handler if found, None otherwise
214
+ """
215
+ return self._handlers.get(verb_name)
216
+
217
+ def has_handler(self, verb_name: str) -> bool:
218
+ """
219
+ Check if a handler exists for a specific verb
220
+
221
+ Args:
222
+ verb_name: The name of the verb
223
+
224
+ Returns:
225
+ True if a handler exists, False otherwise
226
+ """
227
+ return verb_name in self._handlers
@@ -0,0 +1,368 @@
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
+ SwmlRenderer for generating complete SWML documents for SignalWire AI Agents
12
+ """
13
+
14
+ from typing import Dict, List, Any, Optional, Union
15
+ import json
16
+ import yaml
17
+
18
+ from signalwire_agents.core.swml_service import SWMLService
19
+ from signalwire_agents.core.swml_builder import SWMLBuilder
20
+
21
+
22
+ class SwmlRenderer:
23
+ """
24
+ Renders SWML documents for SignalWire AI Agents with AI and SWAIG components
25
+
26
+ This class provides backward-compatible methods for rendering SWML documents
27
+ while also supporting the new SWMLService architecture. It can work either
28
+ standalone (legacy mode) or with a SWMLService instance.
29
+ """
30
+
31
+ @staticmethod
32
+ def render_swml(
33
+ prompt: Union[str, List[Dict[str, Any]]],
34
+ post_prompt: Optional[str] = None,
35
+ post_prompt_url: Optional[str] = None,
36
+ swaig_functions: Optional[List[Dict[str, Any]]] = None,
37
+ startup_hook_url: Optional[str] = None,
38
+ hangup_hook_url: Optional[str] = None,
39
+ prompt_is_pom: bool = False,
40
+ params: Optional[Dict[str, Any]] = None,
41
+ add_answer: bool = False,
42
+ record_call: bool = False,
43
+ record_format: str = "mp4",
44
+ record_stereo: bool = True,
45
+ format: str = "json",
46
+ default_webhook_url: Optional[str] = None,
47
+ service: Optional[SWMLService] = None
48
+ ) -> str:
49
+ """
50
+ Generate a complete SWML document with AI configuration
51
+
52
+ Args:
53
+ prompt: Either a string prompt or a POM in list-of-dict format
54
+ post_prompt: Optional post-prompt text (for summary)
55
+ post_prompt_url: URL to receive the post-prompt result
56
+ swaig_functions: List of SWAIG function definitions
57
+ startup_hook_url: URL for startup hook
58
+ hangup_hook_url: URL for hangup hook
59
+ prompt_is_pom: Whether prompt is a POM object or raw text
60
+ params: Additional AI params (temperature, etc)
61
+ add_answer: Whether to auto-add the answer block after AI
62
+ record_call: Whether to add a record_call block
63
+ record_format: Format for recording the call
64
+ record_stereo: Whether to record in stereo
65
+ format: Output format, 'json' or 'yaml'
66
+ default_webhook_url: Optional default webhook URL for all SWAIG functions
67
+ service: Optional SWMLService instance to use
68
+
69
+ Returns:
70
+ SWML document as a string
71
+ """
72
+ # If we have a service, use it to build the document
73
+ if service:
74
+ # Create a builder for the service
75
+ builder = SWMLBuilder(service)
76
+
77
+ # Reset the document to start fresh
78
+ builder.reset()
79
+
80
+ # Add answer block if requested
81
+ if add_answer:
82
+ builder.answer()
83
+
84
+ # Add record_call if requested
85
+ if record_call:
86
+ # TODO: Add record_call to builder API
87
+ service.add_verb("record_call", {
88
+ "format": record_format,
89
+ "stereo": record_stereo
90
+ })
91
+
92
+ # Configure SWAIG object for AI verb
93
+ swaig_config = {}
94
+ functions = []
95
+
96
+ # Add startup hook if provided
97
+ if startup_hook_url:
98
+ functions.append({
99
+ "function": "startup_hook",
100
+ "description": "Called when the call starts",
101
+ "parameters": {
102
+ "type": "object",
103
+ "properties": {}
104
+ },
105
+ "web_hook_url": startup_hook_url
106
+ })
107
+
108
+ # Add hangup hook if provided
109
+ if hangup_hook_url:
110
+ functions.append({
111
+ "function": "hangup_hook",
112
+ "description": "Called when the call ends",
113
+ "parameters": {
114
+ "type": "object",
115
+ "properties": {}
116
+ },
117
+ "web_hook_url": hangup_hook_url
118
+ })
119
+
120
+ # Add regular functions if provided
121
+ if swaig_functions:
122
+ for func in swaig_functions:
123
+ # Skip special hooks as we've already added them
124
+ if func.get("function") not in ["startup_hook", "hangup_hook"]:
125
+ functions.append(func)
126
+
127
+ # Only add SWAIG if we have functions or a default URL
128
+ if functions or default_webhook_url:
129
+ swaig_config = {}
130
+
131
+ # Add defaults if we have a default webhook URL
132
+ if default_webhook_url:
133
+ swaig_config["defaults"] = {
134
+ "web_hook_url": default_webhook_url
135
+ }
136
+
137
+ # Add functions if we have any
138
+ if functions:
139
+ swaig_config["functions"] = functions
140
+
141
+ # Add AI verb with appropriate configuration
142
+ builder.ai(
143
+ prompt_text=None if prompt_is_pom else prompt,
144
+ prompt_pom=prompt if prompt_is_pom else None,
145
+ post_prompt=post_prompt,
146
+ post_prompt_url=post_prompt_url,
147
+ swaig=swaig_config if swaig_config else None,
148
+ **(params or {})
149
+ )
150
+
151
+ # Get the document as a dictionary or string based on format
152
+ if format.lower() == "yaml":
153
+ import yaml
154
+ return yaml.dump(builder.build(), sort_keys=False)
155
+ else:
156
+ return builder.render()
157
+ else:
158
+ # Legacy implementation (unchanged for backward compatibility)
159
+ # Start building the SWML document
160
+ swml = {
161
+ "version": "1.0.0",
162
+ "sections": {
163
+ "main": []
164
+ }
165
+ }
166
+
167
+ # Build the AI block
168
+ ai_block = {
169
+ "ai": {
170
+ "prompt": {}
171
+ }
172
+ }
173
+
174
+ # Set prompt based on type
175
+ if prompt_is_pom:
176
+ ai_block["ai"]["prompt"]["pom"] = prompt
177
+ else:
178
+ ai_block["ai"]["prompt"]["text"] = prompt
179
+
180
+ # Add post_prompt if provided
181
+ if post_prompt:
182
+ ai_block["ai"]["post_prompt"] = {
183
+ "text": post_prompt
184
+ }
185
+
186
+ # Add post_prompt_url if provided
187
+ if post_prompt_url:
188
+ ai_block["ai"]["post_prompt_url"] = post_prompt_url
189
+
190
+ # SWAIG is a dictionary not an array (fix from old implementation)
191
+ ai_block["ai"]["SWAIG"] = {}
192
+
193
+ # Add defaults if we have a default webhook URL
194
+ if default_webhook_url:
195
+ ai_block["ai"]["SWAIG"]["defaults"] = {
196
+ "web_hook_url": default_webhook_url
197
+ }
198
+
199
+ # Collect all functions
200
+ functions = []
201
+
202
+ # Add SWAIG hooks if provided
203
+ if startup_hook_url:
204
+ startup_hook = {
205
+ "function": "startup_hook",
206
+ "description": "Called when the call starts",
207
+ "parameters": {
208
+ "type": "object",
209
+ "properties": {}
210
+ },
211
+ "web_hook_url": startup_hook_url
212
+ }
213
+ functions.append(startup_hook)
214
+
215
+ if hangup_hook_url:
216
+ hangup_hook = {
217
+ "function": "hangup_hook",
218
+ "description": "Called when the call ends",
219
+ "parameters": {
220
+ "type": "object",
221
+ "properties": {}
222
+ },
223
+ "web_hook_url": hangup_hook_url
224
+ }
225
+ functions.append(hangup_hook)
226
+
227
+ # Add regular functions from the provided list
228
+ if swaig_functions:
229
+ for func in swaig_functions:
230
+ # Skip special hooks as we've already added them
231
+ if func.get("function") not in ["startup_hook", "hangup_hook"]:
232
+ functions.append(func)
233
+
234
+ # Add functions to SWAIG if we have any
235
+ if functions:
236
+ ai_block["ai"]["SWAIG"]["functions"] = functions
237
+
238
+ # Add AI params if provided (but not rendering settings)
239
+ if params:
240
+ # Filter out non-AI parameters that should be separate SWML methods
241
+ ai_params = {k: v for k, v in params.items()
242
+ if k not in ["auto_answer", "record_call", "record_format", "record_stereo"]}
243
+
244
+ # Only update if we have valid AI parameters
245
+ if ai_params:
246
+ ai_block["ai"]["params"] = ai_params
247
+
248
+ # Start building the SWML blocks
249
+ main_blocks = []
250
+
251
+ # Add answer block first if requested (to answer the call)
252
+ if add_answer:
253
+ main_blocks.append({"answer": {}})
254
+
255
+ # Add record_call block next if requested
256
+ if record_call:
257
+ main_blocks.append({
258
+ "record_call": {
259
+ "format": record_format,
260
+ "stereo": record_stereo # SWML expects a boolean not a string
261
+ }
262
+ })
263
+
264
+ # Add the AI block
265
+ main_blocks.append(ai_block)
266
+
267
+ # Set the main section to our ordered blocks
268
+ swml["sections"]["main"] = main_blocks
269
+
270
+ # Return in requested format
271
+ if format.lower() == "yaml":
272
+ return yaml.dump(swml, sort_keys=False)
273
+ else:
274
+ return json.dumps(swml, indent=2)
275
+
276
+ @staticmethod
277
+ def render_function_response_swml(
278
+ response_text: str,
279
+ actions: Optional[List[Dict[str, Any]]] = None,
280
+ format: str = "json",
281
+ service: Optional[SWMLService] = None
282
+ ) -> str:
283
+ """
284
+ Generate a SWML document for a function response
285
+
286
+ Args:
287
+ response_text: Text to say/display
288
+ actions: List of SWML actions to execute
289
+ format: Output format, 'json' or 'yaml'
290
+ service: Optional SWMLService instance to use
291
+
292
+ Returns:
293
+ SWML document as a string
294
+ """
295
+ if service:
296
+ # Use the service to build the document
297
+ service.reset_document()
298
+
299
+ # Add a play block for the response if provided
300
+ if response_text:
301
+ service.add_verb("play", {
302
+ "url": f"say:{response_text}"
303
+ })
304
+
305
+ # Add any actions
306
+ if actions:
307
+ for action in actions:
308
+ if action["type"] == "play":
309
+ service.add_verb("play", {
310
+ "url": action["url"]
311
+ })
312
+ elif action["type"] == "transfer":
313
+ service.add_verb("connect", [
314
+ {"to": action["dest"]}
315
+ ])
316
+ elif action["type"] == "hang_up":
317
+ service.add_verb("hangup", {})
318
+ # Additional action types could be added here
319
+
320
+ # Return in requested format
321
+ if format.lower() == "yaml":
322
+ import yaml
323
+ return yaml.dump(service.get_document(), sort_keys=False)
324
+ else:
325
+ return service.render_document()
326
+ else:
327
+ # Legacy implementation (unchanged for backward compatibility)
328
+ swml = {
329
+ "version": "1.0.0",
330
+ "sections": {
331
+ "main": []
332
+ }
333
+ }
334
+
335
+ # Add a play block for the response if provided
336
+ if response_text:
337
+ swml["sections"]["main"].append({
338
+ "play": {
339
+ "url": f"say:{response_text}"
340
+ }
341
+ })
342
+
343
+ # Add any actions
344
+ if actions:
345
+ for action in actions:
346
+ if action["type"] == "play":
347
+ swml["sections"]["main"].append({
348
+ "play": {
349
+ "url": action["url"]
350
+ }
351
+ })
352
+ elif action["type"] == "transfer":
353
+ swml["sections"]["main"].append({
354
+ "connect": [
355
+ {"to": action["dest"]}
356
+ ]
357
+ })
358
+ elif action["type"] == "hang_up":
359
+ swml["sections"]["main"].append({
360
+ "hangup": {}
361
+ })
362
+ # Additional action types could be added here
363
+
364
+ # Return in requested format
365
+ if format.lower() == "yaml":
366
+ return yaml.dump(swml, sort_keys=False)
367
+ else:
368
+ return json.dumps(swml)