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.

Files changed (80) hide show
  1. wappa/cli/examples/init/.env.example +33 -0
  2. wappa/cli/examples/init/app/__init__.py +0 -0
  3. wappa/cli/examples/init/app/main.py +8 -0
  4. wappa/cli/examples/init/app/master_event.py +8 -0
  5. wappa/cli/examples/json_cache_example/.env.example +33 -0
  6. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  7. wappa/cli/examples/json_cache_example/app/main.py +235 -0
  8. wappa/cli/examples/json_cache_example/app/master_event.py +419 -0
  9. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  10. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +275 -0
  11. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  12. wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
  13. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
  14. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
  15. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
  16. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
  17. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  18. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
  19. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -0
  20. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  21. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  22. wappa/cli/examples/openai_transcript/app/main.py +8 -0
  23. wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
  24. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  25. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -0
  26. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  27. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  28. wappa/cli/examples/redis_cache_example/app/main.py +234 -0
  29. wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
  30. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
  31. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  32. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
  33. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
  34. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
  35. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
  36. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
  37. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  38. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
  39. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -0
  40. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  41. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  42. wappa/cli/examples/simple_echo_example/app/main.py +183 -0
  43. wappa/cli/examples/simple_echo_example/app/master_event.py +209 -0
  44. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  45. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  46. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  47. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  48. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +484 -0
  49. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
  50. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
  51. wappa/cli/examples/wappa_full_example/app/main.py +257 -0
  52. wappa/cli/examples/wappa_full_example/app/master_event.py +445 -0
  53. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  54. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  55. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  56. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  57. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  58. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  59. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  60. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  61. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  62. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  63. wappa/cli/examples/wappa_full_example/app/models/state_models.py +425 -0
  64. wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
  65. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
  66. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  67. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
  68. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
  69. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
  70. wappa/cli/main.py +8 -4
  71. wappa/core/config/settings.py +34 -2
  72. wappa/persistence/__init__.py +2 -2
  73. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
  74. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/RECORD +77 -13
  75. wappa/cli/examples/init/pyproject.toml +0 -7
  76. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  77. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  78. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
  79. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
  80. {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