wappa 0.1.8__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.

Files changed (147) hide show
  1. wappa/__init__.py +4 -5
  2. wappa/api/controllers/webhook_controller.py +5 -2
  3. wappa/api/dependencies/__init__.py +0 -5
  4. wappa/api/middleware/error_handler.py +4 -4
  5. wappa/api/middleware/owner.py +11 -5
  6. wappa/api/routes/webhooks.py +2 -2
  7. wappa/cli/__init__.py +1 -1
  8. wappa/cli/examples/init/.env.example +33 -0
  9. wappa/cli/examples/init/app/__init__.py +0 -0
  10. wappa/cli/examples/init/app/main.py +9 -0
  11. wappa/cli/examples/init/app/master_event.py +10 -0
  12. wappa/cli/examples/json_cache_example/.env.example +33 -0
  13. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  14. wappa/cli/examples/json_cache_example/app/main.py +247 -0
  15. wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
  16. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  17. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
  18. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  19. wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
  20. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
  21. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
  22. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
  23. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
  24. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  25. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
  26. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
  27. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  28. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  29. wappa/cli/examples/openai_transcript/app/main.py +9 -0
  30. wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
  31. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  32. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
  33. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  34. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  35. wappa/cli/examples/redis_cache_example/app/main.py +246 -0
  36. wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
  37. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
  38. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  39. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
  40. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
  41. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
  42. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
  43. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
  44. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  45. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
  46. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
  47. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  48. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  49. wappa/cli/examples/simple_echo_example/app/main.py +191 -0
  50. wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
  51. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  52. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  53. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  54. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  55. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
  56. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
  57. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
  58. wappa/cli/examples/wappa_full_example/app/main.py +269 -0
  59. wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
  60. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  61. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  62. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  63. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  64. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  65. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  66. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  67. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  68. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  69. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  70. wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
  71. wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
  72. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
  73. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  74. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
  75. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
  76. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
  77. wappa/cli/main.py +14 -5
  78. wappa/core/__init__.py +18 -23
  79. wappa/core/config/settings.py +7 -5
  80. wappa/core/events/default_handlers.py +1 -1
  81. wappa/core/factory/wappa_builder.py +38 -25
  82. wappa/core/plugins/redis_plugin.py +1 -3
  83. wappa/core/plugins/wappa_core_plugin.py +7 -6
  84. wappa/core/types.py +12 -12
  85. wappa/core/wappa_app.py +10 -8
  86. wappa/database/__init__.py +3 -4
  87. wappa/domain/enums/messenger_platform.py +1 -2
  88. wappa/domain/factories/media_factory.py +5 -20
  89. wappa/domain/factories/message_factory.py +5 -20
  90. wappa/domain/factories/messenger_factory.py +2 -4
  91. wappa/domain/interfaces/cache_interface.py +7 -7
  92. wappa/domain/interfaces/media_interface.py +2 -5
  93. wappa/domain/models/media_result.py +1 -3
  94. wappa/domain/models/platforms/platform_config.py +1 -3
  95. wappa/messaging/__init__.py +9 -12
  96. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  97. wappa/models/__init__.py +27 -35
  98. wappa/persistence/__init__.py +12 -15
  99. wappa/persistence/cache_factory.py +0 -1
  100. wappa/persistence/json/__init__.py +1 -1
  101. wappa/persistence/json/cache_adapters.py +37 -25
  102. wappa/persistence/json/handlers/state_handler.py +60 -52
  103. wappa/persistence/json/handlers/table_handler.py +51 -49
  104. wappa/persistence/json/handlers/user_handler.py +71 -55
  105. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  106. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  107. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  108. wappa/persistence/json/json_cache_factory.py +4 -8
  109. wappa/persistence/json/storage_manager.py +66 -79
  110. wappa/persistence/memory/__init__.py +1 -1
  111. wappa/persistence/memory/cache_adapters.py +37 -25
  112. wappa/persistence/memory/handlers/state_handler.py +62 -52
  113. wappa/persistence/memory/handlers/table_handler.py +59 -53
  114. wappa/persistence/memory/handlers/user_handler.py +75 -55
  115. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  116. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  117. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  118. wappa/persistence/memory/memory_cache_factory.py +3 -7
  119. wappa/persistence/memory/storage_manager.py +52 -62
  120. wappa/persistence/redis/cache_adapters.py +27 -21
  121. wappa/persistence/redis/ops.py +11 -11
  122. wappa/persistence/redis/redis_client.py +4 -6
  123. wappa/persistence/redis/redis_manager.py +12 -4
  124. wappa/processors/factory.py +5 -5
  125. wappa/schemas/factory.py +2 -5
  126. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  127. wappa/schemas/whatsapp/validators.py +3 -3
  128. wappa/webhooks/__init__.py +17 -18
  129. wappa/webhooks/factory.py +3 -5
  130. wappa/webhooks/whatsapp/__init__.py +10 -13
  131. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  132. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  133. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  134. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  135. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  136. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  137. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  138. wappa/webhooks/whatsapp/status_models.py +2 -2
  139. wappa/webhooks/whatsapp/validators.py +3 -3
  140. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  141. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
  142. wappa/cli/examples/init/pyproject.toml +0 -7
  143. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  144. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  145. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  146. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  147. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,174 @@
