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,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,9 @@
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
+
@@ -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']