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.
- signalwire_agents/__init__.py +10 -1
- signalwire_agents/agent_server.py +73 -44
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.1.dist-info}/METADATA +75 -30
- signalwire_agents-0.1.1.dist-info/RECORD +9 -0
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.1.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.1.dist-info/licenses/LICENSE +21 -0
- signalwire_agents/core/__init__.py +0 -20
- signalwire_agents/core/agent_base.py +0 -2449
- signalwire_agents/core/function_result.py +0 -104
- signalwire_agents/core/pom_builder.py +0 -195
- signalwire_agents/core/security/__init__.py +0 -0
- signalwire_agents/core/security/session_manager.py +0 -170
- signalwire_agents/core/state/__init__.py +0 -8
- signalwire_agents/core/state/file_state_manager.py +0 -210
- signalwire_agents/core/state/state_manager.py +0 -92
- signalwire_agents/core/swaig_function.py +0 -163
- signalwire_agents/core/swml_builder.py +0 -205
- signalwire_agents/core/swml_handler.py +0 -218
- signalwire_agents/core/swml_renderer.py +0 -359
- signalwire_agents/core/swml_service.py +0 -1009
- signalwire_agents/prefabs/__init__.py +0 -15
- signalwire_agents/prefabs/concierge.py +0 -276
- signalwire_agents/prefabs/faq_bot.py +0 -314
- signalwire_agents/prefabs/info_gatherer.py +0 -253
- signalwire_agents/prefabs/survey.py +0 -387
- signalwire_agents/utils/__init__.py +0 -0
- signalwire_agents/utils/pom_utils.py +0 -0
- signalwire_agents/utils/schema_utils.py +0 -348
- signalwire_agents/utils/token_generators.py +0 -0
- signalwire_agents/utils/validators.py +0 -0
- signalwire_agents-0.1.0.dist-info/RECORD +0 -32
- {signalwire_agents-0.1.0.data → signalwire_agents-0.1.1.data}/data/schema.json +0 -0
- {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,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
|