signalwire-agents 0.1.0__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 (32) hide show
  1. signalwire_agents/__init__.py +17 -0
  2. signalwire_agents/agent_server.py +336 -0
  3. signalwire_agents/core/__init__.py +20 -0
  4. signalwire_agents/core/agent_base.py +2449 -0
  5. signalwire_agents/core/function_result.py +104 -0
  6. signalwire_agents/core/pom_builder.py +195 -0
  7. signalwire_agents/core/security/__init__.py +0 -0
  8. signalwire_agents/core/security/session_manager.py +170 -0
  9. signalwire_agents/core/state/__init__.py +8 -0
  10. signalwire_agents/core/state/file_state_manager.py +210 -0
  11. signalwire_agents/core/state/state_manager.py +92 -0
  12. signalwire_agents/core/swaig_function.py +163 -0
  13. signalwire_agents/core/swml_builder.py +205 -0
  14. signalwire_agents/core/swml_handler.py +218 -0
  15. signalwire_agents/core/swml_renderer.py +359 -0
  16. signalwire_agents/core/swml_service.py +1009 -0
  17. signalwire_agents/prefabs/__init__.py +15 -0
  18. signalwire_agents/prefabs/concierge.py +276 -0
  19. signalwire_agents/prefabs/faq_bot.py +314 -0
  20. signalwire_agents/prefabs/info_gatherer.py +253 -0
  21. signalwire_agents/prefabs/survey.py +387 -0
  22. signalwire_agents/schema.json +5611 -0
  23. signalwire_agents/utils/__init__.py +0 -0
  24. signalwire_agents/utils/pom_utils.py +0 -0
  25. signalwire_agents/utils/schema_utils.py +348 -0
  26. signalwire_agents/utils/token_generators.py +0 -0
  27. signalwire_agents/utils/validators.py +0 -0
  28. signalwire_agents-0.1.0.data/data/schema.json +5611 -0
  29. signalwire_agents-0.1.0.dist-info/METADATA +154 -0
  30. signalwire_agents-0.1.0.dist-info/RECORD +32 -0
  31. signalwire_agents-0.1.0.dist-info/WHEEL +5 -0
  32. signalwire_agents-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,92 @@
