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,123 @@
|
|
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
|
+
SwaigFunctionResult class for handling the response format of SWAIG function calls
|
12
|
+
"""
|
13
|
+
|
14
|
+
from typing import Dict, List, Any, Optional, Union
|
15
|
+
|
16
|
+
|
17
|
+
class SwaigFunctionResult:
|
18
|
+
"""
|
19
|
+
Wrapper around SWAIG function responses that handles proper formatting
|
20
|
+
of response text and actions.
|
21
|
+
|
22
|
+
Example:
|
23
|
+
return SwaigFunctionResult("Found your order")
|
24
|
+
|
25
|
+
# With actions
|
26
|
+
return (
|
27
|
+
SwaigFunctionResult("I'll transfer you to support")
|
28
|
+
.add_action("transfer", {"dest": "support"})
|
29
|
+
)
|
30
|
+
|
31
|
+
# With simple action value
|
32
|
+
return (
|
33
|
+
SwaigFunctionResult("I'll confirm that")
|
34
|
+
.add_action("confirm", True)
|
35
|
+
)
|
36
|
+
|
37
|
+
# With multiple actions
|
38
|
+
return (
|
39
|
+
SwaigFunctionResult("Processing your request")
|
40
|
+
.add_actions([
|
41
|
+
{"set_global_data": {"key": "value"}},
|
42
|
+
{"play": {"url": "music.mp3"}}
|
43
|
+
])
|
44
|
+
)
|
45
|
+
"""
|
46
|
+
def __init__(self, response: Optional[str] = None):
|
47
|
+
"""
|
48
|
+
Initialize a new SWAIG function result
|
49
|
+
|
50
|
+
Args:
|
51
|
+
response: Optional natural language response to include
|
52
|
+
"""
|
53
|
+
self.response = response or ""
|
54
|
+
self.action: List[Dict[str, Any]] = []
|
55
|
+
|
56
|
+
def set_response(self, response: str) -> 'SwaigFunctionResult':
|
57
|
+
"""
|
58
|
+
Set the natural language response text
|
59
|
+
|
60
|
+
Args:
|
61
|
+
response: The text the AI should say
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Self for method chaining
|
65
|
+
"""
|
66
|
+
self.response = response
|
67
|
+
return self
|
68
|
+
|
69
|
+
def add_action(self, name: str, data: Any) -> 'SwaigFunctionResult':
|
70
|
+
"""
|
71
|
+
Add a structured action to the response
|
72
|
+
|
73
|
+
Args:
|
74
|
+
name: The name/type of the action (e.g., "play", "transfer")
|
75
|
+
data: The data for the action - can be a string, boolean, object, or array
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Self for method chaining
|
79
|
+
"""
|
80
|
+
self.action.append({name: data})
|
81
|
+
return self
|
82
|
+
|
83
|
+
def add_actions(self, actions: List[Dict[str, Any]]) -> 'SwaigFunctionResult':
|
84
|
+
"""
|
85
|
+
Add multiple structured actions to the response
|
86
|
+
|
87
|
+
Args:
|
88
|
+
actions: List of action objects to add to the response
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Self for method chaining
|
92
|
+
"""
|
93
|
+
self.action.extend(actions)
|
94
|
+
return self
|
95
|
+
|
96
|
+
def to_dict(self) -> Dict[str, Any]:
|
97
|
+
"""
|
98
|
+
Convert to the JSON structure expected by SWAIG
|
99
|
+
|
100
|
+
The result must have at least one of:
|
101
|
+
- 'response': Text to be spoken by the AI
|
102
|
+
- 'action': Array of action objects
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Dictionary in SWAIG function response format
|
106
|
+
"""
|
107
|
+
# Create the result object
|
108
|
+
result = {}
|
109
|
+
|
110
|
+
# Add response if present
|
111
|
+
if self.response:
|
112
|
+
result["response"] = self.response
|
113
|
+
|
114
|
+
# Add action if present
|
115
|
+
if self.action:
|
116
|
+
result["action"] = self.action
|
117
|
+
|
118
|
+
# Ensure we have at least one of response or action
|
119
|
+
if not result:
|
120
|
+
# Default response if neither is present
|
121
|
+
result["response"] = "Action completed."
|
122
|
+
|
123
|
+
return result
|
@@ -0,0 +1,204 @@
|
|
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
|
+
PomBuilder for creating structured POM prompts for SignalWire AI Agents
|
12
|
+
"""
|
13
|
+
|
14
|
+
try:
|
15
|
+
from signalwire_pom.pom import PromptObjectModel, Section
|
16
|
+
except ImportError:
|
17
|
+
raise ImportError(
|
18
|
+
"signalwire-pom package is required. "
|
19
|
+
"Install it with: pip install signalwire-pom"
|
20
|
+
)
|
21
|
+
|
22
|
+
from typing import List, Dict, Any, Optional, Union
|
23
|
+
|
24
|
+
|
25
|
+
class PomBuilder:
|
26
|
+
"""
|
27
|
+
Builder class for creating structured prompts using the Prompt Object Model.
|
28
|
+
|
29
|
+
This class is a flexible wrapper around the POM API that allows for:
|
30
|
+
- Dynamic creation of sections on demand
|
31
|
+
- Adding content to existing sections
|
32
|
+
- Nesting subsections
|
33
|
+
- Rendering to markdown or XML
|
34
|
+
|
35
|
+
Unlike previous implementations, there are no predefined section types -
|
36
|
+
you can create any section structure that fits your needs.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self):
|
40
|
+
"""Initialize a new POM builder with an empty POM"""
|
41
|
+
self.pom = PromptObjectModel()
|
42
|
+
self._sections: Dict[str, Section] = {}
|
43
|
+
|
44
|
+
def add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None,
|
45
|
+
numbered: bool = False, numbered_bullets: bool = False,
|
46
|
+
subsections: Optional[List[Dict[str, Any]]] = None) -> 'PomBuilder':
|
47
|
+
"""
|
48
|
+
Add a new section to the POM
|
49
|
+
|
50
|
+
Args:
|
51
|
+
title: Section title
|
52
|
+
body: Optional body text
|
53
|
+
bullets: Optional list of bullet points
|
54
|
+
numbered: Whether to number this section
|
55
|
+
numbered_bullets: Whether to number bullet points
|
56
|
+
subsections: Optional list of subsection objects
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Self for method chaining
|
60
|
+
"""
|
61
|
+
section = self.pom.add_section(
|
62
|
+
title=title,
|
63
|
+
body=body,
|
64
|
+
bullets=bullets or [],
|
65
|
+
numbered=numbered,
|
66
|
+
numberedBullets=numbered_bullets
|
67
|
+
)
|
68
|
+
self._sections[title] = section
|
69
|
+
|
70
|
+
# Process subsections if provided
|
71
|
+
if subsections:
|
72
|
+
for subsection_data in subsections:
|
73
|
+
if 'title' in subsection_data:
|
74
|
+
subsection_title = subsection_data['title']
|
75
|
+
subsection_body = subsection_data.get('body', '')
|
76
|
+
subsection_bullets = subsection_data.get('bullets', [])
|
77
|
+
|
78
|
+
section.add_subsection(
|
79
|
+
title=subsection_title,
|
80
|
+
body=subsection_body,
|
81
|
+
bullets=subsection_bullets or []
|
82
|
+
)
|
83
|
+
|
84
|
+
return self
|
85
|
+
|
86
|
+
def add_to_section(self, title: str, body: Optional[str] = None, bullet: Optional[str] = None, bullets: Optional[List[str]] = None) -> 'PomBuilder':
|
87
|
+
"""
|
88
|
+
Add content to an existing section
|
89
|
+
|
90
|
+
Args:
|
91
|
+
title: Section title
|
92
|
+
body: Text to append to the section body
|
93
|
+
bullet: Single bullet to add
|
94
|
+
bullets: List of bullets to add
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Self for method chaining
|
98
|
+
"""
|
99
|
+
# Create section if it doesn't exist - auto-vivification
|
100
|
+
if title not in self._sections:
|
101
|
+
self.add_section(title)
|
102
|
+
|
103
|
+
section = self._sections[title]
|
104
|
+
|
105
|
+
# Add body text if provided
|
106
|
+
if body:
|
107
|
+
if hasattr(section, 'body') and section.body:
|
108
|
+
section.body = f"{section.body}\n\n{body}"
|
109
|
+
else:
|
110
|
+
section.body = body
|
111
|
+
|
112
|
+
# Process bullets
|
113
|
+
if bullet:
|
114
|
+
section.bullets.append(bullet)
|
115
|
+
|
116
|
+
if bullets:
|
117
|
+
section.bullets.extend(bullets)
|
118
|
+
|
119
|
+
return self
|
120
|
+
|
121
|
+
def add_subsection(self, parent_title: str, title: str, body: str = "",
|
122
|
+
bullets: Optional[List[str]] = None) -> 'PomBuilder':
|
123
|
+
"""
|
124
|
+
Add a subsection to an existing section, creating the parent if needed
|
125
|
+
|
126
|
+
Args:
|
127
|
+
parent_title: Title of the parent section
|
128
|
+
title: Title for the new subsection
|
129
|
+
body: Optional body text
|
130
|
+
bullets: Optional list of bullet points
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Self for method chaining
|
134
|
+
"""
|
135
|
+
# Create parent section if it doesn't exist - auto-vivification
|
136
|
+
if parent_title not in self._sections:
|
137
|
+
self.add_section(parent_title)
|
138
|
+
|
139
|
+
parent = self._sections[parent_title]
|
140
|
+
subsection = parent.add_subsection(
|
141
|
+
title=title,
|
142
|
+
body=body,
|
143
|
+
bullets=bullets or []
|
144
|
+
)
|
145
|
+
return self
|
146
|
+
|
147
|
+
def has_section(self, title: str) -> bool:
|
148
|
+
"""
|
149
|
+
Check if a section with the given title exists
|
150
|
+
|
151
|
+
Args:
|
152
|
+
title: Section title to check
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
True if the section exists, False otherwise
|
156
|
+
"""
|
157
|
+
return title in self._sections
|
158
|
+
|
159
|
+
def get_section(self, title: str) -> Optional[Section]:
|
160
|
+
"""
|
161
|
+
Get a section by title
|
162
|
+
|
163
|
+
Args:
|
164
|
+
title: Section title
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Section object or None if not found
|
168
|
+
"""
|
169
|
+
return self._sections.get(title)
|
170
|
+
|
171
|
+
def render_markdown(self) -> str:
|
172
|
+
"""Render the POM as markdown"""
|
173
|
+
return self.pom.render_markdown()
|
174
|
+
|
175
|
+
def render_xml(self) -> str:
|
176
|
+
"""Render the POM as XML"""
|
177
|
+
return self.pom.render_xml()
|
178
|
+
|
179
|
+
def to_dict(self) -> List[Dict[str, Any]]:
|
180
|
+
"""Convert the POM to a list of section dictionaries"""
|
181
|
+
return self.pom.to_dict()
|
182
|
+
|
183
|
+
def to_json(self) -> str:
|
184
|
+
"""Convert the POM to a JSON string"""
|
185
|
+
return self.pom.to_json()
|
186
|
+
|
187
|
+
@classmethod
|
188
|
+
def from_sections(cls, sections: List[Dict[str, Any]]) -> 'PomBuilder':
|
189
|
+
"""
|
190
|
+
Create a PomBuilder from a list of section dictionaries
|
191
|
+
|
192
|
+
Args:
|
193
|
+
sections: List of section definition dictionaries
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
A new PomBuilder instance with the sections added
|
197
|
+
"""
|
198
|
+
builder = cls()
|
199
|
+
builder.pom = PromptObjectModel.from_json(sections)
|
200
|
+
# Rebuild the sections dict
|
201
|
+
for section in builder.pom.sections:
|
202
|
+
if section.title:
|
203
|
+
builder._sections[section.title] = section
|
204
|
+
return builder
|
@@ -0,0 +1,179 @@
|
|
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
|
+
Session manager for handling call sessions and security tokens
|
12
|
+
"""
|
13
|
+
|
14
|
+
from typing import Dict, Any, Optional, Tuple
|
15
|
+
import secrets
|
16
|
+
import time
|
17
|
+
from datetime import datetime
|
18
|
+
|
19
|
+
|
20
|
+
class CallSession:
|
21
|
+
"""
|
22
|
+
Represents a single call session with associated tokens and state
|
23
|
+
"""
|
24
|
+
def __init__(self, call_id: str):
|
25
|
+
self.call_id = call_id
|
26
|
+
self.tokens: Dict[str, str] = {} # function_name -> token
|
27
|
+
self.state = "pending" # pending, active, expired
|
28
|
+
self.started_at = datetime.now()
|
29
|
+
self.metadata: Dict[str, Any] = {} # Custom state for the call
|
30
|
+
|
31
|
+
|
32
|
+
class SessionManager:
|
33
|
+
"""
|
34
|
+
Manages call sessions and their associated security tokens
|
35
|
+
"""
|
36
|
+
def __init__(self, token_expiry_secs: int = 600):
|
37
|
+
"""
|
38
|
+
Initialize the session manager
|
39
|
+
|
40
|
+
Args:
|
41
|
+
token_expiry_secs: Seconds until tokens expire (default: 10 minutes)
|
42
|
+
"""
|
43
|
+
self._active_calls: Dict[str, CallSession] = {}
|
44
|
+
self.token_expiry_secs = token_expiry_secs
|
45
|
+
|
46
|
+
def create_session(self, call_id: Optional[str] = None) -> str:
|
47
|
+
"""
|
48
|
+
Create a new call session
|
49
|
+
|
50
|
+
Args:
|
51
|
+
call_id: Optional call ID, generated if not provided
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
The call_id for the new session
|
55
|
+
"""
|
56
|
+
# Generate call_id if not provided
|
57
|
+
if not call_id:
|
58
|
+
call_id = secrets.token_urlsafe(16)
|
59
|
+
|
60
|
+
# Create new session
|
61
|
+
self._active_calls[call_id] = CallSession(call_id)
|
62
|
+
return call_id
|
63
|
+
|
64
|
+
def generate_token(self, function_name: str, call_id: str) -> str:
|
65
|
+
"""
|
66
|
+
Generate a secure token for a function call
|
67
|
+
|
68
|
+
Args:
|
69
|
+
function_name: Name of the function to generate a token for
|
70
|
+
call_id: Call session ID
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
A secure random token
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
ValueError: If the call session does not exist
|
77
|
+
"""
|
78
|
+
if call_id not in self._active_calls:
|
79
|
+
raise ValueError(f"No active session for call_id: {call_id}")
|
80
|
+
|
81
|
+
token = secrets.token_urlsafe(24)
|
82
|
+
self._active_calls[call_id].tokens[function_name] = token
|
83
|
+
return token
|
84
|
+
|
85
|
+
def validate_token(self, call_id: str, function_name: str, token: str) -> bool:
|
86
|
+
"""
|
87
|
+
Validate a function call token
|
88
|
+
|
89
|
+
Args:
|
90
|
+
call_id: Call session ID
|
91
|
+
function_name: Name of the function being called
|
92
|
+
token: Token to validate
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
True if valid, False otherwise
|
96
|
+
"""
|
97
|
+
session = self._active_calls.get(call_id)
|
98
|
+
if not session or session.state != "active":
|
99
|
+
return False
|
100
|
+
|
101
|
+
# Check if token matches and is not expired
|
102
|
+
expected_token = session.tokens.get(function_name)
|
103
|
+
if not expected_token or expected_token != token:
|
104
|
+
return False
|
105
|
+
|
106
|
+
# Check expiry
|
107
|
+
now = datetime.now()
|
108
|
+
seconds_elapsed = (now - session.started_at).total_seconds()
|
109
|
+
if seconds_elapsed > self.token_expiry_secs:
|
110
|
+
session.state = "expired"
|
111
|
+
return False
|
112
|
+
|
113
|
+
return True
|
114
|
+
|
115
|
+
def activate_session(self, call_id: str) -> bool:
|
116
|
+
"""
|
117
|
+
Activate a call session (called by startup_hook)
|
118
|
+
|
119
|
+
Args:
|
120
|
+
call_id: Call session ID
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
True if successful, False otherwise
|
124
|
+
"""
|
125
|
+
session = self._active_calls.get(call_id)
|
126
|
+
if not session:
|
127
|
+
return False
|
128
|
+
|
129
|
+
session.state = "active"
|
130
|
+
return True
|
131
|
+
|
132
|
+
def end_session(self, call_id: str) -> bool:
|
133
|
+
"""
|
134
|
+
End a call session (called by hangup_hook)
|
135
|
+
|
136
|
+
Args:
|
137
|
+
call_id: Call session ID
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
True if successful, False otherwise
|
141
|
+
"""
|
142
|
+
if call_id in self._active_calls:
|
143
|
+
del self._active_calls[call_id]
|
144
|
+
return True
|
145
|
+
return False
|
146
|
+
|
147
|
+
def get_session_metadata(self, call_id: str) -> Optional[Dict[str, Any]]:
|
148
|
+
"""
|
149
|
+
Get custom metadata for a call session
|
150
|
+
|
151
|
+
Args:
|
152
|
+
call_id: Call session ID
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
Metadata dict or None if session not found
|
156
|
+
"""
|
157
|
+
session = self._active_calls.get(call_id)
|
158
|
+
if not session:
|
159
|
+
return None
|
160
|
+
return session.metadata
|
161
|
+
|
162
|
+
def set_session_metadata(self, call_id: str, key: str, value: Any) -> bool:
|
163
|
+
"""
|
164
|
+
Set custom metadata for a call session
|
165
|
+
|
166
|
+
Args:
|
167
|
+
call_id: Call session ID
|
168
|
+
key: Metadata key
|
169
|
+
value: Metadata value
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
True if successful, False otherwise
|
173
|
+
"""
|
174
|
+
session = self._active_calls.get(call_id)
|
175
|
+
if not session:
|
176
|
+
return False
|
177
|
+
|
178
|
+
session.metadata[key] = value
|
179
|
+
return True
|
@@ -0,0 +1,17 @@
|
|
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
|
+
State management for SignalWire AI Agents
|
12
|
+
"""
|
13
|
+
|
14
|
+
from .state_manager import StateManager
|
15
|
+
from .file_state_manager import FileStateManager
|
16
|
+
|
17
|
+
__all__ = ['StateManager', 'FileStateManager']
|