signalwire-agents 0.1.0__py3-none-any.whl → 0.1.1__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 (33) hide show
  1. signalwire_agents/__init__.py +10 -1
  2. signalwire_agents/agent_server.py +73 -44
  3. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.1.dist-info}/METADATA +75 -30
  4. signalwire_agents-0.1.1.dist-info/RECORD +9 -0
  5. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.1.dist-info}/WHEEL +1 -1
  6. signalwire_agents-0.1.1.dist-info/licenses/LICENSE +21 -0
  7. signalwire_agents/core/__init__.py +0 -20
  8. signalwire_agents/core/agent_base.py +0 -2449
  9. signalwire_agents/core/function_result.py +0 -104
  10. signalwire_agents/core/pom_builder.py +0 -195
  11. signalwire_agents/core/security/__init__.py +0 -0
  12. signalwire_agents/core/security/session_manager.py +0 -170
  13. signalwire_agents/core/state/__init__.py +0 -8
  14. signalwire_agents/core/state/file_state_manager.py +0 -210
  15. signalwire_agents/core/state/state_manager.py +0 -92
  16. signalwire_agents/core/swaig_function.py +0 -163
  17. signalwire_agents/core/swml_builder.py +0 -205
  18. signalwire_agents/core/swml_handler.py +0 -218
  19. signalwire_agents/core/swml_renderer.py +0 -359
  20. signalwire_agents/core/swml_service.py +0 -1009
  21. signalwire_agents/prefabs/__init__.py +0 -15
  22. signalwire_agents/prefabs/concierge.py +0 -276
  23. signalwire_agents/prefabs/faq_bot.py +0 -314
  24. signalwire_agents/prefabs/info_gatherer.py +0 -253
  25. signalwire_agents/prefabs/survey.py +0 -387
  26. signalwire_agents/utils/__init__.py +0 -0
  27. signalwire_agents/utils/pom_utils.py +0 -0
  28. signalwire_agents/utils/schema_utils.py +0 -348
  29. signalwire_agents/utils/token_generators.py +0 -0
  30. signalwire_agents/utils/validators.py +0 -0
  31. signalwire_agents-0.1.0.dist-info/RECORD +0 -32
  32. {signalwire_agents-0.1.0.data → signalwire_agents-0.1.1.data}/data/schema.json +0 -0
  33. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.1.dist-info}/top_level.txt +0 -0