1
+ """
2
+ Abstract base class for state management
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Any, Optional
7
+
8
+
9
+ class StateManager(ABC):
10
+ """
11
+ Abstract base class for state management
12
+
13
+ This defines the interface that all state manager implementations
14
+ must follow. State managers are responsible for storing, retrieving,
15
+ and managing call-specific state data.
16
+ """
17
+
18
+ @abstractmethod
19
+ def store(self, call_id: str, data: Dict[str, Any]) -> bool:
20
+ """
21
+ Store state data for a call
22
+
23
+ Args:
24
+ call_id: Unique identifier for the call
25
+ data: Dictionary of state data to store
26
+
27
+ Returns:
28
+ True if successful, False otherwise
29
+ """
30
+ pass
31
+
32
+ @abstractmethod
33
+ def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
34
+ """
35
+ Retrieve state data for a call
36
+
37
+ Args:
38
+ call_id: Unique identifier for the call
39
+
40
+ Returns:
41
+ Dictionary of state data or None if not found
42
+ """
43
+ pass
44
+
45
+ @abstractmethod
46
+ def update(self, call_id: str, data: Dict[str, Any]) -> bool:
47
+ """
48
+ Update state data for a call
49
+
50
+ Args:
51
+ call_id: Unique identifier for the call
52
+ data: Dictionary of state data to update (merged with existing)
53
+
54
+ Returns:
55
+ True if successful, False otherwise
56
+ """
57
+ pass
58
+
59
+ @abstractmethod
60
+ def delete(self, call_id: str) -> bool:
61
+ """
62
+ Delete state data for a call
63
+
64
+ Args:
65
+ call_id: Unique identifier for the call
66
+
67
+ Returns:
68
+ True if successful, False otherwise
69
+ """
70
+ pass
71
+
72
+ @abstractmethod
73
+ def cleanup_expired(self) -> int:
74
+ """
75
+ Clean up expired state data
76
+
77
+ Returns:
78
+ Number of expired items cleaned up
79
+ """
80
+ pass
81
+
82
+ def exists(self, call_id: str) -> bool:
83
+ """
84
+ Check if state exists for a call
85
+
86
+ Args:
87
+ call_id: Unique identifier for the call
88
+
89
+ Returns:
90
+ True if state exists, False otherwise
91
+ """
92
+ return self.retrieve(call_id) is not None
@@ -0,0 +1,163 @@
1
+ """
2
+ SwaigFunction class for defining and managing SWAIG function interfaces
3
+ """
4
+
5
+ from typing import Dict, Any, Optional, Callable, List, Type, Union
6
+ import inspect
7
+ import logging
8
+
9
+
10
+ class SWAIGFunction:
11
+ """
12
+ Represents a SWAIG function for AI integration
13
+ """
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ handler: Callable,
18
+ description: str,
19
+ parameters: Dict[str, Dict] = None,
20
+ secure: bool = False,
21
+ fillers: Optional[Dict[str, List[str]]] = None
22
+ ):
23
+ """
24
+ Initialize a new SWAIG function
25
+
26
+ Args:
27
+ name: Name of the function to appear in SWML
28
+ handler: Function to call when this SWAIG function is invoked
29
+ description: Human-readable description of the function
30
+ parameters: Dictionary of parameters, keys are parameter names, values are param definitions
31
+ secure: Whether this function requires token validation
32
+ fillers: Optional dictionary of filler phrases by language code
33
+ """
34
+ self.name = name
35
+ self.handler = handler
36
+ self.description = description
37
+ self.parameters = parameters or {}
38
+ self.secure = secure
39
+ self.fillers = fillers
40
+
41
+ def _ensure_parameter_structure(self) -> Dict:
42
+ """
43
+ Ensure the parameters are correctly structured for SWML
44
+
45
+ Returns:
46
+ Parameters dict with correct structure
47
+ """
48
+ if not self.parameters:
49
+ return {"type": "object", "properties": {}}
50
+
51
+ # Check if we already have the correct structure
52
+ if "type" in self.parameters and "properties" in self.parameters:
53
+ return self.parameters
54
+
55
+ # Otherwise, wrap the parameters in the expected structure
56
+ return {
57
+ "type": "object",
58
+ "properties": self.parameters
59
+ }
60
+
61
+ def __call__(self, *args, **kwargs):
62
+ """
63
+ Call the underlying handler function
64
+ """
65
+ return self.handler(*args, **kwargs)
66
+
67
+ def execute(self, args: Dict[str, Any], raw_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
68
+ """
69
+ Execute the function with the given arguments
70
+
71
+ Args:
72
+ args: Parsed arguments for the function
73
+ raw_data: Optional raw request data
74
+
75
+ Returns:
76
+ Function result as a dictionary (from SwaigFunctionResult.to_dict())
77
+ """
78
+ try:
79
+ # Raw data is mandatory, but we'll handle the case where it's null for robustness
80
+ if raw_data is None:
81
+ raw_data = {} # Provide an empty dict as fallback
82
+
83
+ # Call the handler with both args and raw_data
84
+ result = self.handler(args, raw_data)
85
+
86
+ # Import here to avoid circular imports
87
+ from signalwire_agents.core.function_result import SwaigFunctionResult
88
+
89
+ # Handle different result types - everything must end up as a SwaigFunctionResult
90
+ if isinstance(result, SwaigFunctionResult):
91
+ # Already a SwaigFunctionResult - just convert to dict
92
+ return result.to_dict()
93
+ elif isinstance(result, dict) and "response" in result:
94
+ # Already in the correct format - use as is
95
+ return result
96
+ elif isinstance(result, dict):
97
+ # Dictionary without response - create a SwaigFunctionResult
98
+ return SwaigFunctionResult("Function completed successfully").to_dict()
99
+ else:
100
+ # String or other type - create a SwaigFunctionResult with the string representation
101
+ return SwaigFunctionResult(str(result)).to_dict()
102
+
103
+ except Exception as e:
104
+ # Log the error for debugging but don't expose details to the AI
105
+ logging.error(f"Error executing SWAIG function {self.name}: {str(e)}")
106
+ # Return a generic error message
107
+ return SwaigFunctionResult(
108
+ "Sorry, I couldn't complete that action. Please try again or contact support if the issue persists."
109
+ ).to_dict()
110
+
111
+ def validate_args(self, args: Dict[str, Any]) -> bool:
112
+ """
113
+ Validate the arguments against the parameter schema
114
+
115
+ Args:
116
+ args: Arguments to validate
117
+
118
+ Returns:
119
+ True if valid, False otherwise
120
+ """
121
+ # TODO: Implement JSON Schema validation
122
+ return True
123
+
124
+ def to_swaig(self, base_url: str, token: Optional[str] = None, call_id: Optional[str] = None, include_auth: bool = True) -> Dict[str, Any]:
125
+ """
126
+ Convert this function to a SWAIG-compatible JSON object for SWML
127
+
128
+ Args:
129
+ base_url: Base URL for the webhook
130
+ token: Optional auth token to include
131
+ call_id: Optional call ID for session tracking
132
+ include_auth: Whether to include auth credentials in URL
133
+
134
+ Returns:
135
+ Dictionary representation for the SWAIG array in SWML
136
+ """
137
+ # All functions use a single /swaig endpoint
138
+ url = f"{base_url}/swaig"
139
+
140
+ # Add token and call_id parameters if provided
141
+ if token and call_id:
142
+ url = f"{url}?token={token}&call_id={call_id}"
143
+
144
+ # Create properly structured function definition
145
+ function_def = {
146
+ "function": self.name,
147
+ "description": self.description,
148
+ "parameters": self._ensure_parameter_structure(),
149
+ }
150
+
151
+ # Only add web_hook_url if not using defaults
152
+ # This will be handled by the defaults section in the SWAIG array
153
+ if url:
154
+ function_def["web_hook_url"] = url
155
+
156
+ # Add fillers if provided
157
+ if self.fillers and len(self.fillers) > 0:
158
+ function_def["fillers"] = self.fillers
159
+
160
+ return function_def
161
+
162
+ # Add an alias for backward compatibility
163
+ SwaigFunction = SWAIGFunction
@@ -0,0 +1,205 @@
1
+ """
2
+ SWML Builder - Fluent API for building SWML documents
3
+
4
+ This module provides a fluent builder API for creating SWML documents.
5
+ It allows for chaining method calls to build up a document step by step.
6
+ """
7
+
8
+ from typing import Dict, List, Any, Optional, Union, Self, TypeVar
9
+
10
+ from signalwire_agents.core.swml_service import SWMLService
11
+
12
+
13
+ T = TypeVar('T', bound='SWMLBuilder')
14
+
15
+
16
+ class SWMLBuilder:
17
+ """
18
+ Fluent builder for SWML documents
19
+
20
+ This class provides a fluent interface for building SWML documents
21
+ by chaining method calls. It delegates to an underlying SWMLService
22
+ instance for the actual document creation.
23
+ """
24
+
25
+ def __init__(self, service: SWMLService):
26
+ """
27
+ Initialize with a SWMLService instance
28
+
29
+ Args:
30
+ service: The SWMLService to delegate to
31
+ """
32
+ self.service = service
33
+
34
+ def answer(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> Self:
35
+ """
36
+ Add an 'answer' verb to the main section
37
+
38
+ Args:
39
+ max_duration: Maximum duration in seconds
40
+ codecs: Comma-separated list of codecs
41
+
42
+ Returns:
43
+ Self for method chaining
44
+ """
45
+ self.service.add_answer_verb(max_duration, codecs)
46
+ return self
47
+
48
+ def hangup(self, reason: Optional[str] = None) -> Self:
49
+ """
50
+ Add a 'hangup' verb to the main section
51
+
52
+ Args:
53
+ reason: Optional reason for hangup
54
+
55
+ Returns:
56
+ Self for method chaining
57
+ """
58
+ self.service.add_hangup_verb(reason)
59
+ return self
60
+
61
+ def ai(self,
62
+ prompt_text: Optional[str] = None,
63
+ prompt_pom: Optional[List[Dict[str, Any]]] = None,
64
+ post_prompt: Optional[str] = None,
65
+ post_prompt_url: Optional[str] = None,
66
+ swaig: Optional[Dict[str, Any]] = None,
67
+ **kwargs) -> Self:
68
+ """
69
+ Add an 'ai' verb to the main section
70
+
71
+ Args:
72
+ prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
73
+ prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
74
+ post_prompt: Optional post-prompt text
75
+ post_prompt_url: Optional URL for post-prompt processing
76
+ swaig: Optional SWAIG configuration
77
+ **kwargs: Additional AI parameters
78
+
79
+ Returns:
80
+ Self for method chaining
81
+ """
82
+ self.service.add_ai_verb(
83
+ prompt_text=prompt_text,
84
+ prompt_pom=prompt_pom,
85
+ post_prompt=post_prompt,
86
+ post_prompt_url=post_prompt_url,
87
+ swaig=swaig,
88
+ **kwargs
89
+ )
90
+ return self
91
+
92
+ def play(self, url: Optional[str] = None, urls: Optional[List[str]] = None,
93
+ volume: Optional[float] = None, say_voice: Optional[str] = None,
94
+ say_language: Optional[str] = None, say_gender: Optional[str] = None,
95
+ auto_answer: Optional[bool] = None) -> Self:
96
+ """
97
+ Add a 'play' verb to the main section
98
+
99
+ Args:
100
+ url: Single URL to play (mutually exclusive with urls)
101
+ urls: List of URLs to play (mutually exclusive with url)
102
+ volume: Volume level (-40 to 40)
103
+ say_voice: Voice for text-to-speech
104
+ say_language: Language for text-to-speech
105
+ say_gender: Gender for text-to-speech
106
+ auto_answer: Whether to auto-answer the call
107
+
108
+ Returns:
109
+ Self for method chaining
110
+ """
111
+ # Create base config
112
+ config = {}
113
+
114
+ # Add play config (either single URL or list)
115
+ if url is not None:
116
+ config["url"] = url
117
+ elif urls is not None:
118
+ config["urls"] = urls
119
+ else:
120
+ raise ValueError("Either url or urls must be provided")
121
+
122
+ # Add optional parameters
123
+ if volume is not None:
124
+ config["volume"] = volume
125
+ if say_voice is not None:
126
+ config["say_voice"] = say_voice
127
+ if say_language is not None:
128
+ config["say_language"] = say_language
129
+ if say_gender is not None:
130
+ config["say_gender"] = say_gender
131
+ if auto_answer is not None:
132
+ config["auto_answer"] = auto_answer
133
+
134
+ # Add the verb
135
+ self.service.add_verb("play", config)
136
+ return self
137
+
138
+ def say(self, text: str, voice: Optional[str] = None,
139
+ language: Optional[str] = None, gender: Optional[str] = None,
140
+ volume: Optional[float] = None) -> Self:
141
+ """
142
+ Add a 'play' verb with say: prefix for text-to-speech
143
+
144
+ Args:
145
+ text: Text to speak
146
+ voice: Voice for text-to-speech
147
+ language: Language for text-to-speech
148
+ gender: Gender for text-to-speech
149
+ volume: Volume level (-40 to 40)
150
+
151
+ Returns:
152
+ Self for method chaining
153
+ """
154
+ # Create play config with say: prefix
155
+ url = f"say:{text}"
156
+
157
+ # Add the verb
158
+ return self.play(
159
+ url=url,
160
+ say_voice=voice,
161
+ say_language=language,
162
+ say_gender=gender,
163
+ volume=volume
164
+ )
165
+
166
+ def add_section(self, section_name: str) -> Self:
167
+ """
168
+ Add a new section to the document
169
+
170
+ Args:
171
+ section_name: Name of the section to add
172
+
173
+ Returns:
174
+ Self for method chaining
175
+ """
176
+ self.service.add_section(section_name)
177
+ return self
178
+
179
+ def build(self) -> Dict[str, Any]:
180
+ """
181
+ Build and return the SWML document
182
+
183
+ Returns:
184
+ The complete SWML document as a dictionary
185
+ """
186
+ return self.service.get_document()
187
+
188
+ def render(self) -> str:
189
+ """
190
+ Build and render the SWML document as a JSON string
191
+
192
+ Returns:
193
+ The complete SWML document as a JSON string
194
+ """
195
+ return self.service.render_document()
196
+
197
+ def reset(self) -> Self:
198
+ """
199
+ Reset the document to an empty state
200
+
201
+ Returns:
202
+ Self for method chaining
203
+ """
204
+ self.service.reset_document()
205
+ return self
@@ -0,0 +1,218 @@
1
+ """
2
+ SWML Verb Handlers - Interface and implementations for SWML verb handling
3
+
4
+ This module defines the base interface for SWML verb handlers and provides
5
+ implementations for specific verbs that require special handling.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Dict, List, Any, Optional, Tuple
10
+
11
+
12
+ class SWMLVerbHandler(ABC):
13
+ """
14
+ Base interface for SWML verb handlers
15
+
16
+ This abstract class defines the interface that all SWML verb handlers
17
+ must implement. Verb handlers provide specialized logic for complex
18
+ SWML verbs that cannot be handled generically.
19
+ """
20
+
21
+ @abstractmethod
22
+ def get_verb_name(self) -> str:
23
+ """
24
+ Get the name of the verb this handler handles
25
+
26
+ Returns:
27
+ The verb name as a string
28
+ """
29
+ pass
30
+
31
+ @abstractmethod
32
+ def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
33
+ """
34
+ Validate the configuration for this verb
35
+
36
+ Args:
37
+ config: The configuration dictionary for this verb
38
+
39
+ Returns:
40
+ (is_valid, error_messages) tuple
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ def build_config(self, **kwargs) -> Dict[str, Any]:
46
+ """
47
+ Build a configuration for this verb from the provided arguments
48
+
49
+ Args:
50
+ **kwargs: Keyword arguments specific to this verb
51
+
52
+ Returns:
53
+ Configuration dictionary
54
+ """
55
+ pass
56
+
57
+
58
+ class AIVerbHandler(SWMLVerbHandler):
59
+ """
60
+ Handler for the SWML 'ai' verb
61
+
62
+ The 'ai' verb is complex and requires specialized handling, particularly
63
+ for managing prompts, SWAIG functions, and AI configurations.
64
+ """
65
+
66
+ def get_verb_name(self) -> str:
67
+ """
68
+ Get the name of the verb this handler handles
69
+
70
+ Returns:
71
+ "ai" as the verb name
72
+ """
73
+ return "ai"
74
+
75
+ def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
76
+ """
77
+ Validate the configuration for the AI verb
78
+
79
+ Args:
80
+ config: The configuration dictionary for the AI verb
81
+
82
+ Returns:
83
+ (is_valid, error_messages) tuple
84
+ """
85
+ errors = []
86
+
87
+ # Check required fields
88
+ if "prompt" not in config:
89
+ errors.append("Missing required field 'prompt'")
90
+
91
+ # Validate prompt structure if present
92
+ if "prompt" in config:
93
+ prompt = config["prompt"]
94
+ if not isinstance(prompt, dict):
95
+ errors.append("'prompt' must be an object")
96
+ elif "text" not in prompt and "pom" not in prompt:
97
+ errors.append("'prompt' must contain either 'text' or 'pom'")
98
+
99
+ # Validate SWAIG structure if present
100
+ if "SWAIG" in config:
101
+ swaig = config["SWAIG"]
102
+ if not isinstance(swaig, dict):
103
+ errors.append("'SWAIG' must be an object")
104
+
105
+ return len(errors) == 0, errors
106
+
107
+ def build_config(self,
108
+ prompt_text: Optional[str] = None,
109
+ prompt_pom: Optional[List[Dict[str, Any]]] = None,
110
+ post_prompt: Optional[str] = None,
111
+ post_prompt_url: Optional[str] = None,
112
+ swaig: Optional[Dict[str, Any]] = None,
113
+ **kwargs) -> Dict[str, Any]:
114
+ """
115
+ Build a configuration for the AI verb
116
+
117
+ Args:
118
+ prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
119
+ prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
120
+ post_prompt: Optional post-prompt text
121
+ post_prompt_url: Optional URL for post-prompt processing
122
+ swaig: Optional SWAIG configuration
123
+ **kwargs: Additional AI parameters
124
+
125
+ Returns:
126
+ AI verb configuration dictionary
127
+ """
128
+ config = {}
129
+
130
+ # Add prompt (either text or POM)
131
+ if prompt_text is not None:
132
+ config["prompt"] = {"text": prompt_text}
133
+ elif prompt_pom is not None:
134
+ config["prompt"] = {"pom": prompt_pom}
135
+ else:
136
+ raise ValueError("Either prompt_text or prompt_pom must be provided")
137
+
138
+ # Add post-prompt if provided
139
+ if post_prompt is not None:
140
+ config["post_prompt"] = {"text": post_prompt}
141
+
142
+ # Add post-prompt URL if provided
143
+ if post_prompt_url is not None:
144
+ config["post_prompt_url"] = post_prompt_url
145
+
146
+ # Add SWAIG if provided
147
+ if swaig is not None:
148
+ config["SWAIG"] = swaig
149
+
150
+ # Add any additional parameters
151
+ if "params" not in config:
152
+ config["params"] = {}
153
+
154
+ for key, value in kwargs.items():
155
+ # Special handling for certain parameters
156
+ if key == "languages":
157
+ config["languages"] = value
158
+ elif key == "hints":
159
+ config["hints"] = value
160
+ elif key == "pronounce":
161
+ config["pronounce"] = value
162
+ elif key == "global_data":
163
+ config["global_data"] = value
164
+ else:
165
+ # Add to params object
166
+ config["params"][key] = value
167
+
168
+ return config
169
+
170
+
171
+ class VerbHandlerRegistry:
172
+ """
173
+ Registry for SWML verb handlers
174
+
175
+ This class maintains a registry of handlers for special SWML verbs
176
+ and provides methods for accessing and using them.
177
+ """
178
+
179
+ def __init__(self):
180
+ """Initialize the registry with default handlers"""
181
+ self._handlers = {}
182
+
183
+ # Register default handlers
184
+ self.register_handler(AIVerbHandler())
185
+
186
+ def register_handler(self, handler: SWMLVerbHandler) -> None:
187
+ """
188
+ Register a new verb handler
189
+
190
+ Args:
191
+ handler: The handler to register
192
+ """
193
+ verb_name = handler.get_verb_name()
194
+ self._handlers[verb_name] = handler
195
+
196
+ def get_handler(self, verb_name: str) -> Optional[SWMLVerbHandler]:
197
+ """
198
+ Get the handler for a specific verb
199
+
200
+ Args:
201
+ verb_name: The name of the verb
202
+
203
+ Returns:
204
+ The handler if found, None otherwise
205
+ """
206
+ return self._handlers.get(verb_name)
207
+
208
+ def has_handler(self, verb_name: str) -> bool:
209
+ """
210
+ Check if a handler exists for a specific verb
211
+
212
+ Args:
213
+ verb_name: The name of the verb
214
+
215
+ Returns:
216
+ True if a handler exists, False otherwise
217
+ """
218
+ return verb_name in self._handlers