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