wappa 0.1.7__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/core/config/settings.py +34 -2
- wappa/persistence/__init__.py +2 -2
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/RECORD +77 -13
- 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.7.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Score modules for JSON Cache Example following SOLID principles.
|
|
3
|
+
|
|
4
|
+
Each score module handles a specific business concern:
|
|
5
|
+
- score_user_management: User profile and caching logic
|
|
6
|
+
- score_message_history: Message logging and history retrieval
|
|
7
|
+
- score_state_commands: /WAPPA, /EXIT command processing
|
|
8
|
+
- score_cache_statistics: Cache monitoring and statistics
|
|
9
|
+
|
|
10
|
+
This architecture follows the Single Responsibility and Open/Closed principles.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .score_base import ScoreBase, ScoreDependencies
|
|
14
|
+
from .score_cache_statistics import CacheStatisticsScore
|
|
15
|
+
from .score_message_history import MessageHistoryScore
|
|
16
|
+
from .score_state_commands import StateCommandsScore
|
|
17
|
+
from .score_user_management import UserManagementScore
|
|
18
|
+
|
|
19
|
+
# Available score modules for automatic discovery
|
|
20
|
+
AVAILABLE_SCORES = [
|
|
21
|
+
UserManagementScore,
|
|
22
|
+
MessageHistoryScore,
|
|
23
|
+
StateCommandsScore,
|
|
24
|
+
CacheStatisticsScore,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ScoreBase",
|
|
29
|
+
"ScoreDependencies",
|
|
30
|
+
"UserManagementScore",
|
|
31
|
+
"MessageHistoryScore",
|
|
32
|
+
"StateCommandsScore",
|
|
33
|
+
"CacheStatisticsScore",
|
|
34
|
+
"AVAILABLE_SCORES",
|
|
35
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface for score modules following Interface Segregation Principle.
|
|
3
|
+
|
|
4
|
+
This module defines the common interface that all score modules must implement,
|
|
5
|
+
ensuring consistent behavior across different business logic handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from logging import Logger
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from wappa.domain.interfaces.cache_interface import ICache
|
|
14
|
+
from wappa.messaging.whatsapp.messenger.whatsapp_messenger import WhatsAppMessenger
|
|
15
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ScoreDependencies:
|
|
20
|
+
"""
|
|
21
|
+
Dependencies required by score modules.
|
|
22
|
+
|
|
23
|
+
This follows Dependency Inversion Principle by providing
|
|
24
|
+
abstractions that score modules depend on.
|
|
25
|
+
"""
|
|
26
|
+
messenger: WhatsAppMessenger
|
|
27
|
+
user_cache: ICache
|
|
28
|
+
table_cache: ICache
|
|
29
|
+
state_cache: ICache
|
|
30
|
+
logger: Logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ScoreBase(ABC):
|
|
34
|
+
"""
|
|
35
|
+
Base class for all score modules.
|
|
36
|
+
|
|
37
|
+
Implements Interface Segregation Principle by providing
|
|
38
|
+
only the methods that score modules actually need.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, dependencies: ScoreDependencies):
|
|
42
|
+
"""
|
|
43
|
+
Initialize score with injected dependencies.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
dependencies: Required dependencies for the score
|
|
47
|
+
"""
|
|
48
|
+
self.messenger = dependencies.messenger
|
|
49
|
+
self.user_cache = dependencies.user_cache
|
|
50
|
+
self.table_cache = dependencies.table_cache
|
|
51
|
+
self.state_cache = dependencies.state_cache
|
|
52
|
+
self.logger = dependencies.logger
|
|
53
|
+
|
|
54
|
+
# Track processing statistics
|
|
55
|
+
self._processing_count = 0
|
|
56
|
+
self._error_count = 0
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def score_name(self) -> str:
|
|
60
|
+
"""Return the name of this score module."""
|
|
61
|
+
return self.__class__.__name__
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def processing_stats(self) -> dict:
|
|
65
|
+
"""Return processing statistics for this score."""
|
|
66
|
+
return {
|
|
67
|
+
'processed': self._processing_count,
|
|
68
|
+
'errors': self._error_count,
|
|
69
|
+
'success_rate': (
|
|
70
|
+
(self._processing_count - self._error_count) / self._processing_count
|
|
71
|
+
if self._processing_count > 0 else 0.0
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Determine if this score can handle the given webhook.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
webhook: Incoming message webhook to evaluate
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if this score should process the webhook
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Process the webhook with this score's business logic.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
webhook: Incoming message webhook to process
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if processing was successful and complete
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def validate_dependencies(self) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Validate that all required dependencies are available.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if all dependencies are valid
|
|
107
|
+
"""
|
|
108
|
+
if not all([self.messenger, self.user_cache, self.table_cache,
|
|
109
|
+
self.state_cache, self.logger]):
|
|
110
|
+
self.logger.error(f"{self.score_name}: Missing required dependencies")
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def _record_processing(self, success: bool = True) -> None:
|
|
115
|
+
"""Record processing statistics."""
|
|
116
|
+
self._processing_count += 1
|
|
117
|
+
if not success:
|
|
118
|
+
self._error_count += 1
|
|
119
|
+
|
|
120
|
+
async def _handle_error(self, error: Exception, context: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Handle errors consistently across score modules.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
error: Exception that occurred
|
|
126
|
+
context: Context where error occurred
|
|
127
|
+
"""
|
|
128
|
+
self._record_processing(success=False)
|
|
129
|
+
self.logger.error(
|
|
130
|
+
f"{self.score_name} error in {context}: {str(error)}",
|
|
131
|
+
exc_info=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def __str__(self) -> str:
|
|
135
|
+
"""String representation of the score."""
|
|
136
|
+
return f"{self.score_name}(processed={self._processing_count}, errors={self._error_count})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ScoreRegistry:
|
|
140
|
+
"""
|
|
141
|
+
Registry for managing score modules.
|
|
142
|
+
|
|
143
|
+
Implements Open/Closed Principle by allowing new scores
|
|
144
|
+
to be registered without modifying existing code.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self):
|
|
148
|
+
self._scores: list[ScoreBase] = []
|
|
149
|
+
|
|
150
|
+
def register_score(self, score: ScoreBase) -> None:
|
|
151
|
+
"""Register a score module."""
|
|
152
|
+
if not isinstance(score, ScoreBase):
|
|
153
|
+
raise ValueError("Score must inherit from ScoreBase")
|
|
154
|
+
|
|
155
|
+
self._scores.append(score)
|
|
156
|
+
|
|
157
|
+
def get_scores(self) -> list[ScoreBase]:
|
|
158
|
+
"""Get all registered scores."""
|
|
159
|
+
return self._scores.copy()
|
|
160
|
+
|
|
161
|
+
async def find_handler(self, webhook: IncomingMessageWebhook) -> Optional[ScoreBase]:
|
|
162
|
+
"""
|
|
163
|
+
Find the first score that can handle the webhook.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
webhook: Webhook to find handler for
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Score that can handle the webhook, or None
|
|
170
|
+
"""
|
|
171
|
+
for score in self._scores:
|
|
172
|
+
try:
|
|
173
|
+
if await score.can_handle(webhook):
|
|
174
|
+
return score
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# Log error but continue to next score
|
|
177
|
+
score.logger.error(f"Error checking if {score.score_name} can handle webhook: {e}")
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def get_score_stats(self) -> dict:
|
|
182
|
+
"""Get statistics for all registered scores."""
|
|
183
|
+
return {
|
|
184
|
+
score.score_name: score.processing_stats
|
|
185
|
+
for score in self._scores
|
|
186
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache Statistics Score - Single Responsibility: JSON cache monitoring and statistics.
|
|
3
|
+
|
|
4
|
+
This module handles all cache statistics operations including:
|
|
5
|
+
- Cache hit/miss tracking
|
|
6
|
+
- Cache performance metrics
|
|
7
|
+
- /STATS command processing
|
|
8
|
+
- Cache health monitoring
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ..models.json_demo_models import CacheStats
|
|
12
|
+
from ..utils.cache_utils import get_cache_ttl
|
|
13
|
+
from ..utils.message_utils import extract_command_from_message, extract_user_data
|
|
14
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
15
|
+
|
|
16
|
+
from .score_base import ScoreBase
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CacheStatisticsScore(ScoreBase):
|
|
20
|
+
"""
|
|
21
|
+
Handles cache statistics monitoring and reporting operations.
|
|
22
|
+
|
|
23
|
+
Follows Single Responsibility Principle by focusing only
|
|
24
|
+
on cache performance monitoring and statistics.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
This score handles /STATS command specifically.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
webhook: Incoming message webhook
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if this is a /STATS command
|
|
36
|
+
"""
|
|
37
|
+
message_text = webhook.get_message_text()
|
|
38
|
+
if not message_text:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
command, _ = extract_command_from_message(message_text.strip())
|
|
42
|
+
return command == "/STATS"
|
|
43
|
+
|
|
44
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Process cache statistics request.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
webhook: Incoming message webhook
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if processing was successful
|
|
53
|
+
"""
|
|
54
|
+
if not await self.validate_dependencies():
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
await self._handle_stats_request(webhook)
|
|
59
|
+
self._record_processing(success=True)
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
await self._handle_error(e, "cache_statistics_processing")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
async def _handle_stats_request(self, webhook: IncomingMessageWebhook) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Handle /STATS command to show cache statistics.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
webhook: Incoming message webhook
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
user_data = extract_user_data(webhook)
|
|
75
|
+
user_id = user_data['user_id']
|
|
76
|
+
|
|
77
|
+
# Collect statistics from all cache layers
|
|
78
|
+
stats = await self._collect_cache_statistics()
|
|
79
|
+
|
|
80
|
+
# Generate statistics report
|
|
81
|
+
stats_message = self._format_statistics_message(stats)
|
|
82
|
+
|
|
83
|
+
# Mark message as read with typing indicator first
|
|
84
|
+
await self.messenger.mark_as_read(
|
|
85
|
+
message_id=webhook.message.message_id,
|
|
86
|
+
typing=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Send statistics to user
|
|
90
|
+
result = await self.messenger.send_text(
|
|
91
|
+
recipient=user_id,
|
|
92
|
+
text=stats_message,
|
|
93
|
+
reply_to_message_id=webhook.message.message_id
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if result.success:
|
|
97
|
+
self.logger.info(f"✅ Cache statistics sent to {user_id}")
|
|
98
|
+
else:
|
|
99
|
+
self.logger.error(f"❌ Failed to send statistics: {result.error}")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.logger.error(f"Error handling stats request: {e}")
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
async def _collect_cache_statistics(self) -> CacheStats:
|
|
106
|
+
"""
|
|
107
|
+
Get or create cache statistics from table cache (persistent storage).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CacheStats object from table cache or newly created
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
# Use table cache to store/retrieve statistics (proper table key format)
|
|
114
|
+
# table_name:pkid format as required by table cache
|
|
115
|
+
stats_key = self.table_cache.create_table_key("cache_statistics", "global")
|
|
116
|
+
|
|
117
|
+
# Try to get existing stats from table cache
|
|
118
|
+
existing_stats = await self.table_cache.get(stats_key, models=CacheStats)
|
|
119
|
+
|
|
120
|
+
if existing_stats:
|
|
121
|
+
# Update existing stats
|
|
122
|
+
stats = existing_stats
|
|
123
|
+
stats.total_operations += 1 # Increment operation count
|
|
124
|
+
stats.last_updated = stats.last_updated # Will auto-update via Pydantic
|
|
125
|
+
self.logger.debug("📊 Retrieved existing cache statistics from table cache")
|
|
126
|
+
else:
|
|
127
|
+
# Create new stats
|
|
128
|
+
stats = CacheStats()
|
|
129
|
+
stats.cache_type = "JSON"
|
|
130
|
+
stats.total_operations = 1
|
|
131
|
+
self.logger.info("📊 Created new cache statistics in table cache")
|
|
132
|
+
|
|
133
|
+
# Test cache connectivity using a simple operation
|
|
134
|
+
try:
|
|
135
|
+
# Test connectivity by creating a temporary data entry
|
|
136
|
+
test_data = {"test": "connectivity_check", "timestamp": str(stats.last_updated)}
|
|
137
|
+
test_key = "connectivity_test"
|
|
138
|
+
|
|
139
|
+
# Store test data in user_cache
|
|
140
|
+
await self.user_cache.set(test_key, test_data, ttl=5)
|
|
141
|
+
test_result = await self.user_cache.get(test_key)
|
|
142
|
+
|
|
143
|
+
if test_result and isinstance(test_result, dict) and test_result.get("test") == "connectivity_check":
|
|
144
|
+
stats.connection_status = "File System"
|
|
145
|
+
stats.is_healthy = True
|
|
146
|
+
# Clean up test key
|
|
147
|
+
await self.user_cache.delete(test_key)
|
|
148
|
+
self.logger.debug("✅ Cache connectivity test passed")
|
|
149
|
+
else:
|
|
150
|
+
stats.connection_status = "File System Issues"
|
|
151
|
+
stats.is_healthy = False
|
|
152
|
+
stats.errors += 1
|
|
153
|
+
self.logger.warning(f"⚠️ Cache connectivity test failed: got {test_result}")
|
|
154
|
+
|
|
155
|
+
except Exception as cache_error:
|
|
156
|
+
self.logger.error(f"Cache connectivity test failed: {cache_error}")
|
|
157
|
+
stats.connection_status = f"Error: {str(cache_error)}"
|
|
158
|
+
stats.is_healthy = False
|
|
159
|
+
stats.errors += 1
|
|
160
|
+
|
|
161
|
+
# Store updated statistics in table cache (persistent like message history)
|
|
162
|
+
await self.table_cache.set(stats_key, stats)
|
|
163
|
+
|
|
164
|
+
self.logger.info(f"📊 Cache statistics updated in table cache: {stats.connection_status}")
|
|
165
|
+
return stats
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
self.logger.error(f"Error collecting cache statistics: {e}")
|
|
169
|
+
# Return minimal error stats (don't store errors)
|
|
170
|
+
error_stats = CacheStats()
|
|
171
|
+
error_stats.cache_type = "JSON (Error)"
|
|
172
|
+
error_stats.connection_status = f"Collection Error: {str(e)}"
|
|
173
|
+
error_stats.is_healthy = False
|
|
174
|
+
error_stats.errors = 1
|
|
175
|
+
return error_stats
|
|
176
|
+
|
|
177
|
+
def _format_statistics_message(self, stats: CacheStats) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Format cache statistics for user display.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
stats: CacheStats object with collected metrics
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Formatted statistics message
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
health_icon = "🟢" if stats.is_healthy else "🔴"
|
|
189
|
+
|
|
190
|
+
message = [
|
|
191
|
+
f"📊 *Cache Statistics Report*\n",
|
|
192
|
+
f"{health_icon} *Cache Status*: {stats.connection_status}\n",
|
|
193
|
+
f"⚙️ *Cache Type*: {stats.cache_type}\n",
|
|
194
|
+
f"📈 *Total Operations*: {stats.total_operations}\n",
|
|
195
|
+
f"❌ *Errors*: {stats.errors}\n",
|
|
196
|
+
f"🕐 *Last Updated*: {stats.last_updated.strftime('%H:%M:%S')}\n\n",
|
|
197
|
+
|
|
198
|
+
# Cache metrics
|
|
199
|
+
"*Cache Metrics:*\n",
|
|
200
|
+
f"• User Cache Hits: {stats.user_cache_hits}\n",
|
|
201
|
+
f"• User Cache Misses: {stats.user_cache_misses}\n",
|
|
202
|
+
f"• Table Entries: {stats.table_cache_entries}\n",
|
|
203
|
+
f"• Active States: {stats.state_cache_active}\n\n",
|
|
204
|
+
|
|
205
|
+
# Performance indicators
|
|
206
|
+
"*Performance:*\n",
|
|
207
|
+
f"• Health: {'🟢 Healthy' if stats.is_healthy else '🔴 Unhealthy'}\n",
|
|
208
|
+
f"• Connection: {stats.connection_status}\n\n",
|
|
209
|
+
|
|
210
|
+
# Tips for users
|
|
211
|
+
"*Available Commands:*\n",
|
|
212
|
+
"• `/HISTORY` - View message history\n",
|
|
213
|
+
"• `/WAPPA` - Enter WAPPA state\n",
|
|
214
|
+
"• `/EXIT` - Exit WAPPA state\n",
|
|
215
|
+
"• `/STATS` - View these statistics"
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
return "".join(message)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.logger.error(f"Error formatting statistics message: {e}")
|
|
222
|
+
return (
|
|
223
|
+
"📊 Cache Statistics\n\n"
|
|
224
|
+
f"❌ Error formatting statistics: {str(e)}\n\n"
|
|
225
|
+
"Please try again or contact support if the issue persists."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def get_cache_health(self) -> dict:
|
|
229
|
+
"""
|
|
230
|
+
Get cache health status (for other score modules).
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary with cache health information
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
stats = await self._collect_cache_statistics()
|
|
237
|
+
return {
|
|
238
|
+
'healthy': stats.is_healthy,
|
|
239
|
+
'status': stats.connection_status,
|
|
240
|
+
'cache_type': stats.cache_type
|
|
241
|
+
}
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.logger.error(f"Error getting cache health: {e}")
|
|
244
|
+
return {
|
|
245
|
+
'healthy': False,
|
|
246
|
+
'status': f'Health Check Error: {str(e)}',
|
|
247
|
+
'cache_type': 'Unknown'
|
|
248
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message History Score - Single Responsibility: Message logging and history management.
|
|
3
|
+
|
|
4
|
+
This module handles all message history operations including:
|
|
5
|
+
- Message logging to table cache
|
|
6
|
+
- Message history retrieval and formatting
|
|
7
|
+
- /HISTORY command processing
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ..models.json_demo_models import MessageLog
|
|
11
|
+
from ..utils.cache_utils import create_message_history_key, get_cache_ttl
|
|
12
|
+
from ..utils.message_utils import (
|
|
13
|
+
extract_command_from_message,
|
|
14
|
+
extract_user_data,
|
|
15
|
+
format_message_history_display,
|
|
16
|
+
sanitize_message_text,
|
|
17
|
+
)
|
|
18
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
19
|
+
|
|
20
|
+
from .score_base import ScoreBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MessageHistoryScore(ScoreBase):
|
|
24
|
+
"""
|
|
25
|
+
Handles message history logging and retrieval operations.
|
|
26
|
+
|
|
27
|
+
Follows Single Responsibility Principle by focusing only
|
|
28
|
+
on message history management.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
This score handles all messages for logging, plus /HISTORY command.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
webhook: Incoming message webhook
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Always True since all messages should be logged
|
|
40
|
+
"""
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Process message logging and handle /HISTORY command.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
webhook: Incoming message webhook
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if processing was successful
|
|
52
|
+
"""
|
|
53
|
+
if not await self.validate_dependencies():
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Always log the message first
|
|
58
|
+
await self._log_message(webhook)
|
|
59
|
+
|
|
60
|
+
# Check if this is a /HISTORY command
|
|
61
|
+
message_text = webhook.get_message_text()
|
|
62
|
+
if message_text:
|
|
63
|
+
command, _ = extract_command_from_message(message_text.strip())
|
|
64
|
+
if command == "/HISTORY":
|
|
65
|
+
await self._handle_history_request(webhook)
|
|
66
|
+
|
|
67
|
+
self._record_processing(success=True)
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
await self._handle_error(e, "message_history_processing")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
async def _log_message(self, webhook: IncomingMessageWebhook) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Log message to user's message history.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
webhook: Incoming message webhook
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
user_data = extract_user_data(webhook)
|
|
83
|
+
user_id = user_data['user_id']
|
|
84
|
+
tenant_id = user_data['tenant_id']
|
|
85
|
+
|
|
86
|
+
message_text = webhook.get_message_text()
|
|
87
|
+
message_type = webhook.get_message_type_name()
|
|
88
|
+
|
|
89
|
+
# Generate cache key using utility function
|
|
90
|
+
log_key = create_message_history_key(user_id)
|
|
91
|
+
|
|
92
|
+
# Try to get existing message history
|
|
93
|
+
message_log = await self.table_cache.get(log_key, models=MessageLog)
|
|
94
|
+
|
|
95
|
+
if message_log:
|
|
96
|
+
self.logger.debug(
|
|
97
|
+
f"📝 Found existing history for {user_id} "
|
|
98
|
+
f"({message_log.get_message_count()} messages)"
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
# Create new message history
|
|
102
|
+
message_log = MessageLog(
|
|
103
|
+
user_id=user_id,
|
|
104
|
+
tenant_id=tenant_id
|
|
105
|
+
)
|
|
106
|
+
self.logger.info(f"📝 Creating new message history for {user_id}")
|
|
107
|
+
|
|
108
|
+
# Add the new message to history
|
|
109
|
+
message_content = sanitize_message_text(
|
|
110
|
+
message_text or f"[{message_type.upper()} MESSAGE]"
|
|
111
|
+
)
|
|
112
|
+
message_log.add_message(message_content, message_type)
|
|
113
|
+
|
|
114
|
+
# Store back to JSON cache with TTL
|
|
115
|
+
ttl = get_cache_ttl('message')
|
|
116
|
+
await self.table_cache.set(log_key, message_log, ttl=ttl)
|
|
117
|
+
|
|
118
|
+
self.logger.info(
|
|
119
|
+
f"📝 Message logged: {user_id} "
|
|
120
|
+
f"(total: {message_log.get_message_count()} messages)"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.logger.error(f"Error logging message: {e}")
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
async def _handle_history_request(self, webhook: IncomingMessageWebhook) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Handle /HISTORY command to show user's message history.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
webhook: Incoming message webhook
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
user_data = extract_user_data(webhook)
|
|
136
|
+
user_id = user_data['user_id']
|
|
137
|
+
|
|
138
|
+
# Get user's message history
|
|
139
|
+
log_key = create_message_history_key(user_id)
|
|
140
|
+
message_log = await self.table_cache.get(log_key, models=MessageLog)
|
|
141
|
+
|
|
142
|
+
if message_log:
|
|
143
|
+
# User has message history
|
|
144
|
+
recent_messages = message_log.get_recent_messages(20)
|
|
145
|
+
total_count = message_log.get_message_count()
|
|
146
|
+
|
|
147
|
+
if recent_messages:
|
|
148
|
+
# Format history for display
|
|
149
|
+
history_text = format_message_history_display(
|
|
150
|
+
recent_messages, total_count, 20
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
history_text = "📚 Your message history is empty. Start chatting to build your history!"
|
|
154
|
+
else:
|
|
155
|
+
# No history found
|
|
156
|
+
history_text = "📚 No message history found. This is your first message! Welcome! 👋"
|
|
157
|
+
|
|
158
|
+
# Send history to user
|
|
159
|
+
result = await self.messenger.send_text(
|
|
160
|
+
recipient=user_id,
|
|
161
|
+
text=history_text,
|
|
162
|
+
reply_to_message_id=webhook.message.message_id
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if result.success:
|
|
166
|
+
self.logger.info(f"✅ History sent to {user_id}")
|
|
167
|
+
else:
|
|
168
|
+
self.logger.error(f"❌ Failed to send history: {result.error}")
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self.logger.error(f"Error handling history request: {e}")
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
async def get_message_count(self, user_id: str) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Get message count for user (for other score modules).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
user_id: User's phone number ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Number of messages from user, 0 if no history
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
log_key = create_message_history_key(user_id)
|
|
186
|
+
message_log = await self.table_cache.get(log_key, models=MessageLog)
|
|
187
|
+
return message_log.get_message_count() if message_log else 0
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.error(f"Error getting message count for {user_id}: {e}")
|
|
190
|
+
return 0
|