wappa 0.1.8__py3-none-any.whl → 0.1.9__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.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +8 -0
- wappa/cli/examples/init/app/master_event.py +8 -0
- wappa/cli/examples/json_cache_example/.env.example +33 -0
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/main.py +235 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +275 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/openai_transcript/.gitignore +63 -4
- wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
- wappa/cli/examples/openai_transcript/app/main.py +8 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -0
- wappa/cli/examples/redis_cache_example/.env.example +33 -0
- wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
- wappa/cli/examples/redis_cache_example/app/main.py +234 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/simple_echo_example/.env.example +33 -0
- wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
- wappa/cli/examples/simple_echo_example/app/main.py +183 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +209 -0
- wappa/cli/examples/wappa_full_example/.env.example +33 -0
- wappa/cli/examples/wappa_full_example/.gitignore +63 -4
- wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +484 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/main.py +257 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +445 -0
- wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
- wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +425 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
- wappa/cli/main.py +8 -4
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/RECORD +75 -11
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive state models for the Wappa Full Example application.
|
|
3
|
+
|
|
4
|
+
Contains models for managing button and list interactive states with TTL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StateType(str, Enum):
|
|
15
|
+
"""Types of interactive states."""
|
|
16
|
+
BUTTON = "button"
|
|
17
|
+
LIST = "list"
|
|
18
|
+
CTA = "cta"
|
|
19
|
+
LOCATION = "location"
|
|
20
|
+
CUSTOM = "custom"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StateStatus(str, Enum):
|
|
24
|
+
"""Status of interactive states."""
|
|
25
|
+
ACTIVE = "active"
|
|
26
|
+
EXPIRED = "expired"
|
|
27
|
+
COMPLETED = "completed"
|
|
28
|
+
CANCELLED = "cancelled"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InteractiveState(BaseModel):
|
|
32
|
+
"""Base model for interactive session states (buttons/lists)."""
|
|
33
|
+
|
|
34
|
+
# State identification
|
|
35
|
+
state_id: str = Field(default_factory=lambda: f"state_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}")
|
|
36
|
+
user_id: str
|
|
37
|
+
state_type: StateType
|
|
38
|
+
|
|
39
|
+
# State timing
|
|
40
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
41
|
+
expires_at: datetime
|
|
42
|
+
last_activity: datetime = Field(default_factory=datetime.now)
|
|
43
|
+
|
|
44
|
+
# State data
|
|
45
|
+
context: Dict[str, Any] = Field(default_factory=dict)
|
|
46
|
+
original_message_id: Optional[str] = None
|
|
47
|
+
interactive_message_id: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
# State management
|
|
50
|
+
status: StateStatus = StateStatus.ACTIVE
|
|
51
|
+
attempts: int = 0
|
|
52
|
+
max_attempts: int = 5
|
|
53
|
+
|
|
54
|
+
# Metadata
|
|
55
|
+
creation_metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
@field_validator('user_id', mode='before')
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_user_id(cls, v):
|
|
60
|
+
"""Convert user ID to string if it's an integer."""
|
|
61
|
+
return str(v) if v is not None else v
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create_with_ttl(cls, user_id: str, state_type: StateType, ttl_seconds: int = 600, **kwargs) -> "InteractiveState":
|
|
65
|
+
"""Create a new interactive state with TTL."""
|
|
66
|
+
expires_at = datetime.now() + timedelta(seconds=ttl_seconds)
|
|
67
|
+
return cls(
|
|
68
|
+
user_id=user_id,
|
|
69
|
+
state_type=state_type,
|
|
70
|
+
expires_at=expires_at,
|
|
71
|
+
**kwargs
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def is_expired(self) -> bool:
|
|
75
|
+
"""Check if the state has expired."""
|
|
76
|
+
return datetime.now() > self.expires_at or self.status == StateStatus.EXPIRED
|
|
77
|
+
|
|
78
|
+
def is_active(self) -> bool:
|
|
79
|
+
"""Check if the state is currently active."""
|
|
80
|
+
return not self.is_expired() and self.status == StateStatus.ACTIVE
|
|
81
|
+
|
|
82
|
+
def time_remaining_seconds(self) -> int:
|
|
83
|
+
"""Get remaining time in seconds."""
|
|
84
|
+
if self.is_expired():
|
|
85
|
+
return 0
|
|
86
|
+
remaining = self.expires_at - datetime.now()
|
|
87
|
+
return max(0, int(remaining.total_seconds()))
|
|
88
|
+
|
|
89
|
+
def time_remaining_minutes(self) -> int:
|
|
90
|
+
"""Get remaining time in minutes."""
|
|
91
|
+
return max(0, self.time_remaining_seconds() // 60)
|
|
92
|
+
|
|
93
|
+
def increment_attempts(self) -> None:
|
|
94
|
+
"""Increment the attempts counter."""
|
|
95
|
+
self.attempts += 1
|
|
96
|
+
self.last_activity = datetime.now()
|
|
97
|
+
|
|
98
|
+
if self.attempts >= self.max_attempts:
|
|
99
|
+
self.status = StateStatus.CANCELLED
|
|
100
|
+
|
|
101
|
+
def mark_completed(self) -> None:
|
|
102
|
+
"""Mark the state as completed."""
|
|
103
|
+
self.status = StateStatus.COMPLETED
|
|
104
|
+
self.last_activity = datetime.now()
|
|
105
|
+
|
|
106
|
+
def mark_expired(self) -> None:
|
|
107
|
+
"""Mark the state as expired."""
|
|
108
|
+
self.status = StateStatus.EXPIRED
|
|
109
|
+
self.last_activity = datetime.now()
|
|
110
|
+
|
|
111
|
+
def mark_cancelled(self) -> None:
|
|
112
|
+
"""Mark the state as cancelled."""
|
|
113
|
+
self.status = StateStatus.CANCELLED
|
|
114
|
+
self.last_activity = datetime.now()
|
|
115
|
+
|
|
116
|
+
def update_context(self, key: str, value: Any) -> None:
|
|
117
|
+
"""Update a context value."""
|
|
118
|
+
self.context[key] = value
|
|
119
|
+
self.last_activity = datetime.now()
|
|
120
|
+
|
|
121
|
+
def get_context_value(self, key: str, default: Any = None) -> Any:
|
|
122
|
+
"""Get a context value."""
|
|
123
|
+
return self.context.get(key, default)
|
|
124
|
+
|
|
125
|
+
def extend_ttl(self, additional_seconds: int) -> None:
|
|
126
|
+
"""Extend the TTL of this state."""
|
|
127
|
+
if self.is_active():
|
|
128
|
+
self.expires_at = self.expires_at + timedelta(seconds=additional_seconds)
|
|
129
|
+
self.last_activity = datetime.now()
|
|
130
|
+
|
|
131
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
132
|
+
"""Get a summary of this state."""
|
|
133
|
+
return {
|
|
134
|
+
"state_id": self.state_id,
|
|
135
|
+
"user_id": self.user_id,
|
|
136
|
+
"state_type": self.state_type.value,
|
|
137
|
+
"status": self.status.value,
|
|
138
|
+
"is_active": self.is_active(),
|
|
139
|
+
"time_remaining_seconds": self.time_remaining_seconds(),
|
|
140
|
+
"attempts": self.attempts,
|
|
141
|
+
"max_attempts": self.max_attempts,
|
|
142
|
+
"created_at": self.created_at.isoformat(),
|
|
143
|
+
"expires_at": self.expires_at.isoformat(),
|
|
144
|
+
"context": self.context
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ButtonState(InteractiveState):
|
|
149
|
+
"""State model specifically for button interactions."""
|
|
150
|
+
|
|
151
|
+
state_type: StateType = StateType.BUTTON
|
|
152
|
+
|
|
153
|
+
# Button-specific data
|
|
154
|
+
button_options: List[Dict[str, str]] = Field(default_factory=list)
|
|
155
|
+
selected_button_id: Optional[str] = None
|
|
156
|
+
button_message_text: Optional[str] = None
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def create_button_state(
|
|
160
|
+
cls,
|
|
161
|
+
user_id: str,
|
|
162
|
+
buttons: List[Dict[str, str]],
|
|
163
|
+
message_text: str,
|
|
164
|
+
ttl_seconds: int = 600,
|
|
165
|
+
original_message_id: str = None
|
|
166
|
+
) -> "ButtonState":
|
|
167
|
+
"""Create a new button state."""
|
|
168
|
+
return cls.create_with_ttl(
|
|
169
|
+
user_id=user_id,
|
|
170
|
+
state_type=StateType.BUTTON,
|
|
171
|
+
ttl_seconds=ttl_seconds,
|
|
172
|
+
button_options=buttons,
|
|
173
|
+
button_message_text=message_text,
|
|
174
|
+
original_message_id=original_message_id,
|
|
175
|
+
context={
|
|
176
|
+
"button_count": len(buttons),
|
|
177
|
+
"message_text": message_text,
|
|
178
|
+
"expected_selections": [btn.get("id", "") for btn in buttons]
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def is_valid_selection(self, button_id: str) -> bool:
|
|
183
|
+
"""Check if the button selection is valid."""
|
|
184
|
+
valid_ids = [btn.get("id", "") for btn in self.button_options]
|
|
185
|
+
return button_id in valid_ids
|
|
186
|
+
|
|
187
|
+
def handle_selection(self, button_id: str) -> bool:
|
|
188
|
+
"""Handle button selection."""
|
|
189
|
+
if self.is_valid_selection(button_id) and self.is_active():
|
|
190
|
+
self.selected_button_id = button_id
|
|
191
|
+
self.mark_completed()
|
|
192
|
+
self.update_context("selected_button_id", button_id)
|
|
193
|
+
self.update_context("selected_at", datetime.now().isoformat())
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def get_selected_button(self) -> Optional[Dict[str, str]]:
|
|
198
|
+
"""Get the selected button information."""
|
|
199
|
+
if not self.selected_button_id:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
for button in self.button_options:
|
|
203
|
+
if button.get("id") == self.selected_button_id:
|
|
204
|
+
return button
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class ListState(InteractiveState):
|
|
209
|
+
"""State model specifically for list interactions."""
|
|
210
|
+
|
|
211
|
+
state_type: StateType = StateType.LIST
|
|
212
|
+
|
|
213
|
+
# List-specific data
|
|
214
|
+
list_sections: List[Dict[str, Any]] = Field(default_factory=list)
|
|
215
|
+
selected_item_id: Optional[str] = None
|
|
216
|
+
list_button_text: str = "Choose an option"
|
|
217
|
+
list_message_text: Optional[str] = None
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def create_list_state(
|
|
221
|
+
cls,
|
|
222
|
+
user_id: str,
|
|
223
|
+
sections: List[Dict[str, Any]],
|
|
224
|
+
message_text: str,
|
|
225
|
+
button_text: str = "Choose an option",
|
|
226
|
+
ttl_seconds: int = 600,
|
|
227
|
+
original_message_id: str = None
|
|
228
|
+
) -> "ListState":
|
|
229
|
+
"""Create a new list state."""
|
|
230
|
+
# Extract all possible item IDs for validation
|
|
231
|
+
valid_ids = []
|
|
232
|
+
for section in sections:
|
|
233
|
+
rows = section.get("rows", [])
|
|
234
|
+
for row in rows:
|
|
235
|
+
if "id" in row:
|
|
236
|
+
valid_ids.append(row["id"])
|
|
237
|
+
|
|
238
|
+
return cls.create_with_ttl(
|
|
239
|
+
user_id=user_id,
|
|
240
|
+
state_type=StateType.LIST,
|
|
241
|
+
ttl_seconds=ttl_seconds,
|
|
242
|
+
list_sections=sections,
|
|
243
|
+
list_message_text=message_text,
|
|
244
|
+
list_button_text=button_text,
|
|
245
|
+
original_message_id=original_message_id,
|
|
246
|
+
context={
|
|
247
|
+
"sections_count": len(sections),
|
|
248
|
+
"total_items": len(valid_ids),
|
|
249
|
+
"message_text": message_text,
|
|
250
|
+
"button_text": button_text,
|
|
251
|
+
"expected_selections": valid_ids
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def is_valid_selection(self, item_id: str) -> bool:
|
|
256
|
+
"""Check if the list item selection is valid."""
|
|
257
|
+
valid_ids = self.get_context_value("expected_selections", [])
|
|
258
|
+
return item_id in valid_ids
|
|
259
|
+
|
|
260
|
+
def handle_selection(self, item_id: str) -> bool:
|
|
261
|
+
"""Handle list item selection."""
|
|
262
|
+
if self.is_valid_selection(item_id) and self.is_active():
|
|
263
|
+
self.selected_item_id = item_id
|
|
264
|
+
self.mark_completed()
|
|
265
|
+
self.update_context("selected_item_id", item_id)
|
|
266
|
+
self.update_context("selected_at", datetime.now().isoformat())
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def get_selected_item(self) -> Optional[Dict[str, Any]]:
|
|
271
|
+
"""Get the selected list item information."""
|
|
272
|
+
if not self.selected_item_id:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
for section in self.list_sections:
|
|
276
|
+
rows = section.get("rows", [])
|
|
277
|
+
for row in rows:
|
|
278
|
+
if row.get("id") == self.selected_item_id:
|
|
279
|
+
return row
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class CommandState(InteractiveState):
|
|
284
|
+
"""State model for command-based interactions."""
|
|
285
|
+
|
|
286
|
+
state_type: StateType = StateType.CUSTOM
|
|
287
|
+
|
|
288
|
+
# Command-specific data
|
|
289
|
+
command_name: str
|
|
290
|
+
expected_responses: List[str] = Field(default_factory=list)
|
|
291
|
+
current_step: int = 0
|
|
292
|
+
total_steps: int = 1
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def create_command_state(
|
|
296
|
+
cls,
|
|
297
|
+
user_id: str,
|
|
298
|
+
command_name: str,
|
|
299
|
+
expected_responses: List[str] = None,
|
|
300
|
+
ttl_seconds: int = 600,
|
|
301
|
+
total_steps: int = 1,
|
|
302
|
+
original_message_id: str = None
|
|
303
|
+
) -> "CommandState":
|
|
304
|
+
"""Create a new command state."""
|
|
305
|
+
return cls.create_with_ttl(
|
|
306
|
+
user_id=user_id,
|
|
307
|
+
state_type=StateType.CUSTOM,
|
|
308
|
+
ttl_seconds=ttl_seconds,
|
|
309
|
+
command_name=command_name,
|
|
310
|
+
expected_responses=expected_responses or [],
|
|
311
|
+
total_steps=total_steps,
|
|
312
|
+
original_message_id=original_message_id,
|
|
313
|
+
context={
|
|
314
|
+
"command_name": command_name,
|
|
315
|
+
"expected_responses": expected_responses or [],
|
|
316
|
+
"total_steps": total_steps,
|
|
317
|
+
"current_step": 0
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def advance_step(self) -> bool:
|
|
322
|
+
"""Advance to the next step."""
|
|
323
|
+
if self.current_step < self.total_steps - 1:
|
|
324
|
+
self.current_step += 1
|
|
325
|
+
self.update_context("current_step", self.current_step)
|
|
326
|
+
return True
|
|
327
|
+
else:
|
|
328
|
+
self.mark_completed()
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
def is_final_step(self) -> bool:
|
|
332
|
+
"""Check if this is the final step."""
|
|
333
|
+
return self.current_step >= self.total_steps - 1
|
|
334
|
+
|
|
335
|
+
def get_progress_percentage(self) -> float:
|
|
336
|
+
"""Get the progress percentage."""
|
|
337
|
+
if self.total_steps <= 1:
|
|
338
|
+
return 100.0 if self.status == StateStatus.COMPLETED else 0.0
|
|
339
|
+
return (self.current_step / self.total_steps) * 100.0
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class StateManager(BaseModel):
|
|
343
|
+
"""Manager for handling multiple interactive states."""
|
|
344
|
+
|
|
345
|
+
active_states: Dict[str, InteractiveState] = Field(default_factory=dict)
|
|
346
|
+
completed_states: Dict[str, InteractiveState] = Field(default_factory=dict)
|
|
347
|
+
|
|
348
|
+
def add_state(self, state: InteractiveState) -> None:
|
|
349
|
+
"""Add a new state to the manager."""
|
|
350
|
+
state_key = f"{state.user_id}_{state.state_type.value}"
|
|
351
|
+
|
|
352
|
+
# Remove any existing state of the same type for this user
|
|
353
|
+
if state_key in self.active_states:
|
|
354
|
+
old_state = self.active_states[state_key]
|
|
355
|
+
old_state.mark_cancelled()
|
|
356
|
+
self.completed_states[f"{state_key}_{old_state.state_id}"] = old_state
|
|
357
|
+
|
|
358
|
+
self.active_states[state_key] = state
|
|
359
|
+
|
|
360
|
+
def get_user_state(self, user_id: str, state_type: StateType) -> Optional[InteractiveState]:
|
|
361
|
+
"""Get the active state for a user and state type."""
|
|
362
|
+
state_key = f"{user_id}_{state_type.value}"
|
|
363
|
+
state = self.active_states.get(state_key)
|
|
364
|
+
|
|
365
|
+
if state and state.is_expired():
|
|
366
|
+
state.mark_expired()
|
|
367
|
+
self.completed_states[f"{state_key}_{state.state_id}"] = state
|
|
368
|
+
del self.active_states[state_key]
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
return state if state and state.is_active() else None
|
|
372
|
+
|
|
373
|
+
def remove_state(self, user_id: str, state_type: StateType) -> Optional[InteractiveState]:
|
|
374
|
+
"""Remove a state from active states."""
|
|
375
|
+
state_key = f"{user_id}_{state_type.value}"
|
|
376
|
+
state = self.active_states.pop(state_key, None)
|
|
377
|
+
|
|
378
|
+
if state:
|
|
379
|
+
self.completed_states[f"{state_key}_{state.state_id}"] = state
|
|
380
|
+
|
|
381
|
+
return state
|
|
382
|
+
|
|
383
|
+
def cleanup_expired_states(self) -> int:
|
|
384
|
+
"""Clean up expired states and return count of cleaned states."""
|
|
385
|
+
expired_count = 0
|
|
386
|
+
expired_keys = []
|
|
387
|
+
|
|
388
|
+
for state_key, state in self.active_states.items():
|
|
389
|
+
if state.is_expired():
|
|
390
|
+
state.mark_expired()
|
|
391
|
+
self.completed_states[f"{state_key}_{state.state_id}"] = state
|
|
392
|
+
expired_keys.append(state_key)
|
|
393
|
+
expired_count += 1
|
|
394
|
+
|
|
395
|
+
for key in expired_keys:
|
|
396
|
+
del self.active_states[key]
|
|
397
|
+
|
|
398
|
+
return expired_count
|
|
399
|
+
|
|
400
|
+
def get_user_states(self, user_id: str) -> List[InteractiveState]:
|
|
401
|
+
"""Get all active states for a user."""
|
|
402
|
+
user_states = []
|
|
403
|
+
for state in self.active_states.values():
|
|
404
|
+
if state.user_id == user_id and state.is_active():
|
|
405
|
+
user_states.append(state)
|
|
406
|
+
return user_states
|
|
407
|
+
|
|
408
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
409
|
+
"""Get manager statistics."""
|
|
410
|
+
active_count = len(self.active_states)
|
|
411
|
+
completed_count = len(self.completed_states)
|
|
412
|
+
|
|
413
|
+
# Count by state type
|
|
414
|
+
type_counts = {}
|
|
415
|
+
for state in self.active_states.values():
|
|
416
|
+
state_type = state.state_type.value
|
|
417
|
+
type_counts[state_type] = type_counts.get(state_type, 0) + 1
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
"active_states": active_count,
|
|
421
|
+
"completed_states": completed_count,
|
|
422
|
+
"total_states": active_count + completed_count,
|
|
423
|
+
"state_type_distribution": type_counts,
|
|
424
|
+
"cleanup_needed": sum(1 for state in self.active_states.values() if state.is_expired())
|
|
425
|
+
}
|