signalwire-agents 0.1.1__py3-none-any.whl → 0.1.2__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/agent_server.py +1 -1
- signalwire_agents/core/__init__.py +29 -0
- signalwire_agents/core/agent_base.py +2541 -0
- signalwire_agents/core/function_result.py +123 -0
- signalwire_agents/core/pom_builder.py +204 -0
- signalwire_agents/core/security/__init__.py +9 -0
- signalwire_agents/core/security/session_manager.py +179 -0
- signalwire_agents/core/state/__init__.py +17 -0
- signalwire_agents/core/state/file_state_manager.py +219 -0
- signalwire_agents/core/state/state_manager.py +101 -0
- signalwire_agents/core/swaig_function.py +172 -0
- signalwire_agents/core/swml_builder.py +214 -0
- signalwire_agents/core/swml_handler.py +227 -0
- signalwire_agents/core/swml_renderer.py +368 -0
- signalwire_agents/core/swml_service.py +1057 -0
- signalwire_agents/prefabs/__init__.py +26 -0
- signalwire_agents/prefabs/concierge.py +267 -0
- signalwire_agents/prefabs/faq_bot.py +305 -0
- signalwire_agents/prefabs/info_gatherer.py +263 -0
- signalwire_agents/prefabs/receptionist.py +294 -0
- signalwire_agents/prefabs/survey.py +378 -0
- signalwire_agents/utils/__init__.py +9 -0
- signalwire_agents/utils/pom_utils.py +9 -0
- signalwire_agents/utils/schema_utils.py +357 -0
- signalwire_agents/utils/token_generators.py +9 -0
- signalwire_agents/utils/validators.py +9 -0
- {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +1 -1
- signalwire_agents-0.1.2.dist-info/RECORD +34 -0
- signalwire_agents-0.1.1.dist-info/RECORD +0 -9
- {signalwire_agents-0.1.1.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,219 @@
|
|
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
|
+
File-based implementation of the StateManager interface
|
12
|
+
"""
|
13
|
+
|
14
|
+
import os
|
15
|
+
import json
|
16
|
+
import tempfile
|
17
|
+
import time
|
18
|
+
from datetime import datetime, timedelta
|
19
|
+
from typing import Dict, Any, Optional, List
|
20
|
+
|
21
|
+
from .state_manager import StateManager
|
22
|
+
|
23
|
+
|
24
|
+
class FileStateManager(StateManager):
|
25
|
+
"""
|
26
|
+
File-based state manager implementation
|
27
|
+
|
28
|
+
This implementation stores state data as JSON files in a directory.
|
29
|
+
Each call's state is stored in a separate file named by call_id.
|
30
|
+
Files older than expiry_days are automatically cleaned up.
|
31
|
+
|
32
|
+
This is suitable for development and low-volume deployments.
|
33
|
+
For production, consider using database or Redis implementations.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
storage_dir: Optional[str] = None,
|
39
|
+
expiry_days: float = 1.0
|
40
|
+
):
|
41
|
+
"""
|
42
|
+
Initialize the file state manager
|
43
|
+
|
44
|
+
Args:
|
45
|
+
storage_dir: Directory to store state files (default: system temp dir)
|
46
|
+
expiry_days: Days after which state files are considered expired
|
47
|
+
"""
|
48
|
+
self.storage_dir = storage_dir or os.path.join(tempfile.gettempdir(), "signalwire_state")
|
49
|
+
self.expiry_days = expiry_days
|
50
|
+
|
51
|
+
# Create storage directory if it doesn't exist
|
52
|
+
if not os.path.exists(self.storage_dir):
|
53
|
+
os.makedirs(self.storage_dir)
|
54
|
+
|
55
|
+
def _get_file_path(self, call_id: str) -> str:
|
56
|
+
"""Get the file path for a call_id"""
|
57
|
+
# Sanitize call_id to ensure it's safe for a filename
|
58
|
+
sanitized_id = "".join(c for c in call_id if c.isalnum() or c in "-_.")
|
59
|
+
return os.path.join(self.storage_dir, f"{sanitized_id}.json")
|
60
|
+
|
61
|
+
def store(self, call_id: str, data: Dict[str, Any]) -> bool:
|
62
|
+
"""
|
63
|
+
Store state data for a call
|
64
|
+
|
65
|
+
Args:
|
66
|
+
call_id: Unique identifier for the call
|
67
|
+
data: Dictionary of state data to store
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
True if successful, False otherwise
|
71
|
+
"""
|
72
|
+
try:
|
73
|
+
# Add metadata including timestamp
|
74
|
+
state_data = {
|
75
|
+
"call_id": call_id,
|
76
|
+
"created_at": datetime.now().isoformat(),
|
77
|
+
"last_updated": datetime.now().isoformat(),
|
78
|
+
"data": data
|
79
|
+
}
|
80
|
+
|
81
|
+
file_path = self._get_file_path(call_id)
|
82
|
+
with open(file_path, "w") as f:
|
83
|
+
json.dump(state_data, f, indent=2)
|
84
|
+
return True
|
85
|
+
except Exception as e:
|
86
|
+
print(f"Error storing state for call {call_id}: {e}")
|
87
|
+
return False
|
88
|
+
|
89
|
+
def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
|
90
|
+
"""
|
91
|
+
Retrieve state data for a call
|
92
|
+
|
93
|
+
Args:
|
94
|
+
call_id: Unique identifier for the call
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Dictionary of state data or None if not found
|
98
|
+
"""
|
99
|
+
file_path = self._get_file_path(call_id)
|
100
|
+
if not os.path.exists(file_path):
|
101
|
+
return None
|
102
|
+
|
103
|
+
try:
|
104
|
+
with open(file_path, "r") as f:
|
105
|
+
state_data = json.load(f)
|
106
|
+
|
107
|
+
# Check if the file is expired
|
108
|
+
created_at = datetime.fromisoformat(state_data["created_at"])
|
109
|
+
if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
|
110
|
+
# Expired, so delete it and return None
|
111
|
+
os.remove(file_path)
|
112
|
+
return None
|
113
|
+
|
114
|
+
return state_data["data"]
|
115
|
+
except Exception as e:
|
116
|
+
print(f"Error retrieving state for call {call_id}: {e}")
|
117
|
+
return None
|
118
|
+
|
119
|
+
def update(self, call_id: str, data: Dict[str, Any]) -> bool:
|
120
|
+
"""
|
121
|
+
Update state data for a call
|
122
|
+
|
123
|
+
Args:
|
124
|
+
call_id: Unique identifier for the call
|
125
|
+
data: Dictionary of state data to update (merged with existing)
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
True if successful, False otherwise
|
129
|
+
"""
|
130
|
+
file_path = self._get_file_path(call_id)
|
131
|
+
if not os.path.exists(file_path):
|
132
|
+
# If no existing data, just store new data
|
133
|
+
return self.store(call_id, data)
|
134
|
+
|
135
|
+
try:
|
136
|
+
# Read existing data
|
137
|
+
with open(file_path, "r") as f:
|
138
|
+
state_data = json.load(f)
|
139
|
+
|
140
|
+
# Update the data (deep merge)
|
141
|
+
existing_data = state_data["data"]
|
142
|
+
self._deep_update(existing_data, data)
|
143
|
+
|
144
|
+
# Update metadata
|
145
|
+
state_data["last_updated"] = datetime.now().isoformat()
|
146
|
+
state_data["data"] = existing_data
|
147
|
+
|
148
|
+
# Write back to file
|
149
|
+
with open(file_path, "w") as f:
|
150
|
+
json.dump(state_data, f, indent=2)
|
151
|
+
|
152
|
+
return True
|
153
|
+
except Exception as e:
|
154
|
+
print(f"Error updating state for call {call_id}: {e}")
|
155
|
+
return False
|
156
|
+
|
157
|
+
def delete(self, call_id: str) -> bool:
|
158
|
+
"""
|
159
|
+
Delete state data for a call
|
160
|
+
|
161
|
+
Args:
|
162
|
+
call_id: Unique identifier for the call
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
True if successful, False otherwise
|
166
|
+
"""
|
167
|
+
file_path = self._get_file_path(call_id)
|
168
|
+
if not os.path.exists(file_path):
|
169
|
+
return False
|
170
|
+
|
171
|
+
try:
|
172
|
+
os.remove(file_path)
|
173
|
+
return True
|
174
|
+
except Exception as e:
|
175
|
+
print(f"Error deleting state for call {call_id}: {e}")
|
176
|
+
return False
|
177
|
+
|
178
|
+
def cleanup_expired(self) -> int:
|
179
|
+
"""
|
180
|
+
Clean up expired state files
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Number of expired files cleaned up
|
184
|
+
"""
|
185
|
+
count = 0
|
186
|
+
try:
|
187
|
+
# Get all state files
|
188
|
+
for filename in os.listdir(self.storage_dir):
|
189
|
+
if not filename.endswith(".json"):
|
190
|
+
continue
|
191
|
+
|
192
|
+
file_path = os.path.join(self.storage_dir, filename)
|
193
|
+
|
194
|
+
try:
|
195
|
+
# Read the file to check creation time
|
196
|
+
with open(file_path, "r") as f:
|
197
|
+
state_data = json.load(f)
|
198
|
+
|
199
|
+
# Check if the file is expired
|
200
|
+
created_at = datetime.fromisoformat(state_data["created_at"])
|
201
|
+
if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
|
202
|
+
os.remove(file_path)
|
203
|
+
count += 1
|
204
|
+
except Exception:
|
205
|
+
# Skip files that can't be processed
|
206
|
+
continue
|
207
|
+
|
208
|
+
return count
|
209
|
+
except Exception as e:
|
210
|
+
print(f"Error cleaning up expired state files: {e}")
|
211
|
+
return count
|
212
|
+
|
213
|
+
def _deep_update(self, d: Dict, u: Dict) -> None:
|
214
|
+
"""Recursively update a nested dictionary"""
|
215
|
+
for k, v in u.items():
|
216
|
+
if isinstance(v, dict) and k in d and isinstance(d[k], dict):
|
217
|
+
self._deep_update(d[k], v)
|
218
|
+
else:
|
219
|
+
d[k] = v
|
@@ -0,0 +1,101 @@
|
|
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
|
+
Abstract base class for state management
|
12
|
+
"""
|
13
|
+
|
14
|
+
from abc import ABC, abstractmethod
|
15
|
+
from typing import Dict, Any, Optional
|
16
|
+
|
17
|
+
|
18
|
+
class StateManager(ABC):
|
19
|
+
"""
|
20
|
+
Abstract base class for state management
|
21
|
+
|
22
|
+
This defines the interface that all state manager implementations
|
23
|
+
must follow. State managers are responsible for storing, retrieving,
|
24
|
+
and managing call-specific state data.
|
25
|
+
"""
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def store(self, call_id: str, data: Dict[str, Any]) -> bool:
|
29
|
+
"""
|
30
|
+
Store state data for a call
|
31
|
+
|
32
|
+
Args:
|
33
|
+
call_id: Unique identifier for the call
|
34
|
+
data: Dictionary of state data to store
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
True if successful, False otherwise
|
38
|
+
"""
|
39
|
+
pass
|
40
|
+
|
41
|
+
@abstractmethod
|
42
|
+
def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
|
43
|
+
"""
|
44
|
+
Retrieve state data for a call
|
45
|
+
|
46
|
+
Args:
|
47
|
+
call_id: Unique identifier for the call
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Dictionary of state data or None if not found
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
@abstractmethod
|
55
|
+
def update(self, call_id: str, data: Dict[str, Any]) -> bool:
|
56
|
+
"""
|
57
|
+
Update state data for a call
|
58
|
+
|
59
|
+
Args:
|
60
|
+
call_id: Unique identifier for the call
|
61
|
+
data: Dictionary of state data to update (merged with existing)
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
True if successful, False otherwise
|
65
|
+
"""
|
66
|
+
pass
|
67
|
+
|
68
|
+
@abstractmethod
|
69
|
+
def delete(self, call_id: str) -> bool:
|
70
|
+
"""
|
71
|
+
Delete state data for a call
|
72
|
+
|
73
|
+
Args:
|
74
|
+
call_id: Unique identifier for the call
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
True if successful, False otherwise
|
78
|
+
"""
|
79
|
+
pass
|
80
|
+
|
81
|
+
@abstractmethod
|
82
|
+
def cleanup_expired(self) -> int:
|
83
|
+
"""
|
84
|
+
Clean up expired state data
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Number of expired items cleaned up
|
88
|
+
"""
|
89
|
+
pass
|
90
|
+
|
91
|
+
def exists(self, call_id: str) -> bool:
|
92
|
+
"""
|
93
|
+
Check if state exists for a call
|
94
|
+
|
95
|
+
Args:
|
96
|
+
call_id: Unique identifier for the call
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
True if state exists, False otherwise
|
100
|
+
"""
|
101
|
+
return self.retrieve(call_id) is not None
|
@@ -0,0 +1,172 @@
|
|
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
|
+
SwaigFunction class for defining and managing SWAIG function interfaces
|
12
|
+
"""
|
13
|
+
|
14
|
+
from typing import Dict, Any, Optional, Callable, List, Type, Union
|
15
|
+
import inspect
|
16
|
+
import logging
|
17
|
+
|
18
|
+
|
19
|
+
class SWAIGFunction:
|
20
|
+
"""
|
21
|
+
Represents a SWAIG function for AI integration
|
22
|
+
"""
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
name: str,
|
26
|
+
handler: Callable,
|
27
|
+
description: str,
|
28
|
+
parameters: Dict[str, Dict] = None,
|
29
|
+
secure: bool = False,
|
30
|
+
fillers: Optional[Dict[str, List[str]]] = None
|
31
|
+
):
|
32
|
+
"""
|
33
|
+
Initialize a new SWAIG function
|
34
|
+
|
35
|
+
Args:
|
36
|
+
name: Name of the function to appear in SWML
|
37
|
+
handler: Function to call when this SWAIG function is invoked
|
38
|
+
description: Human-readable description of the function
|
39
|
+
parameters: Dictionary of parameters, keys are parameter names, values are param definitions
|
40
|
+
secure: Whether this function requires token validation
|
41
|
+
fillers: Optional dictionary of filler phrases by language code
|
42
|
+
"""
|
43
|
+
self.name = name
|
44
|
+
self.handler = handler
|
45
|
+
self.description = description
|
46
|
+
self.parameters = parameters or {}
|
47
|
+
self.secure = secure
|
48
|
+
self.fillers = fillers
|
49
|
+
|
50
|
+
def _ensure_parameter_structure(self) -> Dict:
|
51
|
+
"""
|
52
|
+
Ensure the parameters are correctly structured for SWML
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Parameters dict with correct structure
|
56
|
+
"""
|
57
|
+
if not self.parameters:
|
58
|
+
return {"type": "object", "properties": {}}
|
59
|
+
|
60
|
+
# Check if we already have the correct structure
|
61
|
+
if "type" in self.parameters and "properties" in self.parameters:
|
62
|
+
return self.parameters
|
63
|
+
|
64
|
+
# Otherwise, wrap the parameters in the expected structure
|
65
|
+
return {
|
66
|
+
"type": "object",
|
67
|
+
"properties": self.parameters
|
68
|
+
}
|
69
|
+
|
70
|
+
def __call__(self, *args, **kwargs):
|
71
|
+
"""
|
72
|
+
Call the underlying handler function
|
73
|
+
"""
|
74
|
+
return self.handler(*args, **kwargs)
|
75
|
+
|
76
|
+
def execute(self, args: Dict[str, Any], raw_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
77
|
+
"""
|
78
|
+
Execute the function with the given arguments
|
79
|
+
|
80
|
+
Args:
|
81
|
+
args: Parsed arguments for the function
|
82
|
+
raw_data: Optional raw request data
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Function result as a dictionary (from SwaigFunctionResult.to_dict())
|
86
|
+
"""
|
87
|
+
try:
|
88
|
+
# Raw data is mandatory, but we'll handle the case where it's null for robustness
|
89
|
+
if raw_data is None:
|
90
|
+
raw_data = {} # Provide an empty dict as fallback
|
91
|
+
|
92
|
+
# Call the handler with both args and raw_data
|
93
|
+
result = self.handler(args, raw_data)
|
94
|
+
|
95
|
+
# Import here to avoid circular imports
|
96
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
97
|
+
|
98
|
+
# Handle different result types - everything must end up as a SwaigFunctionResult
|
99
|
+
if isinstance(result, SwaigFunctionResult):
|
100
|
+
# Already a SwaigFunctionResult - just convert to dict
|
101
|
+
return result.to_dict()
|
102
|
+
elif isinstance(result, dict) and "response" in result:
|
103
|
+
# Already in the correct format - use as is
|
104
|
+
return result
|
105
|
+
elif isinstance(result, dict):
|
106
|
+
# Dictionary without response - create a SwaigFunctionResult
|
107
|
+
return SwaigFunctionResult("Function completed successfully").to_dict()
|
108
|
+
else:
|
109
|
+
# String or other type - create a SwaigFunctionResult with the string representation
|
110
|
+
return SwaigFunctionResult(str(result)).to_dict()
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
# Log the error for debugging but don't expose details to the AI
|
114
|
+
logging.error(f"Error executing SWAIG function {self.name}: {str(e)}")
|
115
|
+
# Return a generic error message
|
116
|
+
return SwaigFunctionResult(
|
117
|
+
"Sorry, I couldn't complete that action. Please try again or contact support if the issue persists."
|
118
|
+
).to_dict()
|
119
|
+
|
120
|
+
def validate_args(self, args: Dict[str, Any]) -> bool:
|
121
|
+
"""
|
122
|
+
Validate the arguments against the parameter schema
|
123
|
+
|
124
|
+
Args:
|
125
|
+
args: Arguments to validate
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
True if valid, False otherwise
|
129
|
+
"""
|
130
|
+
# TODO: Implement JSON Schema validation
|
131
|
+
return True
|
132
|
+
|
133
|
+
def to_swaig(self, base_url: str, token: Optional[str] = None, call_id: Optional[str] = None, include_auth: bool = True) -> Dict[str, Any]:
|
134
|
+
"""
|
135
|
+
Convert this function to a SWAIG-compatible JSON object for SWML
|
136
|
+
|
137
|
+
Args:
|
138
|
+
base_url: Base URL for the webhook
|
139
|
+
token: Optional auth token to include
|
140
|
+
call_id: Optional call ID for session tracking
|
141
|
+
include_auth: Whether to include auth credentials in URL
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
Dictionary representation for the SWAIG array in SWML
|
145
|
+
"""
|
146
|
+
# All functions use a single /swaig endpoint
|
147
|
+
url = f"{base_url}/swaig"
|
148
|
+
|
149
|
+
# Add token and call_id parameters if provided
|
150
|
+
if token and call_id:
|
151
|
+
url = f"{url}?token={token}&call_id={call_id}"
|
152
|
+
|
153
|
+
# Create properly structured function definition
|
154
|
+
function_def = {
|
155
|
+
"function": self.name,
|
156
|
+
"description": self.description,
|
157
|
+
"parameters": self._ensure_parameter_structure(),
|
158
|
+
}
|
159
|
+
|
160
|
+
# Only add web_hook_url if not using defaults
|
161
|
+
# This will be handled by the defaults section in the SWAIG array
|
162
|
+
if url:
|
163
|
+
function_def["web_hook_url"] = url
|
164
|
+
|
165
|
+
# Add fillers if provided
|
166
|
+
if self.fillers and len(self.fillers) > 0:
|
167
|
+
function_def["fillers"] = self.fillers
|
168
|
+
|
169
|
+
return function_def
|
170
|
+
|
171
|
+
# Add an alias for backward compatibility
|
172
|
+
SwaigFunction = SWAIGFunction
|
@@ -0,0 +1,214 @@
|
|
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 Builder - Fluent API for building SWML documents
|
12
|
+
|
13
|
+
This module provides a fluent builder API for creating SWML documents.
|
14
|
+
It allows for chaining method calls to build up a document step by step.
|
15
|
+
"""
|
16
|
+
|
17
|
+
from typing import Dict, List, Any, Optional, Union, Self, TypeVar
|
18
|
+
|
19
|
+
from signalwire_agents.core.swml_service import SWMLService
|
20
|
+
|
21
|
+
|
22
|
+
T = TypeVar('T', bound='SWMLBuilder')
|
23
|
+
|
24
|
+
|
25
|
+
class SWMLBuilder:
|
26
|
+
"""
|
27
|
+
Fluent builder for SWML documents
|
28
|
+
|
29
|
+
This class provides a fluent interface for building SWML documents
|
30
|
+
by chaining method calls. It delegates to an underlying SWMLService
|
31
|
+
instance for the actual document creation.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self, service: SWMLService):
|
35
|
+
"""
|
36
|
+
Initialize with a SWMLService instance
|
37
|
+
|
38
|
+
Args:
|
39
|
+
service: The SWMLService to delegate to
|
40
|
+
"""
|
41
|
+
self.service = service
|
42
|
+
|
43
|
+
def answer(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> Self:
|
44
|
+
"""
|
45
|
+
Add an 'answer' verb to the main section
|
46
|
+
|
47
|
+
Args:
|
48
|
+
max_duration: Maximum duration in seconds
|
49
|
+
codecs: Comma-separated list of codecs
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Self for method chaining
|
53
|
+
"""
|
54
|
+
self.service.add_answer_verb(max_duration, codecs)
|
55
|
+
return self
|
56
|
+
|
57
|
+
def hangup(self, reason: Optional[str] = None) -> Self:
|
58
|
+
"""
|
59
|
+
Add a 'hangup' verb to the main section
|
60
|
+
|
61
|
+
Args:
|
62
|
+
reason: Optional reason for hangup
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Self for method chaining
|
66
|
+
"""
|
67
|
+
self.service.add_hangup_verb(reason)
|
68
|
+
return self
|
69
|
+
|
70
|
+
def ai(self,
|
71
|
+
prompt_text: Optional[str] = None,
|
72
|
+
prompt_pom: Optional[List[Dict[str, Any]]] = None,
|
73
|
+
post_prompt: Optional[str] = None,
|
74
|
+
post_prompt_url: Optional[str] = None,
|
75
|
+
swaig: Optional[Dict[str, Any]] = None,
|
76
|
+
**kwargs) -> Self:
|
77
|
+
"""
|
78
|
+
Add an 'ai' verb to the main section
|
79
|
+
|
80
|
+
Args:
|
81
|
+
prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
|
82
|
+
prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
|
83
|
+
post_prompt: Optional post-prompt text
|
84
|
+
post_prompt_url: Optional URL for post-prompt processing
|
85
|
+
swaig: Optional SWAIG configuration
|
86
|
+
**kwargs: Additional AI parameters
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Self for method chaining
|
90
|
+
"""
|
91
|
+
self.service.add_ai_verb(
|
92
|
+
prompt_text=prompt_text,
|
93
|
+
prompt_pom=prompt_pom,
|
94
|
+
post_prompt=post_prompt,
|
95
|
+
post_prompt_url=post_prompt_url,
|
96
|
+
swaig=swaig,
|
97
|
+
**kwargs
|
98
|
+
)
|
99
|
+
return self
|
100
|
+
|
101
|
+
def play(self, url: Optional[str] = None, urls: Optional[List[str]] = None,
|
102
|
+
volume: Optional[float] = None, say_voice: Optional[str] = None,
|
103
|
+
say_language: Optional[str] = None, say_gender: Optional[str] = None,
|
104
|
+
auto_answer: Optional[bool] = None) -> Self:
|
105
|
+
"""
|
106
|
+
Add a 'play' verb to the main section
|
107
|
+
|
108
|
+
Args:
|
109
|
+
url: Single URL to play (mutually exclusive with urls)
|
110
|
+
urls: List of URLs to play (mutually exclusive with url)
|
111
|
+
volume: Volume level (-40 to 40)
|
112
|
+
say_voice: Voice for text-to-speech
|
113
|
+
say_language: Language for text-to-speech
|
114
|
+
say_gender: Gender for text-to-speech
|
115
|
+
auto_answer: Whether to auto-answer the call
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Self for method chaining
|
119
|
+
"""
|
120
|
+
# Create base config
|
121
|
+
config = {}
|
122
|
+
|
123
|
+
# Add play config (either single URL or list)
|
124
|
+
if url is not None:
|
125
|
+
config["url"] = url
|
126
|
+
elif urls is not None:
|
127
|
+
config["urls"] = urls
|
128
|
+
else:
|
129
|
+
raise ValueError("Either url or urls must be provided")
|
130
|
+
|
131
|
+
# Add optional parameters
|
132
|
+
if volume is not None:
|
133
|
+
config["volume"] = volume
|
134
|
+
if say_voice is not None:
|
135
|
+
config["say_voice"] = say_voice
|
136
|
+
if say_language is not None:
|
137
|
+
config["say_language"] = say_language
|
138
|
+
if say_gender is not None:
|
139
|
+
config["say_gender"] = say_gender
|
140
|
+
if auto_answer is not None:
|
141
|
+
config["auto_answer"] = auto_answer
|
142
|
+
|
143
|
+
# Add the verb
|
144
|
+
self.service.add_verb("play", config)
|
145
|
+
return self
|
146
|
+
|
147
|
+
def say(self, text: str, voice: Optional[str] = None,
|
148
|
+
language: Optional[str] = None, gender: Optional[str] = None,
|
149
|
+
volume: Optional[float] = None) -> Self:
|
150
|
+
"""
|
151
|
+
Add a 'play' verb with say: prefix for text-to-speech
|
152
|
+
|
153
|
+
Args:
|
154
|
+
text: Text to speak
|
155
|
+
voice: Voice for text-to-speech
|
156
|
+
language: Language for text-to-speech
|
157
|
+
gender: Gender for text-to-speech
|
158
|
+
volume: Volume level (-40 to 40)
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Self for method chaining
|
162
|
+
"""
|
163
|
+
# Create play config with say: prefix
|
164
|
+
url = f"say:{text}"
|
165
|
+
|
166
|
+
# Add the verb
|
167
|
+
return self.play(
|
168
|
+
url=url,
|
169
|
+
say_voice=voice,
|
170
|
+
say_language=language,
|
171
|
+
say_gender=gender,
|
172
|
+
volume=volume
|
173
|
+
)
|
174
|
+
|
175
|
+
def add_section(self, section_name: str) -> Self:
|
176
|
+
"""
|
177
|
+
Add a new section to the document
|
178
|
+
|
179
|
+
Args:
|
180
|
+
section_name: Name of the section to add
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Self for method chaining
|
184
|
+
"""
|
185
|
+
self.service.add_section(section_name)
|
186
|
+
return self
|
187
|
+
|
188
|
+
def build(self) -> Dict[str, Any]:
|
189
|
+
"""
|
190
|
+
Build and return the SWML document
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
The complete SWML document as a dictionary
|
194
|
+
"""
|
195
|
+
return self.service.get_document()
|
196
|
+
|
197
|
+
def render(self) -> str:
|
198
|
+
"""
|
199
|
+
Build and render the SWML document as a JSON string
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
The complete SWML document as a JSON string
|
203
|
+
"""
|
204
|
+
return self.service.render_document()
|
205
|
+
|
206
|
+
def reset(self) -> Self:
|
207
|
+
"""
|
208
|
+
Reset the document to an empty state
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
Self for method chaining
|
212
|
+
"""
|
213
|
+
self.service.reset_document()
|
214
|
+
return self
|