1
+ """
2
+ Cache utility functions following Single Responsibility Principle.
3
+
4
+ This module provides cache-related helper functions used across
5
+ different score modules for consistent cache management.
6
+ """
7
+
8
+ import re
9
+
10
+
11
+ def generate_cache_key(prefix: str, identifier: str, suffix: str | None = None) -> str:
12
+ """
13
+ Generate a standardized cache key.
14
+
15
+ Args:
16
+ prefix: Cache type prefix (e.g., 'user', 'state', 'msg_history')
17
+ identifier: Unique identifier (e.g., user_id, session_id)
18
+ suffix: Optional suffix for additional specificity
19
+
20
+ Returns:
21
+ Properly formatted cache key
22
+
23
+ Examples:
24
+ >>> generate_cache_key('user', '1234567890')
25
+ 'user:1234567890'
26
+ >>> generate_cache_key('msg_history', '1234567890', 'recent')
27
+ 'msg_history:1234567890:recent'
28
+ """
29
+ if not prefix or not identifier:
30
+ raise ValueError("Both prefix and identifier are required")
31
+
32
+ # Sanitize inputs
33
+ prefix = sanitize_cache_component(prefix)
34
+ identifier = sanitize_cache_component(identifier)
35
+
36
+ key = f"{prefix}:{identifier}"
37
+
38
+ if suffix:
39
+ suffix = sanitize_cache_component(suffix)
40
+ key += f":{suffix}"
41
+
42
+ return key
43
+
44
+
45
+ def sanitize_cache_component(component: str) -> str:
46
+ """
47
+ Sanitize a cache key component by removing invalid characters.
48
+
49
+ Args:
50
+ component: Component string to sanitize
51
+
52
+ Returns:
53
+ Sanitized component safe for cache keys
54
+ """
55
+ # Remove spaces and special characters, keep alphanumeric, underscore, hyphen
56
+ sanitized = re.sub(r"[^\w\-]", "_", str(component).strip())
57
+
58
+ # Remove multiple consecutive underscores
59
+ sanitized = re.sub(r"_+", "_", sanitized)
60
+
61
+ # Remove leading/trailing underscores
62
+ return sanitized.strip("_")
63
+
64
+
65
+ def get_cache_ttl(cache_type: str) -> int:
66
+ """
67
+ Get standard TTL (time-to-live) values for different cache types.
68
+
69
+ Args:
70
+ cache_type: Type of cache ('user', 'state', 'message', 'statistics')
71
+
72
+ Returns:
73
+ TTL in seconds
74
+
75
+ Raises:
76
+ ValueError: If cache_type is not recognized
77
+ """
78
+ ttl_mapping = {
79
+ "user": 86400, # 24 hours - User profiles
80
+ "state": 3600, # 1 hour - Command states
81
+ "message": 604800, # 7 days - Message history
82
+ "statistics": 3600, # 1 hour - Cache statistics
83
+ "temporary": 600, # 10 minutes - Temporary data
84
+ }
85
+
86
+ if cache_type not in ttl_mapping:
87
+ raise ValueError(
88
+ f"Unknown cache_type: {cache_type}. Valid types: {list(ttl_mapping.keys())}"
89
+ )
90
+
91
+ return ttl_mapping[cache_type]
92
+
93
+
94
+ def validate_cache_key(key: str) -> bool:
95
+ """
96
+ Validate if a cache key follows the expected format.
97
+
98
+ Args:
99
+ key: Cache key to validate
100
+
101
+ Returns:
102
+ True if valid, False otherwise
103
+ """
104
+ if not key or not isinstance(key, str):
105
+ return False
106
+
107
+ # Basic format check: should contain at least one colon
108
+ if ":" not in key:
109
+ return False
110
+
111
+ # Check for invalid characters (spaces, special chars except : _ -)
112
+ if re.search(r"[^\w:\-]", key):
113
+ return False
114
+
115
+ # Should not start or end with colon
116
+ if key.startswith(":") or key.endswith(":"):
117
+ return False
118
+
119
+ # Should not have consecutive colons
120
+ return "::" not in key
121
+
122
+
123
+ def create_user_profile_key(user_id: str) -> str:
124
+ """Create standardized user profile cache key."""
125
+ return generate_cache_key("user", user_id, "profile")
126
+
127
+
128
+ def create_message_history_key(user_id: str) -> str:
129
+ """Create standardized message history cache key."""
130
+ return generate_cache_key("msg_history", user_id)
131
+
132
+
133
+ def create_state_key(user_id: str, state_type: str = "wappa") -> str:
134
+ """Create standardized state cache key."""
135
+ return generate_cache_key("state", user_id, state_type)
136
+
137
+
138
+ def create_statistics_key(scope: str = "global") -> str:
139
+ """Create standardized cache statistics key."""
140
+ return generate_cache_key("stats", scope)
141
+
142
+
143
+ def format_cache_error(operation: str, key: str, error: Exception) -> str:
144
+ """
145
+ Format cache operation error messages consistently.
146
+
147
+ Args:
148
+ operation: Cache operation ('get', 'set', 'delete')
149
+ key: Cache key that failed
150
+ error: Exception that occurred
151
+
152
+ Returns:
153
+ Formatted error message
154
+ """
155
+ return f"Cache {operation} failed for key '{key}': {str(error)}"
156
+
157
+
158
+ def log_cache_operation(
159
+ logger, operation: str, key: str, success: bool, duration_ms: float | None = None
160
+ ) -> None:
161
+ """
162
+ Log cache operations consistently across score modules.
163
+
164
+ Args:
165
+ logger: Logger instance
166
+ operation: Cache operation performed
167
+ key: Cache key used
168
+ success: Whether operation succeeded
169
+ duration_ms: Optional operation duration in milliseconds
170
+ """
171
+ status = "✅" if success else "❌"
172
+ duration_str = f" ({duration_ms:.1f}ms)" if duration_ms else ""
173
+
174
+ logger.debug(f"{status} Cache {operation}: {key}{duration_str}")
@@ -0,0 +1,251 @@
1
+ """
2
+ Message processing utility functions following Single Responsibility Principle.
3
+
4
+ This module provides message-related helper functions used across
5
+ different score modules for consistent message handling.
6
+ """
7
+
8
+ from datetime import datetime
9
+
10
+ from wappa.webhooks import IncomingMessageWebhook
11
+
12
+
13
+ def extract_user_data(webhook: IncomingMessageWebhook) -> dict[str, str]:
14
+ """
15
+ Extract user data from webhook in a standardized format.
16
+
17
+ Args:
18
+ webhook: Incoming message webhook
19
+
20
+ Returns:
21
+ Dictionary with standardized user data
22
+ """
23
+ return {
24
+ "user_id": webhook.user.user_id,
25
+ "user_name": webhook.user.profile_name or "Unknown User",
26
+ "tenant_id": webhook.tenant.get_tenant_key(),
27
+ "message_id": webhook.message.message_id,
28
+ }
29
+
30
+
31
+ def sanitize_message_text(text: str, max_length: int = 500) -> str:
32
+ """
33
+ Sanitize message text for safe storage and processing.
34
+
35
+ Args:
36
+ text: Raw message text
37
+ max_length: Maximum allowed length
38
+
39
+ Returns:
40
+ Sanitized message text
41
+ """
42
+ if not text:
43
+ return ""
44
+
45
+ # Convert to string and strip whitespace
46
+ sanitized = str(text).strip()
47
+
48
+ # Truncate if too long
49
+ if len(sanitized) > max_length:
50
+ sanitized = sanitized[: max_length - 3] + "..."
51
+
52
+ # Replace problematic characters
53
+ sanitized = sanitized.replace("\x00", "").replace("\r\n", "\n")
54
+
55
+ return sanitized
56
+
57
+
58
+ def format_timestamp(dt: datetime, format_type: str = "display") -> str:
59
+ """
60
+ Format timestamps consistently across the application.
61
+
62
+ Args:
63
+ dt: Datetime to format
64
+ format_type: Format type ('display', 'compact', 'iso')
65
+
66
+ Returns:
67
+ Formatted timestamp string
68
+ """
69
+ if format_type == "display":
70
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
71
+ elif format_type == "compact":
72
+ return dt.strftime("%m/%d %H:%M")
73
+ elif format_type == "iso":
74
+ return dt.isoformat()
75
+ else:
76
+ raise ValueError(f"Unknown format_type: {format_type}")
77
+
78
+
79
+ def extract_command_from_message(text: str) -> tuple[str | None, str]:
80
+ """
81
+ Extract command and remaining text from message.
82
+
83
+ Args:
84
+ text: Message text
85
+
86
+ Returns:
87
+ Tuple of (command, remaining_text)
88
+ Command is None if no command found
89
+
90
+ Examples:
91
+ >>> extract_command_from_message("/WAPPA hello")
92
+ ("/WAPPA", "hello")
93
+ >>> extract_command_from_message("regular message")
94
+ (None, "regular message")
95
+ """
96
+ if not text:
97
+ return None, ""
98
+
99
+ text = text.strip()
100
+
101
+ # Check if it starts with a command
102
+ if text.startswith("/"):
103
+ parts = text.split(" ", 1)
104
+ command = parts[0].upper()
105
+ remaining = parts[1] if len(parts) > 1 else ""
106
+ return command, remaining
107
+
108
+ return None, text
109
+
110
+
111
+ def is_special_command(text: str) -> bool:
112
+ """
113
+ Check if message text contains a special command.
114
+
115
+ Args:
116
+ text: Message text to check
117
+
118
+ Returns:
119
+ True if text contains a recognized command
120
+ """
121
+ command, _ = extract_command_from_message(text)
122
+
123
+ if not command:
124
+ return False
125
+
126
+ # List of recognized commands
127
+ special_commands = ["/WAPPA", "/EXIT", "/HISTORY", "/HELP", "/STATUS"]
128
+
129
+ return command in special_commands
130
+
131
+
132
+ def get_message_type_display_name(message_type: str) -> str:
133
+ """
134
+ Get human-readable display name for message types.
135
+
136
+ Args:
137
+ message_type: Technical message type
138
+
139
+ Returns:
140
+ Human-readable display name
141
+ """
142
+ type_mapping = {
143
+ "text": "Text",
144
+ "image": "Image",
145
+ "audio": "Audio",
146
+ "video": "Video",
147
+ "document": "Document",
148
+ "location": "Location",
149
+ "contacts": "Contact",
150
+ "interactive": "Interactive",
151
+ "button": "Button Response",
152
+ "list": "List Response",
153
+ "sticker": "Sticker",
154
+ }
155
+
156
+ return type_mapping.get(message_type.lower(), message_type.title())
157
+
158
+
159
+ def create_user_greeting(user_name: str | None, message_count: int) -> str:
160
+ """
161
+ Create personalized user greeting message.
162
+
163
+ Args:
164
+ user_name: User's display name (can be None)
165
+ message_count: Number of messages from user
166
+
167
+ Returns:
168
+ Personalized greeting text
169
+ """
170
+ name = user_name or "there"
171
+
172
+ if message_count == 1:
173
+ return f"👋 Hello {name}! Welcome to the Redis Cache Demo!"
174
+ elif message_count < 5:
175
+ return f"👋 Hello {name}! Nice to see you again!"
176
+ else:
177
+ return f"👋 Hello {name}! You're becoming a regular here! ({message_count} messages)"
178
+
179
+
180
+ def format_message_history_display(
181
+ messages, total_count: int, display_count: int = 20
182
+ ) -> str:
183
+ """
184
+ Format message history for display to user.
185
+
186
+ Args:
187
+ messages: List of MessageHistory objects
188
+ total_count: Total number of messages in history
189
+ display_count: Number of messages being displayed
190
+
191
+ Returns:
192
+ Formatted history text
193
+ """
194
+ if not messages:
195
+ return "📚 Your message history is empty. Start chatting to build your history!"
196
+
197
+ history_text = f"📚 Your Message History ({total_count} total messages):\n\n"
198
+
199
+ for i, msg_history in enumerate(messages, 1):
200
+ timestamp_str = format_timestamp(msg_history.timestamp, "compact")
201
+ msg_type = (
202
+ f"[{get_message_type_display_name(msg_history.message_type)}]"
203
+ if msg_history.message_type != "text"
204
+ else ""
205
+ )
206
+
207
+ # Truncate long messages for display
208
+ display_message = sanitize_message_text(msg_history.message, 50)
209
+
210
+ history_text += f"{i:2d}. {timestamp_str} {msg_type} {display_message}\n"
211
+
212
+ if total_count > display_count:
213
+ history_text += f"\n... showing last {display_count} of {total_count} messages"
214
+
215
+ return history_text
216
+
217
+
218
+ def create_cache_info_message(user_profile, cache_stats) -> str:
219
+ """
220
+ Create informational message about cache status.
221
+
222
+ Args:
223
+ user_profile: User profile data
224
+ cache_stats: Cache statistics data
225
+
226
+ Returns:
227
+ Formatted cache information message
228
+ """
229
+ info_lines = [
230
+ "👤 Your Profile:",
231
+ f"• Messages sent: {user_profile.message_count}",
232
+ f"• First seen: {format_timestamp(user_profile.first_seen, 'compact')}",
233
+ f"• Last seen: {format_timestamp(user_profile.last_seen, 'compact')}",
234
+ "",
235
+ "🎯 Special Commands:",
236
+ "• Send '/WAPPA' to enter special state",
237
+ "• Send '/EXIT' to leave special state",
238
+ "• Send '/HISTORY' to see your message history",
239
+ "",
240
+ "📊 Cache Statistics:",
241
+ f"• Total operations: {cache_stats.total_operations}",
242
+ f"• User cache hit rate: {cache_stats.get_user_hit_rate():.1%}",
243
+ f"• Active states: {cache_stats.state_cache_active}",
244
+ "",
245
+ "💾 This demo showcases Redis caching:",
246
+ "• User data cached in user_cache",
247
+ "• Message history stored in table_cache per user",
248
+ "• Commands tracked in state_cache",
249
+ ]
250
+
251
+ return "\n".join(info_lines)
@@ -1,10 +1,69 @@
1
- # Python-generated files
1
+ # Wappa
2
+ logs/
3
+
4
+ # Environment variables
5
+ .env
6
+ .env.local
7
+ .env.development
8
+ .env.production
9
+
10
+ # Python
2
11
  __pycache__/