@@ -1,104 +0,0 @@
1
- """
2
- SwaigFunctionResult class for handling the response format of SWAIG function calls
3
- """
4
-
5
- from typing import Dict, List, Any, Optional, Union
6
-
7
-
8
- class SwaigActionTypes:
9
- """Constants for standard SWAIG action types"""
10
- PLAY = "play"
11
- TRANSFER = "transfer"
12
- SEND_SMS = "send_sms"
13
- JOIN_ROOM = "join_room"
14
- RETURN = "return"
15
- HANG_UP = "hang_up"
16
- RECORD = "record"
17
- COLLECT = "collect"
18
-
19
-
20
- class SwaigFunctionResult:
21
- """
22
- Wrapper around SWAIG function responses that handles proper formatting
23
- of response text and actions.
24
-
25
- Example:
26
- return SwaigFunctionResult("Found your order")
27
-
28
- # With actions
29
- return (
30
- SwaigFunctionResult("I'll transfer you to support")
31
- .add_action("transfer", {"dest": "support"})
32
- )
33
-
34
- # With simple action value
35
- return (
36
- SwaigFunctionResult("I'll confirm that")
37
- .add_action("confirm", True)
38
- )
39
- """
40
- def __init__(self, response: Optional[str] = None):
41
- """
42
- Initialize a new SWAIG function result
43
-
44
- Args:
45
- response: Optional natural language response to include
46
- """
47
- self.response = response or ""
48
- self.action: List[Dict[str, Any]] = []
49
-
50
- def set_response(self, response: str) -> 'SwaigFunctionResult':
51
- """
52
- Set the natural language response text
53
-
54
- Args:
55
- response: The text the AI should say
56
-
57
- Returns:
58
- Self for method chaining
59
- """
60
- self.response = response
61
- return self
62
-
63
- def add_action(self, name: str, data: Any) -> 'SwaigFunctionResult':
64
- """
65
- Add a structured action to the response
66
-
67
- Args:
68
- name: The name/type of the action (e.g., "play", "transfer")
69
- data: The data for the action - can be a string, boolean, object, or array
70
-
71
- Returns:
72
- Self for method chaining
73
- """
74
- self.action.append({name: data})
75
- return self
76
-
77
- def to_dict(self) -> Dict[str, Any]:
78
- """
79
- Convert to the JSON structure expected by SWAIG
80
-
81
- The result must have at least one of:
82
- - 'response': Text to be spoken by the AI
83
- - 'action': Array of action objects
84
-
85
- Returns:
86
- Dictionary in SWAIG function response format
87
- """
88
- # Create the result object
89
- result = {}
90
-
91
- # Add response if present
92
- if self.response:
93
- result["response"] = self.response
94
-
95
- # Add action if present
96
- if self.action:
97
- result["action"] = self.action
98
-
99
- # Ensure we have at least one of response or action
100
- if not result:
101
- # Default response if neither is present
102
- result["response"] = "Action completed."
103
-
104
- return result
@@ -1,195 +0,0 @@
1
- """
2
- PomBuilder for creating structured POM prompts for SignalWire AI Agents
3
- """
4
-
5
- try:
6
- from signalwire_pom.pom import PromptObjectModel, Section
7
- except ImportError:
8
- raise ImportError(
9
- "signalwire-pom package is required. "
10
- "Install it with: pip install signalwire-pom"
11
- )
12
-
13
- from typing import List, Dict, Any, Optional, Union
14
-
15
-
16
- class PomBuilder:
17
- """
18
- Builder class for creating structured prompts using the Prompt Object Model.
19
-
20
- This class is a flexible wrapper around the POM API that allows for:
21
- - Dynamic creation of sections on demand
22
- - Adding content to existing sections
23
- - Nesting subsections
24
- - Rendering to markdown or XML
25
-
26
- Unlike previous implementations, there are no predefined section types -
27
- you can create any section structure that fits your needs.
28
- """
29
-
30
- def __init__(self):
31
- """Initialize a new POM builder with an empty POM"""
32
- self.pom = PromptObjectModel()
33
- self._sections: Dict[str, Section] = {}
34
-
35
- def add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None,
36
- numbered: bool = False, numbered_bullets: bool = False,
37
- subsections: Optional[List[Dict[str, Any]]] = None) -> 'PomBuilder':
38
- """
39
- Add a new section to the POM
40
-
41
- Args:
42
- title: Section title
43
- body: Optional body text
44
- bullets: Optional list of bullet points
45
- numbered: Whether to number this section
46
- numbered_bullets: Whether to number bullet points
47
- subsections: Optional list of subsection objects
48
-
49
- Returns:
50
- Self for method chaining
51
- """
52
- section = self.pom.add_section(
53
- title=title,
54
- body=body,
55
- bullets=bullets or [],
56
- numbered=numbered,
57
- numberedBullets=numbered_bullets
58
- )
59
- self._sections[title] = section
60
-
61
- # Process subsections if provided
62
- if subsections:
63
- for subsection_data in subsections:
64
- if 'title' in subsection_data:
65
- subsection_title = subsection_data['title']
66
- subsection_body = subsection_data.get('body', '')
67
- subsection_bullets = subsection_data.get('bullets', [])
68
-
69
- section.add_subsection(
70
- title=subsection_title,
71
- body=subsection_body,
72
- bullets=subsection_bullets or []
73
- )
74
-
75
- return self
76
-
77
- def add_to_section(self, title: str, body: Optional[str] = None, bullet: Optional[str] = None, bullets: Optional[List[str]] = None) -> 'PomBuilder':
78
- """
79
- Add content to an existing section
80
-
81
- Args:
82
- title: Section title
83
- body: Text to append to the section body
84
- bullet: Single bullet to add
85
- bullets: List of bullets to add
86
-
87
- Returns:
88
- Self for method chaining
89
- """
90
- # Create section if it doesn't exist - auto-vivification
91
- if title not in self._sections:
92
- self.add_section(title)
93
-
94
- section = self._sections[title]
95
-
96
- # Add body text if provided
97
- if body:
98
- if hasattr(section, 'body') and section.body:
99
- section.body = f"{section.body}\n\n{body}"
100
- else:
101
- section.body = body
102
-
103
- # Process bullets
104
- if bullet:
105
- section.bullets.append(bullet)
106
-
107
- if bullets:
108
- section.bullets.extend(bullets)
109
-
110
- return self
111
-
112
- def add_subsection(self, parent_title: str, title: str, body: str = "",
113
- bullets: Optional[List[str]] = None) -> 'PomBuilder':
114
- """
115
- Add a subsection to an existing section, creating the parent if needed
116
-
117
- Args:
118
- parent_title: Title of the parent section
119
- title: Title for the new subsection
120
- body: Optional body text
121
- bullets: Optional list of bullet points
122
-
123
- Returns:
124
- Self for method chaining
125
- """
126
- # Create parent section if it doesn't exist - auto-vivification
127
- if parent_title not in self._sections:
128
- self.add_section(parent_title)
129
-
130
- parent = self._sections[parent_title]
131
- subsection = parent.add_subsection(
132
- title=title,
133
- body=body,
134
- bullets=bullets or []
135
- )
136
- return self
137
-
138
- def has_section(self, title: str) -> bool:
139
- """
140
- Check if a section with the given title exists
141
-
142
- Args:
143
- title: Section title to check
144
-
145
- Returns:
146
- True if the section exists, False otherwise
147
- """
148
- return title in self._sections
149
-
150
- def get_section(self, title: str) -> Optional[Section]:
151
- """
152
- Get a section by title
153
-
154
- Args:
155
- title: Section title
156
-
157
- Returns:
158
- Section object or None if not found
159
- """
160
- return self._sections.get(title)
161
-
162
- def render_markdown(self) -> str:
163
- """Render the POM as markdown"""
164
- return self.pom.render_markdown()
165
-
166
- def render_xml(self) -> str:
167
- """Render the POM as XML"""
168
- return self.pom.render_xml()
169
-
170
- def to_dict(self) -> List[Dict[str, Any]]:
171
- """Convert the POM to a list of section dictionaries"""
172
- return self.pom.to_dict()
173
-
174
- def to_json(self) -> str:
175
- """Convert the POM to a JSON string"""
176
- return self.pom.to_json()
177
-
178
- @classmethod
179
- def from_sections(cls, sections: List[Dict[str, Any]]) -> 'PomBuilder':
180
- """
181
- Create a PomBuilder from a list of section dictionaries
182
-
183
- Args:
184
- sections: List of section definition dictionaries
185
-
186
- Returns:
187
- A new PomBuilder instance with the sections added
188
- """
189
- builder = cls()
190
- builder.pom = PromptObjectModel.from_json(sections)
191
- # Rebuild the sections dict
192
- for section in builder.pom.sections:
193
- if section.title:
194
- builder._sections[section.title] = section
195
- return builder
File without changes
@@ -1,170 +0,0 @@
1
- """
2
- Session manager for handling call sessions and security tokens
3
- """
4
-
5
- from typing import Dict, Any, Optional, Tuple
6
- import secrets
7
- import time
8
- from datetime import datetime
9
-
10
-
11
- class CallSession:
12
- """
13
- Represents a single call session with associated tokens and state
14
- """
15
- def __init__(self, call_id: str):
16
- self.call_id = call_id
17
- self.tokens: Dict[str, str] = {} # function_name -> token
18
- self.state = "pending" # pending, active, expired
19
- self.started_at = datetime.now()
20
- self.metadata: Dict[str, Any] = {} # Custom state for the call
21
-
22
-
23
- class SessionManager:
24
- """
25
- Manages call sessions and their associated security tokens
26
- """
27
- def __init__(self, token_expiry_secs: int = 600):
28
- """
29
- Initialize the session manager
30
-
31
- Args:
32
- token_expiry_secs: Seconds until tokens expire (default: 10 minutes)
33
- """
34
- self._active_calls: Dict[str, CallSession] = {}
35
- self.token_expiry_secs = token_expiry_secs
36
-
37
- def create_session(self, call_id: Optional[str] = None) -> str:
38
- """
39
- Create a new call session
40
-
41
- Args:
42
- call_id: Optional call ID, generated if not provided
43
-
44
- Returns:
45
- The call_id for the new session
46
- """
47
- # Generate call_id if not provided
48
- if not call_id:
49
- call_id = secrets.token_urlsafe(16)
50
-
51
- # Create new session
52
- self._active_calls[call_id] = CallSession(call_id)
53
- return call_id
54
-
55
- def generate_token(self, function_name: str, call_id: str) -> str:
56
- """
57
- Generate a secure token for a function call
58
-
59
- Args:
60
- function_name: Name of the function to generate a token for
61
- call_id: Call session ID
62
-
63
- Returns:
64
- A secure random token
65
-
66
- Raises:
67
- ValueError: If the call session does not exist
68
- """
69
- if call_id not in self._active_calls:
70
- raise ValueError(f"No active session for call_id: {call_id}")
71
-
72
- token = secrets.token_urlsafe(24)
73
- self._active_calls[call_id].tokens[function_name] = token
74
- return token
75
-
76
- def validate_token(self, call_id: str, function_name: str, token: str) -> bool:
77
- """
78
- Validate a function call token
79
-
80
- Args:
81
- call_id: Call session ID
82
- function_name: Name of the function being called
83
- token: Token to validate
84
-
85
- Returns:
86
- True if valid, False otherwise
87
- """
88
- session = self._active_calls.get(call_id)
89
- if not session or session.state != "active":
90
- return False
91
-
92
- # Check if token matches and is not expired
93
- expected_token = session.tokens.get(function_name)
94
- if not expected_token or expected_token != token:
95
- return False
96
-
97
- # Check expiry
98
- now = datetime.now()
99
- seconds_elapsed = (now - session.started_at).total_seconds()
100
- if seconds_elapsed > self.token_expiry_secs:
101
- session.state = "expired"
102
- return False
103
-
104
- return True
105
-
106
- def activate_session(self, call_id: str) -> bool:
107
- """
108
- Activate a call session (called by startup_hook)
109
-
110
- Args:
111
- call_id: Call session ID
112
-
113
- Returns:
114
- True if successful, False otherwise
115
- """
116
- session = self._active_calls.get(call_id)
117
- if not session:
118
- return False
119
-
120
- session.state = "active"
121
- return True
122
-
123
- def end_session(self, call_id: str) -> bool:
124
- """
125
- End a call session (called by hangup_hook)
126
-
127
- Args:
128
- call_id: Call session ID
129
-
130
- Returns:
131
- True if successful, False otherwise
132
- """
133
- if call_id in self._active_calls:
134
- del self._active_calls[call_id]
135
- return True
136
- return False
137
-
138
- def get_session_metadata(self, call_id: str) -> Optional[Dict[str, Any]]:
139
- """
140
- Get custom metadata for a call session
141
-
142
- Args:
143
- call_id: Call session ID
144
-
145
- Returns:
146
- Metadata dict or None if session not found
147
- """
148
- session = self._active_calls.get(call_id)
149
- if not session:
150
- return None
151
- return session.metadata
152
-
153
- def set_session_metadata(self, call_id: str, key: str, value: Any) -> bool:
154
- """
155
- Set custom metadata for a call session
156
-
157
- Args:
158
- call_id: Call session ID
159
- key: Metadata key
160
- value: Metadata value
161
-
162
- Returns:
163
- True if successful, False otherwise
164
- """
165
- session = self._active_calls.get(call_id)
166
- if not session:
167
- return False
168
-
169
- session.metadata[key] = value
170
- return True
@@ -1,8 +0,0 @@
1
- """
2
- State management for SignalWire AI Agents
3
- """
4
-
5
- from .state_manager import StateManager
6
- from .file_state_manager import FileStateManager
7
-
8
- __all__ = ['StateManager', 'FileStateManager']
@@ -1,210 +0,0 @@
1
- """
2
- File-based implementation of the StateManager interface
3
- """
4
-
5
- import os
6
- import json
7
- import tempfile
8
- import time
9
- from datetime import datetime, timedelta
10
- from typing import Dict, Any, Optional, List
11
-
12
- from .state_manager import StateManager
13
-
14
-
15
- class FileStateManager(StateManager):
16
- """
17
- File-based state manager implementation
18
-
19
- This implementation stores state data as JSON files in a directory.
20
- Each call's state is stored in a separate file named by call_id.
21
- Files older than expiry_days are automatically cleaned up.
22
-
23
- This is suitable for development and low-volume deployments.
24
- For production, consider using database or Redis implementations.
25
- """
26
-
27
- def __init__(
28
- self,
29
- storage_dir: Optional[str] = None,
30
- expiry_days: float = 1.0
31
- ):
32
- """
33
- Initialize the file state manager
34
-
35
- Args:
36
- storage_dir: Directory to store state files (default: system temp dir)
37
- expiry_days: Days after which state files are considered expired
38
- """
39
- self.storage_dir = storage_dir or os.path.join(tempfile.gettempdir(), "signalwire_state")
40
- self.expiry_days = expiry_days
41
-
42
- # Create storage directory if it doesn't exist
43
- if not os.path.exists(self.storage_dir):
44
- os.makedirs(self.storage_dir)
45
-
46
- def _get_file_path(self, call_id: str) -> str:
47
- """Get the file path for a call_id"""
48
- # Sanitize call_id to ensure it's safe for a filename
49
- sanitized_id = "".join(c for c in call_id if c.isalnum() or c in "-_.")
50
- return os.path.join(self.storage_dir, f"{sanitized_id}.json")
51
-
52
- def store(self, call_id: str, data: Dict[str, Any]) -> bool:
53
- """
54
- Store state data for a call
55
-
56
- Args:
57
- call_id: Unique identifier for the call
58
- data: Dictionary of state data to store
59
-
60
- Returns:
61
- True if successful, False otherwise
62
- """
63
- try:
64
- # Add metadata including timestamp
65
- state_data = {
66
- "call_id": call_id,
67
- "created_at": datetime.now().isoformat(),
68
- "last_updated": datetime.now().isoformat(),
69
- "data": data
70
- }
71
-
72
- file_path = self._get_file_path(call_id)
73
- with open(file_path, "w") as f:
74
- json.dump(state_data, f, indent=2)
75
- return True
76
- except Exception as e:
77
- print(f"Error storing state for call {call_id}: {e}")
78
- return False
79
-
80
- def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
81
- """
82
- Retrieve state data for a call
83
-
84
- Args:
85
- call_id: Unique identifier for the call
86
-
87
- Returns:
88
- Dictionary of state data or None if not found
89
- """
90
- file_path = self._get_file_path(call_id)
91
- if not os.path.exists(file_path):
92
- return None
93
-
94
- try:
95
- with open(file_path, "r") as f:
96
- state_data = json.load(f)
97
-
98
- # Check if the file is expired
99
- created_at = datetime.fromisoformat(state_data["created_at"])
100
- if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
101
- # Expired, so delete it and return None
102
- os.remove(file_path)
103
- return None
104
-
105
- return state_data["data"]
106
- except Exception as e:
107
- print(f"Error retrieving state for call {call_id}: {e}")
108
- return None
109
-
110
- def update(self, call_id: str, data: Dict[str, Any]) -> bool:
111
- """
112
- Update state data for a call
113
-
114
- Args:
115
- call_id: Unique identifier for the call
116
- data: Dictionary of state data to update (merged with existing)
117
-
118
- Returns:
119
- True if successful, False otherwise
120
- """
121
- file_path = self._get_file_path(call_id)
122
- if not os.path.exists(file_path):
123
- # If no existing data, just store new data
124
- return self.store(call_id, data)
125
-
126
- try:
127
- # Read existing data
128
- with open(file_path, "r") as f:
129
- state_data = json.load(f)
130
-
131
- # Update the data (deep merge)
132
- existing_data = state_data["data"]
133
- self._deep_update(existing_data, data)
134
-
135
- # Update metadata
136
- state_data["last_updated"] = datetime.now().isoformat()
137
- state_data["data"] = existing_data
138
-
139
- # Write back to file
140
- with open(file_path, "w") as f:
141
- json.dump(state_data, f, indent=2)
142
-
143
- return True
144
- except Exception as e:
145
- print(f"Error updating state for call {call_id}: {e}")
146
- return False
147
-
148
- def delete(self, call_id: str) -> bool:
149
- """
150
- Delete state data for a call
151
-
152
- Args:
153
- call_id: Unique identifier for the call
154
-
155
- Returns:
156
- True if successful, False otherwise
157
- """
158
- file_path = self._get_file_path(call_id)
159
- if not os.path.exists(file_path):
160
- return False
161
-
162
- try:
163
- os.remove(file_path)
164
- return True
165
- except Exception as e:
166
- print(f"Error deleting state for call {call_id}: {e}")
167
- return False
168
-
169
- def cleanup_expired(self) -> int:
170
- """
171
- Clean up expired state files
172
-
173
- Returns:
174
- Number of expired files cleaned up
175
- """
176
- count = 0
177
- try:
178
- # Get all state files
179
- for filename in os.listdir(self.storage_dir):
180
- if not filename.endswith(".json"):
181
- continue
182
-
183
- file_path = os.path.join(self.storage_dir, filename)
184
-
185
- try:
186
- # Read the file to check creation time
187
- with open(file_path, "r") as f:
188
- state_data = json.load(f)
189
-
190
- # Check if the file is expired
191
- created_at = datetime.fromisoformat(state_data["created_at"])
192
- if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
193
- os.remove(file_path)
194
- count += 1
195
- except Exception:
196
- # Skip files that can't be processed
197
- continue
198
-
199
- return count
200
- except Exception as e:
201
- print(f"Error cleaning up expired state files: {e}")
202
- return count
203
-
204
- def _deep_update(self, d: Dict, u: Dict) -> None:
205
- """Recursively update a nested dictionary"""
206
- for k, v in u.items():
207
- if isinstance(v, dict) and k in d and isinstance(d[k], dict):
208
- self._deep_update(d[k], v)
209
- else:
210
- d[k] = v