wappa 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wappa might be problematic. Click here for more details.

Files changed (78) 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-0.1.8.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
  72. {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/RECORD +75 -11
  73. wappa/cli/examples/init/pyproject.toml +0 -7
  74. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  75. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  76. {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
  77. {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
  78. {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,246 @@
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
+ from typing import Dict, Optional, Tuple
10
+
11
+ from wappa.webhooks import IncomingMessageWebhook
12
+
13
+
14
+ def extract_user_data(webhook: IncomingMessageWebhook) -> Dict[str, str]:
15
+ """
16
+ Extract user data from webhook in a standardized format.
17
+
18
+ Args:
19
+ webhook: Incoming message webhook
20
+
21
+ Returns:
22
+ Dictionary with standardized user data
23
+ """
24
+ return {
25
+ 'user_id': webhook.user.user_id,
26
+ 'user_name': webhook.user.profile_name or "Unknown User",
27
+ 'tenant_id': webhook.tenant.get_tenant_key(),
28
+ 'message_id': webhook.message.message_id,
29
+ }
30
+
31
+
32
+ def sanitize_message_text(text: str, max_length: int = 500) -> str:
33
+ """
34
+ Sanitize message text for safe storage and processing.
35
+
36
+ Args:
37
+ text: Raw message text
38
+ max_length: Maximum allowed length
39
+
40
+ Returns:
41
+ Sanitized message text
42
+ """
43
+ if not text:
44
+ return ""
45
+
46
+ # Convert to string and strip whitespace
47
+ sanitized = str(text).strip()
48
+
49
+ # Truncate if too long
50
+ if len(sanitized) > max_length:
51
+ sanitized = sanitized[:max_length-3] + "..."
52
+
53
+ # Replace problematic characters
54
+ sanitized = sanitized.replace('\x00', '').replace('\r\n', '\n')
55
+
56
+ return sanitized
57
+
58
+
59
+ def format_timestamp(dt: datetime, format_type: str = 'display') -> str:
60
+ """
61
+ Format timestamps consistently across the application.
62
+
63
+ Args:
64
+ dt: Datetime to format
65
+ format_type: Format type ('display', 'compact', 'iso')
66
+
67
+ Returns:
68
+ Formatted timestamp string
69
+ """
70
+ if format_type == 'display':
71
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
72
+ elif format_type == 'compact':
73
+ return dt.strftime("%m/%d %H:%M")
74
+ elif format_type == 'iso':
75
+ return dt.isoformat()
76
+ else:
77
+ raise ValueError(f"Unknown format_type: {format_type}")
78
+
79
+
80
+ def extract_command_from_message(text: str) -> Tuple[Optional[str], str]:
81
+ """
82
+ Extract command and remaining text from message.
83
+
84
+ Args:
85
+ text: Message text
86
+
87
+ Returns:
88
+ Tuple of (command, remaining_text)
89
+ Command is None if no command found
90
+
91
+ Examples:
92
+ >>> extract_command_from_message("/WAPPA hello")
93
+ ("/WAPPA", "hello")
94
+ >>> extract_command_from_message("regular message")
95
+ (None, "regular message")
96
+ """
97
+ if not text:
98
+ return None, ""
99
+
100
+ text = text.strip()
101
+
102
+ # Check if it starts with a command
103
+ if text.startswith('/'):
104
+ parts = text.split(' ', 1)
105
+ command = parts[0].upper()
106
+ remaining = parts[1] if len(parts) > 1 else ""
107
+ return command, remaining
108
+
109
+ return None, text
110
+
111
+
112
+ def is_special_command(text: str) -> bool:
113
+ """
114
+ Check if message text contains a special command.
115
+
116
+ Args:
117
+ text: Message text to check
118
+
119
+ Returns:
120
+ True if text contains a recognized command
121
+ """
122
+ command, _ = extract_command_from_message(text)
123
+
124
+ if not command:
125
+ return False
126
+
127
+ # List of recognized commands
128
+ special_commands = ['/WAPPA', '/EXIT', '/HISTORY', '/HELP', '/STATUS']
129
+
130
+ return command in special_commands
131
+
132
+
133
+ def get_message_type_display_name(message_type: str) -> str:
134
+ """
135
+ Get human-readable display name for message types.
136
+
137
+ Args:
138
+ message_type: Technical message type
139
+
140
+ Returns:
141
+ Human-readable display name
142
+ """
143
+ type_mapping = {
144
+ 'text': 'Text',
145
+ 'image': 'Image',
146
+ 'audio': 'Audio',
147
+ 'video': 'Video',
148
+ 'document': 'Document',
149
+ 'location': 'Location',
150
+ 'contacts': 'Contact',
151
+ 'interactive': 'Interactive',
152
+ 'button': 'Button Response',
153
+ 'list': 'List Response',
154
+ 'sticker': 'Sticker',
155
+ }
156
+
157
+ return type_mapping.get(message_type.lower(), message_type.title())
158
+
159
+
160
+ def create_user_greeting(user_name: Optional[str], message_count: int) -> str:
161
+ """
162
+ Create personalized user greeting message.
163
+
164
+ Args:
165
+ user_name: User's display name (can be None)
166
+ message_count: Number of messages from user
167
+
168
+ Returns:
169
+ Personalized greeting text
170
+ """
171
+ name = user_name or "there"
172
+
173
+ if message_count == 1:
174
+ return f"👋 Hello {name}! Welcome to the Redis Cache Demo!"
175
+ elif message_count < 5:
176
+ return f"👋 Hello {name}! Nice to see you again!"
177
+ else:
178
+ return f"👋 Hello {name}! You're becoming a regular here! ({message_count} messages)"
179
+
180
+
181
+ def format_message_history_display(messages, total_count: int, display_count: int = 20) -> str:
182
+ """
183
+ Format message history for display to user.
184
+
185
+ Args:
186
+ messages: List of MessageHistory objects
187
+ total_count: Total number of messages in history
188
+ display_count: Number of messages being displayed
189
+
190
+ Returns:
191
+ Formatted history text
192
+ """
193
+ if not messages:
194
+ return "📚 Your message history is empty. Start chatting to build your history!"
195
+
196
+ history_text = f"📚 Your Message History ({total_count} total messages):\n\n"
197
+
198
+ for i, msg_history in enumerate(messages, 1):
199
+ timestamp_str = format_timestamp(msg_history.timestamp, 'compact')
200
+ msg_type = f"[{get_message_type_display_name(msg_history.message_type)}]" if msg_history.message_type != "text" else ""
201
+
202
+ # Truncate long messages for display
203
+ display_message = sanitize_message_text(msg_history.message, 50)
204
+
205
+ history_text += f"{i:2d}. {timestamp_str} {msg_type} {display_message}\n"
206
+
207
+ if total_count > display_count:
208
+ history_text += f"\n... showing last {display_count} of {total_count} messages"
209
+
210
+ return history_text
211
+
212
+
213
+ def create_cache_info_message(user_profile, cache_stats) -> str:
214
+ """
215
+ Create informational message about cache status.
216
+
217
+ Args:
218
+ user_profile: User profile data
219
+ cache_stats: Cache statistics data
220
+
221
+ Returns:
222
+ Formatted cache information message
223
+ """
224
+ info_lines = [
225
+ f"👤 Your Profile:",
226
+ f"• Messages sent: {user_profile.message_count}",
227
+ f"• First seen: {format_timestamp(user_profile.first_seen, 'compact')}",
228
+ f"• Last seen: {format_timestamp(user_profile.last_seen, 'compact')}",
229
+ "",
230
+ f"🎯 Special Commands:",
231
+ f"• Send '/WAPPA' to enter special state",
232
+ f"• Send '/EXIT' to leave special state",
233
+ f"• Send '/HISTORY' to see your message history",
234
+ "",
235
+ f"📊 Cache Statistics:",
236
+ f"• Total operations: {cache_stats.total_operations}",
237
+ f"• User cache hit rate: {cache_stats.get_user_hit_rate():.1%}",
238
+ f"• Active states: {cache_stats.state_cache_active}",
239
+ "",
240
+ f"💾 This demo showcases Redis caching:",
241
+ f"• User data cached in user_cache",
242
+ f"• Message history stored in table_cache per user",
243
+ f"• Commands tracked in state_cache"
244
+ ]
245
+
246
+ 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,8 @@
1
+ from wappa import Wappa
2
+ from .master_event import TranscriptEventHandler
3
+
4
+ app = Wappa()
5
+ app.set_event_handler(TranscriptEventHandler())
6
+
7
+ if __name__ == "__main__":
8
+ app.run()
@@ -0,0 +1,53 @@
1
+ import tempfile
2
+ import os
3
+
4
+ from wappa import WappaEventHandler
5
+ from wappa.webhooks import IncomingMessageWebhook
6
+ from wappa.core.logging import get_logger
7
+ from wappa.core.config import settings
8
+
9
+ from openai import AsyncOpenAI
10
+
11
+ from .openai_utils import AudioProcessingService
12
+
13
+
14
+
15
+ logger = get_logger("TranscriptEventHandler")
16
+
17
+ class TranscriptEventHandler(WappaEventHandler):
18
+
19
+ async def process_message(self, webhook: IncomingMessageWebhook):
20
+
21
+
22
+ message_type = webhook.get_message_type_name()
23
+
24
+ await self.messenger.mark_as_read(webhook.message.message_id, webhook.user.user_id)
25
+
26
+ if message_type == "audio":
27
+ audio_id = webhook.message.audio.id
28
+
29
+ openai_client = AsyncOpenAI(api_key=settings.openai_api_key)
30
+ audio_service = AudioProcessingService(openai_client)
31
+
32
+ # Option 1: Using tempfile context manager (automatic cleanup)
33
+ async with self.messenger.media_handler.download_media_tempfile(audio_id) as audio_download:
34
+ if audio_download.success:
35
+ transcription = await audio_service.transcribe_audio(audio_download.file_path)
36
+ await self.messenger.send_text(f"*Transcript:*\n\n{transcription}", webhook.user.user_id)
37
+ logger.info(f"Transcribed audio from temp file: {audio_download.file_path}")
38
+ else:
39
+ logger.error(f"Failed to download audio: {audio_download.error}")
40
+ await self.messenger.send_text("Sorry, I couldn't download the audio file.", webhook.user.user_id)
41
+
42
+ # Option 2: Memory-only processing (no files created)
43
+ # Uncomment to use bytes-based processing instead:
44
+ # audio_bytes_result = await self.messenger.media_handler.get_media_as_bytes(audio_id)
45
+ # if audio_bytes_result.success:
46
+ # transcription = await audio_service.transcribe_audio(audio_bytes_result.file_data, "audio.ogg")
47
+ # await self.messenger.send_text(f"*Transcript:*\n\n{transcription}", webhook.user.user_id)
48
+ # logger.info(f"Transcribed audio from memory ({audio_bytes_result.file_size} bytes)")
49
+ # else:
50
+ # logger.error(f"Failed to download audio: {audio_bytes_result.error}")
51
+ # await self.messenger.send_text("Sorry, I couldn't download the audio file.", webhook.user.user_id)
52
+ else:
53
+ await self.messenger.send_text("*Hey Wapp@!*\n\nThis app only receives Audio, send a Voice Note for Transcript", webhook.user.user_id)
@@ -0,0 +1,3 @@
1
+ from .audio_processing import AudioProcessingService
2
+
3
+ all = ["AudioProcessingService"]
@@ -0,0 +1,76 @@
1
+ # Removed direct config import
2
+ # from config.env import OPENAI_API_KEY
3
+ from wappa.core.logging import get_logger # Corrected relative import
4
+ from pathlib import Path
5
+ from typing import Union, BinaryIO
6
+ import io
7
+
8
+ from openai import AsyncOpenAI
9
+
10
+ # Initialize logger with class name
11
+ logger = get_logger("AudioProcessingService")
12
+
13
+
14
+ class AudioProcessingService:
15
+ def __init__(self, async_openai_client: AsyncOpenAI):
16
+ # Accept api_key in init
17
+ self.async_openai_client = async_openai_client
18
+
19
+ async def transcribe_audio(self, audio_source: Union[str, Path, bytes, BinaryIO], filename: str = "audio") -> str:
20
+ """
21
+ Transcribes the audio using OpenAI's speech-to-text API with the gpt-4o-mini-transcribe model.
22
+
23
+ Args:
24
+ audio_source: Can be:
25
+ - str/Path: Path to the audio file to transcribe
26
+ - bytes: Raw audio data
27
+ - BinaryIO: File-like object containing audio data
28
+ filename: Name for the audio (used for OpenAI API, especially for bytes/BinaryIO)
29
+
30
+ Returns:
31
+ The transcribed text.
32
+ """
33
+ try:
34
+ # Handle different input types
35
+ if isinstance(audio_source, (str, Path)):
36
+ # File path input (original behavior)
37
+ audio_path = Path(audio_source)
38
+ with audio_path.open("rb") as audio_file:
39
+ transcription = await self.async_openai_client.audio.transcriptions.create(
40
+ model="gpt-4o-mini-transcribe",
41
+ file=audio_file,
42
+ response_format="json",
43
+ )
44
+ logger.debug(f"Transcription successful for file: {audio_path}")
45
+
46
+ elif isinstance(audio_source, bytes):
47
+ # Bytes input - create BytesIO stream
48
+ audio_stream = io.BytesIO(audio_source)
49
+ audio_stream.name = filename # OpenAI API needs a filename attribute
50
+ transcription = await self.async_openai_client.audio.transcriptions.create(
51
+ model="gpt-4o-mini-transcribe",
52
+ file=audio_stream,
53
+ response_format="json",
54
+ )
55
+ logger.debug(f"Transcription successful for bytes data ({len(audio_source)} bytes)")
56
+
57
+ else:
58
+ # File-like object input
59
+ # Ensure it has a name attribute for OpenAI API
60
+ if not hasattr(audio_source, 'name'):
61
+ audio_source.name = filename
62
+ transcription = await self.async_openai_client.audio.transcriptions.create(
63
+ model="gpt-4o-mini-transcribe",
64
+ file=audio_source,
65
+ response_format="json",
66
+ )
67
+ logger.debug(f"Transcription successful for file-like object: {getattr(audio_source, 'name', 'unknown')}")
68
+
69
+ return transcription.text
70
+
71
+ except FileNotFoundError:
72
+ logger.error(f"Audio file not found for transcription: {audio_source}")
73
+ raise
74
+ except Exception as e:
75
+ logger.error(f"Error during OpenAI transcription call: {e}", exc_info=True)
76
+ 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
+
@@ -0,0 +1,6 @@
1
+ """
2
+ Redis Cache Example App Module
3
+
4
+ This package contains the application code for the Redis Cache Example,
5
+ demonstrating SOLID architecture principles with the Wappa framework.
6
+ """