3
- *.py[oc]
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
4
16
  build/
17
+ develop-eggs/
5
18
  dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
6
27
  wheels/
7
- *.egg-info
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
8
33
 
9
34
  # Virtual environments
10
- .venv
35
+ .venv/
36
+ venv/
37
+ ENV/
38
+ env/
39
+
40
+ # IDE
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # Logs
51
+ app/logs/
52
+ logs/
53
+ *.log
54
+
55
+ # Redis dumps
56
+ dump.rdb
57
+
58
+ # Temporary files
59
+ .tmp/
60
+ temp/
61
+ tmp/
62
+
63
+ # Coverage reports
64
+ htmlcov/
65
+ .tox/
66
+ .coverage
67
+ .coverage.*
68
+ .cache
69
+ .pytest_cache/
File without changes
@@ -0,0 +1,9 @@
1
+ from wappa import Wappa
2
+
3
+ from .master_event import TranscriptEventHandler
4
+
5
+ app = Wappa()
6
+ app.set_event_handler(TranscriptEventHandler())
7
+
8
+ if __name__ == "__main__":
9
+ app.run()
@@ -0,0 +1,62 @@
1
+ from openai import AsyncOpenAI
2
+
3
+ from wappa import WappaEventHandler
4
+ from wappa.core.config import settings
5
+ from wappa.core.logging import get_logger
6
+ from wappa.webhooks import IncomingMessageWebhook
7
+
8
+ from .openai_utils import AudioProcessingService
9
+
10
+ logger = get_logger("TranscriptEventHandler")
11
+
12
+
13
+ class TranscriptEventHandler(WappaEventHandler):
14
+ async def process_message(self, webhook: IncomingMessageWebhook):
15
+ message_type = webhook.get_message_type_name()
16
+
17
+ await self.messenger.mark_as_read(
18
+ webhook.message.message_id, webhook.user.user_id
19
+ )
20
+
21
+ if message_type == "audio":
22
+ audio_id = webhook.message.audio.id
23
+
24
+ openai_client = AsyncOpenAI(api_key=settings.openai_api_key)
25
+ audio_service = AudioProcessingService(openai_client)
26
+
27
+ # Option 1: Using tempfile context manager (automatic cleanup)
28
+ async with self.messenger.media_handler.download_media_tempfile(
29
+ audio_id
30
+ ) as audio_download:
31
+ if audio_download.success:
32
+ transcription = await audio_service.transcribe_audio(
33
+ audio_download.file_path
34
+ )
35
+ await self.messenger.send_text(
36
+ f"*Transcript:*\n\n{transcription}", webhook.user.user_id
37
+ )
38
+ logger.info(
39
+ f"Transcribed audio from temp file: {audio_download.file_path}"
40
+ )
41
+ else:
42
+ logger.error(f"Failed to download audio: {audio_download.error}")
43
+ await self.messenger.send_text(
44
+ "Sorry, I couldn't download the audio file.",
45
+ webhook.user.user_id,
46
+ )
47
+
48
+ # Option 2: Memory-only processing (no files created)
49
+ # Uncomment to use bytes-based processing instead:
50
+ # audio_bytes_result = await self.messenger.media_handler.get_media_as_bytes(audio_id)
51
+ # if audio_bytes_result.success:
52
+ # transcription = await audio_service.transcribe_audio(audio_bytes_result.file_data, "audio.ogg")
53
+ # await self.messenger.send_text(f"*Transcript:*\n\n{transcription}", webhook.user.user_id)
54
+ # logger.info(f"Transcribed audio from memory ({audio_bytes_result.file_size} bytes)")
55
+ # else:
56
+ # logger.error(f"Failed to download audio: {audio_bytes_result.error}")
57
+ # await self.messenger.send_text("Sorry, I couldn't download the audio file.", webhook.user.user_id)
58
+ else:
59
+ await self.messenger.send_text(
60
+ "*Hey Wapp@!*\n\nThis app only receives Audio, send a Voice Note for Transcript",
61
+ webhook.user.user_id,
62
+ )
@@ -0,0 +1,3 @@
1
+ from .audio_processing import AudioProcessingService
2
+
3
+ all = ["AudioProcessingService"]
@@ -0,0 +1,89 @@
1
+ # Removed direct config import
2
+ # from config.env import OPENAI_API_KEY
3
+ import io
4
+ from pathlib import Path
5
+ from typing import BinaryIO
6
+
7
+ from openai import AsyncOpenAI
8
+
9
+ from wappa.core.logging import get_logger # Corrected relative import
10
+
11
+ # Initialize logger with class name
12
+ logger = get_logger("AudioProcessingService")
13
+
14
+
15
+ class AudioProcessingService:
16
+ def __init__(self, async_openai_client: AsyncOpenAI):
17
+ # Accept api_key in init
18
+ self.async_openai_client = async_openai_client
19
+
20
+ async def transcribe_audio(
21
+ self, audio_source: str | Path | bytes | BinaryIO, filename: str = "audio"
22
+ ) -> str:
23
+ """
24
+ Transcribes the audio using OpenAI's speech-to-text API with the gpt-4o-mini-transcribe model.
25
+
26
+ Args:
27
+ audio_source: Can be:
28
+ - str/Path: Path to the audio file to transcribe
29
+ - bytes: Raw audio data
30
+ - BinaryIO: File-like object containing audio data
31
+ filename: Name for the audio (used for OpenAI API, especially for bytes/BinaryIO)
32
+
33
+ Returns:
34
+ The transcribed text.
35
+ """
36
+ try:
37
+ # Handle different input types
38
+ if isinstance(audio_source, str | Path):
39
+ # File path input (original behavior)
40
+ audio_path = Path(audio_source)
41
+ with audio_path.open("rb") as audio_file:
42
+ transcription = (
43
+ await self.async_openai_client.audio.transcriptions.create(
44
+ model="gpt-4o-mini-transcribe",
45
+ file=audio_file,
46
+ response_format="json",
47
+ )
48
+ )
49
+ logger.debug(f"Transcription successful for file: {audio_path}")
50
+
51
+ elif isinstance(audio_source, bytes):
52
+ # Bytes input - create BytesIO stream
53
+ audio_stream = io.BytesIO(audio_source)
54
+ audio_stream.name = filename # OpenAI API needs a filename attribute
55
+ transcription = (
56
+ await self.async_openai_client.audio.transcriptions.create(
57
+ model="gpt-4o-mini-transcribe",
58
+ file=audio_stream,
59
+ response_format="json",
60
+ )
61
+ )
62
+ logger.debug(
63
+ f"Transcription successful for bytes data ({len(audio_source)} bytes)"
64
+ )
65
+
66
+ else:
67
+ # File-like object input
68
+ # Ensure it has a name attribute for OpenAI API
69
+ if not hasattr(audio_source, "name"):
70
+ audio_source.name = filename
71
+ transcription = (
72
+ await self.async_openai_client.audio.transcriptions.create(
73
+ model="gpt-4o-mini-transcribe",
74
+ file=audio_source,
75
+ response_format="json",
76
+ )
77
+ )
78
+ logger.debug(
79
+ f"Transcription successful for file-like object: {getattr(audio_source, 'name', 'unknown')}"
80
+ )
81
+
82
+ return transcription.text
83
+
84
+ except FileNotFoundError:
85
+ logger.error(f"Audio file not found for transcription: {audio_source}")
86
+ raise
87
+ except Exception as e:
88
+ logger.error(f"Error during OpenAI transcription call: {e}", exc_info=True)
89
+ raise
@@ -0,0 +1,33 @@
1
+ # ================================================================
2
+ # WAPPA WHATSAPP FRAMEWORK CONFIGURATION
3
+ # ================================================================
4
+
5
+ # General Configuration
6
+ PORT=8000
7
+ TIME_ZONE=America/Bogota
8
+
9
+ # DEBUG or INFO or WARNING or ERROR or CRITICAL
10
+ LOG_LEVEL=DEBUG
11
+ LOG_DIR=./logs
12
+ ## Environment DEV or PROD
13
+ ENVIRONMENT=DEV
14
+
15
+ # WhatsApp Graph API
16
+ BASE_URL=https://graph.facebook.com/
17
+ API_VERSION=v23.0
18
+
19
+ # WhatsApp Business API Credentials
20
+ WP_ACCESS_TOKEN=
21
+ WP_PHONE_ID=
22
+ WP_BID=
23
+
24
+ # Webhook Configuration
25
+ WHATSAPP_WEBHOOK_VERIFY_TOKEN=
26
+
27
+ # Redis Configuration (Optional - uncomment to enable Redis persistence)
28
+ REDIS_URL=redis://localhost:6379/
29
+ REDIS_MAX_CONNECTIONS=64
30
+
31
+ # Optional: AI Tools
32
+ # OPENAI_API_KEY=
33
+