wappa 0.1.0__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 (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,699 @@
1
+ # mimieapify/symphony_ai/redis/README.md
2
+ # Redis Module - Multi-Pool Clean Architecture
3
+
4
+ This module provides Redis operations with clean separation of concerns, following SOLID principles. The Redis Handler package has been refactored from a monolithic 754-line God class into focused, single-responsibility repositories with **multi-pool support** for different subsystems.
5
+
6
+ ## 🏗️ Multi-Pool Architecture
7
+
8
+ The Redis module now supports multiple Redis pools targeting different databases for optimal separation of concerns:
9
+
10
+ | Pool Alias | Database | Purpose |
11
+ |------------|----------|---------|
12
+ | `"default"` | DB 15 | General operations |
13
+ | `"user"` | DB 11 | User-specific data |
14
+ | `"handlers"` | DB 10 | TTL-based handlers, batch, state, table operations |
15
+ | `"symphony_shared_state"` | DB 9 | Shared state between tools/agents |
16
+ | `"expiry"` | DB 8 | Key-expiry listener |
17
+ | `"pubsub"` | DB 7 | AsyncSendMessage pub/sub |
18
+
19
+ ### Configuration Options
20
+
21
+ ```python
22
+ from mimeiapify.symphony_ai import GlobalSymphonyConfig
23
+
24
+ # Option 1: Single URL (automatic pool creation)
25
+ config = GlobalSymphonyConfig(
26
+ redis_url="redis://localhost:6379"
27
+ # Automatically creates all pools with different database numbers
28
+ )
29
+
30
+ # Option 2: Multi-URL (explicit control)
31
+ config = GlobalSymphonyConfig(
32
+ redis_url={
33
+ "default": "redis://localhost:6379/15",
34
+ "user": "redis://cache:6379/11",
35
+ "handlers": "redis://localhost:6379/10",
36
+ "symphony_shared_state": "redis://localhost:6379/9",
37
+ "expiry": "redis://localhost:6379/8",
38
+ "pubsub": "redis://localhost:6379/7"
39
+ }
40
+ )
41
+
42
+ await GlobalSymphony.create(config)
43
+ ```
44
+
45
+ ## 🏗️ Module Structure
46
+
47
+ ```
48
+ redis/
49
+ ├── __init__.py # Main module exports
50
+ ├── context.py # ContextVar for thread-safe shared state
51
+ ├── redis_client.py # Multi-pool Redis connection management
52
+ ├── ops.py # Low-level atomic Redis operations (with pool alias support)
53
+ ├── README.md # This file
54
+ ├── listeners/ # Key-expiry trigger system
55
+ │ ├── __init__.py # Expiry listener exports
56
+ │ ├── handler_registry.py # Action → handler mapping with decorators
57
+ │ ├── expiry_listener.py # Redis keyspace event subscriber
58
+ │ ├── example_handlers.py # Example trigger handlers
59
+ │ └── README.md # Complete expiry trigger documentation
60
+ └── redis_handler/ # Repository layer
61
+ ├── __init__.py # Repository exports
62
+ ├── utils/ # Infrastructure & utilities
63
+ │ ├── __init__.py # Utils exports
64
+ │ ├── key_factory.py # Stateless key building rules
65
+ │ ├── serde.py # JSON/Enum/DateTime/BaseModel serialization
66
+ │ └── tenant_cache.py # Base class with common Redis patterns + alias support
67
+ ├── user.py # User data management → RedisUser (uses "user" pool)
68
+ ├── shared_state.py # Tool/agent scratch space → RedisSharedState (uses "symphony_shared_state" pool)
69
+ ├── state_handler.py # Handler state management → RedisStateHandler (uses "handlers" pool)
70
+ ├── table.py # Table/DataFrame operations → RedisTable (uses "handlers" pool)
71
+ ├── batch.py # Batch processing → RedisBatch (uses "handlers" pool)
72
+ ├── trigger.py # Expiration triggers → RedisTrigger (uses "expiry" pool)
73
+ └── generic.py # Generic key-value ops → RedisGeneric (uses "default" pool)
74
+ ```
75
+
76
+ ## 🚀 Quick Start
77
+
78
+ ### Basic Repository Usage with Pool Targeting
79
+
80
+ ```python
81
+ from mimeiapify.symphony_ai.redis.redis_handler import (
82
+ RedisUser, RedisStateHandler, RedisTable, RedisSharedState
83
+ )
84
+
85
+ # Initialize repositories - each targets its designated Redis pool automatically
86
+ user = RedisUser(tenant="mimeia", user_id="user123", ttl_default=3600) # → "user" pool (DB 11)
87
+ handler = RedisStateHandler(tenant="mimeia", user_id="user123", ttl_default=1800) # → "handlers" pool (DB 10)
88
+ tables = RedisTable(tenant="mimeia") # → "handlers" pool (DB 10)
89
+ shared_state = RedisSharedState(tenant="mimeia", user_id="user123") # → "symphony_shared_state" pool (DB 9)
90
+
91
+ # SQL-style operations - upsert for hash operations, set for simple key-value
92
+ await user.upsert({"name": "Alice", "score": 100}) # HSET → updates only specified fields
93
+ user_data = await user.get()
94
+ await user.update_field("score", 110) # Single field update
95
+
96
+ # Handler state management
97
+ await handler.upsert("chat_handler", {"step": 1, "data": {...}}) # HSET → field-level updates
98
+ state = await handler.get("chat_handler")
99
+ await handler.update_field("chat_handler", "step", 2)
100
+
101
+ # True merge operations (reads existing + merges + saves)
102
+ final_state = await handler.merge("chat_handler", {"new_field": "value"})
103
+
104
+ # Table operations
105
+ await tables.upsert("users_table", "pk123", {"name": "Bob", "active": True}) # HSET
106
+ row = await tables.get("users_table", "pk123")
107
+ await tables.update_field("users_table", "pk123", "active", False)
108
+
109
+ # Shared state for tools/agents
110
+ await shared_state.upsert("conversation", {"step": 1, "context": "greeting"}) # HSET
111
+ step = await shared_state.get_field("conversation", "step")
112
+ ```
113
+
114
+ ### Direct Pool Targeting (Advanced)
115
+
116
+ ```python
117
+ from mimeiapify.symphony_ai.redis import ops
118
+
119
+ # All ops functions now accept an alias parameter for pool targeting
120
+ await ops.set("key", "value", alias="pubsub") # → pubsub pool (DB 7)
121
+ await ops.hset("hash_key", field="name", value="Alice", alias="user") # → user pool (DB 11)
122
+ await ops.setex("temp_key", 300, "temp_value", alias="expiry") # → expiry pool (DB 8)
123
+
124
+ # Repository methods can override their default pool if needed
125
+ user = RedisUser(tenant="mimeia", user_id="user123")
126
+ await user.upsert({"name": "Alice"}) # → Uses default "user" pool
127
+ await user._hset_with_ttl(user._key(), {"temp": "data"}, 60, alias="expiry") # → Override to "expiry" pool
128
+ ```
129
+
130
+ ### Context-Aware Shared State (Thread-Safe)
131
+
132
+ The `context.py` module provides thread-safe access to shared state using Python's `ContextVar`:
133
+
134
+ ```python
135
+ from mimeiapify.symphony_ai.redis.context import _current_ss, RedisSharedState
136
+ from mimeiapify.symphony_ai import GlobalSymphony
137
+ import asyncio
138
+
139
+ # In your FastAPI handler or async function
140
+ async def handle_user_request(tenant: str, user_id: str, message: str):
141
+ # Create user-specific shared state (automatically uses "symphony_shared_state" pool)
142
+ ss = RedisSharedState(tenant=tenant, user_id=user_id)
143
+
144
+ # Bind to current context (task-local)
145
+ token = _current_ss.set(ss)
146
+ try:
147
+ # Any code running in this context (including tools in thread pools)
148
+ # will see this specific shared state instance
149
+ await process_user_message(message)
150
+ finally:
151
+ _current_ss.reset(token) # Always cleanup
152
+
153
+ # Tools can access the context-bound shared state
154
+ from mimeiapify.symphony_ai.redis.context import _current_ss
155
+
156
+ class SomeAsyncTool:
157
+ async def execute(self):
158
+ # Gets the shared state bound to current request context
159
+ shared_state = _current_ss.get()
160
+ await shared_state.update_field("tool_state", "last_tool", "SomeAsyncTool")
161
+
162
+ # For synchronous tools (like agency-swarm BaseTool)
163
+ class SomeSyncTool:
164
+ def run(self):
165
+ shared_state = _current_ss.get()
166
+ loop = GlobalSymphony.get().loop
167
+
168
+ # Bridge to async world
169
+ coro = shared_state.update_field("tool_state", "last_tool", "SomeSyncTool")
170
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
171
+ return future.result(timeout=5)
172
+ ```
173
+
174
+ ### TTL-Driven Workflows (Expiry Triggers)
175
+
176
+ The `listeners` module provides a powerful system for turning Redis TTLs into background jobs:
177
+
178
+ ```python
179
+ from mimeiapify.symphony_ai.redis.listeners import expiration_registry, run_expiry_listener
180
+ from mimeiapify.symphony_ai.redis.redis_handler import RedisTrigger
181
+
182
+ # 1. Register handlers for expiry events
183
+ @expiration_registry.on_expire_action("process_message_batch")
184
+ async def handle_batch_processing(identifier: str, full_key: str):
185
+ tenant = full_key.split(":", 1)[0]
186
+ logger.info(f"[{tenant}] Processing batch for: {identifier}")
187
+ # Your batch processing logic here...
188
+
189
+ @expiration_registry.on_expire_action("send_reminder")
190
+ async def handle_reminder(user_id: str, full_key: str):
191
+ # Send delayed notification
192
+ await send_notification(user_id, "Don't forget to complete your task!")
193
+
194
+ # 2. Start the listener (in FastAPI lifespan or similar)
195
+ asyncio.create_task(run_expiry_listener(alias="expiry"))
196
+
197
+ # 3. Schedule deferred work from your application
198
+ triggers = RedisTrigger(tenant="mimeia")
199
+
200
+ # Schedule batch processing for 5 minutes later
201
+ await triggers.set("process_message_batch", "wa_123", ttl_seconds=300)
202
+
203
+ # Schedule reminder for 1 hour later
204
+ await triggers.set("send_reminder", "user456", ttl_seconds=3600)
205
+
206
+ # Cancel scheduled work if no longer needed
207
+ await triggers.delete("process_message_batch", "wa_123")
208
+ ```
209
+
210
+ **See `redis/listeners/README.md` for complete documentation with architecture diagrams.**
211
+
212
+ ## 📋 Repository Responsibilities & Pool Assignments
213
+
214
+ ### RedisUser - User Data Management (`"user"` pool - DB 11)
215
+ ```python
216
+ user = RedisUser(tenant="your_tenant", user_id="user123")
217
+
218
+ # SQL-style CRUD operations using HSET (field-level updates)
219
+ await user.upsert({"name": "Alice", "active": True}) # Updates only specified fields
220
+ user_data = await user.get()
221
+ await user.update_field("last_login", datetime.now()) # Single field update
222
+ await user.delete()
223
+
224
+ # Atomic operations
225
+ await user.increment_field("login_count")
226
+ await user.append_to_list("tags", "premium")
227
+
228
+ # Field-level access
229
+ name = await user.get_field("name")
230
+ await user.delete_field("temp_data")
231
+
232
+ # Search (pattern matching across users)
233
+ found_user = await user.find_by_field("email", "alice@example.com")
234
+
235
+ # Check existence
236
+ exists = await user.exists()
237
+ ```
238
+
239
+ ### RedisStateHandler - Conversational State (`"handlers"` pool - DB 10)
240
+ ```python
241
+ handler = RedisStateHandler(tenant="your_tenant", user_id="user123")
242
+
243
+ # State management using HSET (field-level updates)
244
+ await handler.upsert("chat_handler", {"step": 1, "data": {...}}) # Updates only specified fields
245
+ state = await handler.get("chat_handler")
246
+ await handler.update_field("chat_handler", "step", 2)
247
+ current_step = await handler.get_field("chat_handler", "step")
248
+
249
+ # True merge operations (preserves existing state)
250
+ final_state = await handler.merge("chat_handler", {"new_data": "value"}) # Reads + merges + saves
251
+ ```
252
+
253
+ ### RedisTable - Generic Data Tables (`"handlers"` pool - DB 10)
254
+ ```python
255
+ tables = RedisTable(tenant="your_tenant")
256
+
257
+ # Table row operations using HSET (field-level updates)
258
+ await tables.upsert("products", "prod_123", {"name": "Widget", "price": 29.99})
259
+ product = await tables.get("products", "prod_123")
260
+ await tables.update_field("products", "prod_123", "price", 19.99)
261
+ price = await tables.get_field("products", "prod_123", "price")
262
+
263
+ # Cross-table cleanup
264
+ await tables.delete_all_by_pkid("user_123") # Deletes from all tables
265
+ ```
266
+
267
+ ### RedisSharedState - Tool/Agent Scratch Space (`"symphony_shared_state"` pool - DB 9)
268
+ ```python
269
+ shared_state = RedisSharedState(tenant="your_tenant", user_id="user123")
270
+
271
+ # Store conversation state for tools/agents using HSET
272
+ await shared_state.upsert("conversation", {
273
+ "step": 1,
274
+ "context": "user_greeting",
275
+ "collected_data": {"name": "Alice"}
276
+ })
277
+
278
+ # Update specific fields
279
+ await shared_state.update_field("conversation", "step", 2)
280
+ await shared_state.update_field("conversation", "last_tool", "email_validator")
281
+
282
+ # Retrieve state data
283
+ current_step = await shared_state.get_field("conversation", "step")
284
+ full_state = await shared_state.get("conversation")
285
+
286
+ # Manage multiple states per user
287
+ await shared_state.upsert("form_progress", {"page": 1, "completed_fields": []})
288
+ await shared_state.upsert("tool_cache", {"last_api_call": datetime.now()})
289
+
290
+ # Cleanup operations
291
+ states = await shared_state.list_states() # ["conversation", "form_progress", "tool_cache"]
292
+ await shared_state.delete("form_progress")
293
+ await shared_state.clear_all_states() # Delete all states for this user
294
+ ```
295
+
296
+ ### RedisTrigger - Expiration-based Actions (`"expiry"` pool - DB 8)
297
+ ```python
298
+ triggers = RedisTrigger(tenant="your_tenant")
299
+
300
+ # Set expiration triggers using SETEX (simple key-value with TTL)
301
+ await triggers.set("send_reminder", "user_123", ttl_seconds=3600) # 1 hour
302
+ await triggers.set("cleanup_temp", "session_456", ttl_seconds=300) # 5 minutes
303
+
304
+ # Cleanup
305
+ await triggers.delete("send_reminder", "user_123")
306
+ await triggers.delete_all_by_identifier("user_123")
307
+ ```
308
+
309
+ ### RedisBatch - Queue Management (`"handlers"` pool - DB 10)
310
+ ```python
311
+ batch = RedisBatch(tenant="your_tenant")
312
+
313
+ # Enqueue for processing (uses RPUSH + SADD)
314
+ await batch.enqueue("email_service", "daily_reports", "send", {
315
+ "user_id": "123", "template": "daily_summary"
316
+ })
317
+
318
+ # Process batches
319
+ items = await batch.get_chunk("email_service", "daily_reports", "send", 0, 99)
320
+ await batch.trim("email_service", "daily_reports", "send", 100, -1)
321
+
322
+ # Global coordination (class methods - no tenant)
323
+ pending_tenants = await RedisBatch.get_pending_tenants("email_service")
324
+ await RedisBatch.remove_from_pending("email_service", "mimeia")
325
+ ```
326
+
327
+ ### RedisGeneric - Simple Key-Value Operations (`"default"` pool - DB 15)
328
+ ```python
329
+ generic = RedisGeneric(tenant="your_tenant")
330
+
331
+ # Simple key-value operations using SET (complete value replacement)
332
+ await generic.set("config_key", {"theme": "dark", "lang": "en"}) # Replaces entire value
333
+ config = await generic.get("config_key")
334
+ await generic.delete("config_key")
335
+
336
+ # Note: No field operations since this uses simple SET, not HSET
337
+ ```
338
+
339
+ ## 🎯 SQL-Style Method Naming Convention
340
+
341
+ The system now uses **SQL-style naming** that reflects the underlying Redis operation:
342
+
343
+ ### Hash Operations (HSET) - Field-Level Updates
344
+ - `upsert(data_dict)` - Create or update multiple fields (Redis HSET behavior)
345
+ - `update_field(field, value)` - Update single field
346
+ - `get()` / `get_field(field)` - Retrieve operations
347
+ - `delete()` / `delete_field(field)` - Delete operations
348
+
349
+ ### Simple Key-Value Operations (SET) - Complete Replacement
350
+ - `set(value)` - Replace entire value (Redis SET behavior)
351
+ - `get()` - Retrieve value
352
+ - `delete()` - Delete key
353
+
354
+ ### Special Operations
355
+ - `merge(data_dict)` - True merge (read existing + merge + save) - StateHandler only
356
+ - `increment_field(field, amount)` - Atomic increment (HINCRBY)
357
+ - `append_to_list(field, value)` - Append to list field
358
+ - `exists()` - Check existence
359
+
360
+ ## 🔧 Pydantic BaseModel Support
361
+
362
+ The system provides full support for Pydantic models with optimized boolean storage (`"1"`/`"0"` instead of `true`/`false`).
363
+
364
+ ### Define Your Models
365
+
366
+ ```python
367
+ from pydantic import BaseModel
368
+ from typing import List
369
+
370
+ class UserProfile(BaseModel):
371
+ name: str
372
+ active: bool
373
+ score: int
374
+ preferences: List[bool]
375
+
376
+ class UserSettings(BaseModel):
377
+ notifications: bool
378
+ theme: str = "dark"
379
+ auto_save: bool = True
380
+ ```
381
+
382
+ ### Typed Operations
383
+
384
+ ```python
385
+ # Store BaseModel directly
386
+ profile = UserProfile(name="Alice", active=True, score=100, preferences=[True, False])
387
+ await user.update_field("profile", profile)
388
+
389
+ # Retrieve with automatic typing
390
+ models = {
391
+ "profile": UserProfile,
392
+ "settings": UserSettings
393
+ }
394
+ user_data = await user.get(models=models)
395
+ # user_data["profile"] is now a UserProfile instance
396
+ # user_data["profile"].active is bool, not string
397
+
398
+ # Search with typed results
399
+ found_user = await user.find_by_field("active", True, models=models)
400
+
401
+ # Handler state with context models
402
+ handler_models = {"context": ConversationContext}
403
+ state = await handler.get("chat_handler", models=handler_models)
404
+ ```
405
+
406
+ ## 🏗️ Architecture Improvements
407
+
408
+ ### Multi-Pool Benefits
409
+ - **🔒 Data Isolation**: Different subsystems use separate Redis databases
410
+ - **📈 Performance**: Targeted pool usage reduces connection contention
411
+ - **🛡️ Fault Tolerance**: Issues in one pool don't affect others
412
+ - **🔧 Maintenance**: Database-specific operations (FLUSHDB, monitoring)
413
+ - **📊 Monitoring**: Per-pool metrics and alerting
414
+
415
+ ### TenantCache Enhancement
416
+ All repository classes inherit from `TenantCache` which now supports:
417
+ - **Default pool assignment**: Each repository targets its designated pool
418
+ - **Pool override capability**: Methods accept optional `alias` parameter
419
+ - **Inherited method priority**: Uses `_hset_with_ttl()`, `_get_hash()`, etc. before direct ops
420
+ - **Consistent error handling**: Unified logging and fallback behavior
421
+
422
+ ### Operations Layer (ops.py)
423
+ All Redis operations now support pool targeting:
424
+ ```python
425
+ # Every ops function accepts alias parameter
426
+ await ops.set("key", "value", alias="pubsub")
427
+ await ops.hget("hash", "field", alias="user")
428
+ await ops.scan_keys("pattern*", alias="handlers")
429
+ ```
430
+
431
+ ## 🏭 Production Implementation with GlobalSymphony
432
+
433
+ ### 1. FastAPI Integration with Multi-Pool Context
434
+
435
+ ```python
436
+ from mimeiapify.symphony_ai.redis.context import _current_ss, RedisSharedState
437
+ from mimeiapify.symphony_ai import GlobalSymphony
438
+ from mimeiapify.symphony_ai.redis.redis_handler import RedisUser, RedisStateHandler
439
+ from fastapi import FastAPI, Request, Depends
440
+ from contextlib import asynccontextmanager
441
+
442
+ @asynccontextmanager
443
+ async def lifespan(app: FastAPI):
444
+ # Initialize GlobalSymphony with multi-pool Redis
445
+ from mimeiapify.symphony_ai import GlobalSymphonyConfig
446
+
447
+ config = GlobalSymphonyConfig(
448
+ # Option 1: Single URL with auto-pool creation
449
+ redis_url="redis://localhost:6379",
450
+
451
+ # Option 2: Explicit pool configuration
452
+ # redis_url={
453
+ # "default": "redis://localhost:6379/15",
454
+ # "user": "redis://cache-users:6379/11",
455
+ # "handlers": "redis://cache-handlers:6379/10",
456
+ # "symphony_shared_state": "redis://cache-shared:6379/9",
457
+ # "expiry": "redis://cache-expiry:6379/8",
458
+ # "pubsub": "redis://cache-pubsub:6379/7"
459
+ # },
460
+
461
+ workers_user=os.cpu_count() * 4,
462
+ workers_tool=32,
463
+ workers_agent=16,
464
+ max_concurrent=128
465
+ )
466
+
467
+ await GlobalSymphony.create(config)
468
+ yield
469
+
470
+ app = FastAPI(lifespan=lifespan)
471
+
472
+ # Middleware for context binding
473
+ @app.middleware("http")
474
+ async def bind_shared_state_context(request: Request, call_next):
475
+ tenant_id = extract_tenant_from_request(request)
476
+ user_id = extract_user_from_request(request)
477
+
478
+ if tenant_id and user_id:
479
+ # Create and bind shared state to request context
480
+ # Automatically uses "symphony_shared_state" pool
481
+ ss = RedisSharedState(tenant=tenant_id, user_id=user_id)
482
+ token = _current_ss.set(ss)
483
+
484
+ try:
485
+ response = await call_next(request)
486
+ return response
487
+ finally:
488
+ _current_ss.reset(token)
489
+ else:
490
+ return await call_next(request)
491
+
492
+ # FastAPI endpoints can now use context-aware tools
493
+ @app.post("/chat")
494
+ async def handle_chat(message: str, request: Request):
495
+ # Any tools or agents called from here will automatically
496
+ # have access to the correct shared state via _current_ss.get()
497
+
498
+ # Direct access to shared state (symphony_shared_state pool)
499
+ ss = _current_ss.get()
500
+ await ss.update_field("conversation", "last_message", message)
501
+
502
+ # Tools in thread pools will also see the same shared state
503
+ result = await process_with_tools(message)
504
+ return {"response": result}
505
+ ```
506
+
507
+ ### 2. Agency-Swarm Tool Integration with Context
508
+
509
+ ```python
510
+ from agency_swarm.tools import BaseTool
511
+ from mimeiapify.symphony_ai.redis.context import _current_ss
512
+ from mimeiapify.symphony_ai import GlobalSymphony
513
+ import asyncio
514
+
515
+ class AsyncBaseTool(BaseTool):
516
+ """Enhanced BaseTool with context-aware Redis support"""
517
+
518
+ @property
519
+ def shared_state(self) -> RedisSharedState:
520
+ """Get context-bound shared state - safe across threads"""
521
+ return _current_ss.get()
522
+
523
+ def run_async(self, coro) -> Any:
524
+ """Execute async operation from sync tool context"""
525
+ loop = GlobalSymphony.get().loop
526
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
527
+ return future.result(timeout=30)
528
+
529
+ # Convenient sync wrappers for common operations
530
+ def get_state(self, state_name: str) -> dict:
531
+ return self.run_async(self.shared_state.get(state_name)) or {}
532
+
533
+ def upsert_state(self, state_name: str, data: dict) -> bool:
534
+ return self.run_async(self.shared_state.upsert(state_name, data))
535
+
536
+ def get_state_field(self, state_name: str, field: str):
537
+ return self.run_async(self.shared_state.get_field(state_name, field))
538
+
539
+ def update_state_field(self, state_name: str, field: str, value) -> bool:
540
+ return self.run_async(self.shared_state.update_field(state_name, field, value))
541
+
542
+ # Example tool using context-aware shared state
543
+ class EmailValidatorTool(AsyncBaseTool):
544
+ email: str = Field(..., description="Email to validate")
545
+
546
+ def run(self) -> str:
547
+ # No need to manually inject shared state - it's context-aware!
548
+ self.update_state_field("tool_history", "last_tool", "email_validator")
549
+
550
+ # Validate email logic here
551
+ is_valid = "@" in self.email
552
+
553
+ # Store result in shared state
554
+ self.update_state_field("validation_results", self.email, is_valid)
555
+
556
+ return f"Email {self.email} is {'valid' if is_valid else 'invalid'}"
557
+ ```
558
+
559
+ ## 🔧 Migration from Old Architecture
560
+
561
+ ### Key Changes
562
+ - **✅ Multi-pool architecture**: Separate Redis databases for different concerns
563
+ - **✅ SQL-style naming**: `upsert()` for hash operations, `set()` for simple key-value
564
+ - **✅ Pool alias support**: All operations can target specific pools
565
+ - **✅ Inherited method priority**: TenantCache methods used before direct ops
566
+ - **✅ Consistent field operations**: All hash repositories support `get_field()` / `update_field()`
567
+ - **✅ Removed task queue complexity**: No more `GlobalAgentState.task_queue` or `pending_tasks`
568
+ - **✅ Added context-aware shared state**: Thread-safe access via `ContextVar`
569
+ - **✅ Organized utils**: Infrastructure moved to `redis_handler/utils/`
570
+ - **✅ Simplified imports**: Clean package structure with proper `__init__.py` files
571
+
572
+ ### Before (Single Pool + Complex Queue)
573
+ ```python
574
+ # Old approach - single pool, complex queue plumbing
575
+ task_id = str(uuid.uuid4())
576
+ GlobalAgentState.pending_tasks[task_id] = asyncio.Future()
577
+ await GlobalAgentState.task_queue.put((task_id, some_coroutine()))
578
+ result = await GlobalAgentState.pending_tasks[task_id]
579
+
580
+ # Manual shared state injection per tool
581
+ BaseTool._shared_state = redis_shared_state # Race condition!
582
+
583
+ # Inconsistent naming
584
+ await user.set({"name": "Alice"}) # Was this HSET or SET?
585
+ await user.update_field("score", 100) # Mixed naming conventions
586
+ ```
587
+
588
+ ### After (Multi-Pool + Direct Integration)
589
+ ```python
590
+ # New approach - multi-pool, direct and context-aware
591
+ loop = GlobalSymphony.get().loop
592
+ future = asyncio.run_coroutine_threadsafe(some_coroutine(), loop)
593
+ result = future.result(timeout=5)
594
+
595
+ # Context-aware shared state (thread-safe, automatic pool targeting)
596
+ token = _current_ss.set(RedisSharedState(tenant="mimeia", user_id="user123"))
597
+ try:
598
+ # All tools automatically get the right shared state
599
+ result = await call_tools()
600
+ finally:
601
+ _current_ss.reset(token)
602
+
603
+ # Clear SQL-style naming
604
+ await user.upsert({"name": "Alice"}) # HSET - field-level updates
605
+ await generic.set("config", {"theme": "dark"}) # SET - complete replacement
606
+ await user.update_field("score", 100) # Consistent field operations
607
+ ```
608
+
609
+ ## 🔍 Key Features
610
+
611
+ - **✅ Multi-Pool Architecture**: Separate Redis databases for different subsystems
612
+ - **✅ SQL-Style Naming**: Clear distinction between HSET (`upsert`) and SET (`set`) operations
613
+ - **✅ Pool Alias Support**: All operations can target specific Redis pools
614
+ - **✅ Single Responsibility**: Each repository handles one domain with designated pool
615
+ - **✅ Type Safety**: Full Pydantic BaseModel support with boolean optimization
616
+ - **✅ Tenant Isolation**: Automatic key prefixing and scoping
617
+ - **✅ TTL Management**: Flexible per-operation and per-repository TTL control
618
+ - **✅ Atomic Operations**: Built on Redis atomic operations
619
+ - **✅ Context-Aware**: Thread-safe shared state via `ContextVar`
620
+ - **✅ GlobalSymphony Integration**: Seamless event loop and thread pool management
621
+ - **✅ Clean Architecture**: Utils separated from business logic, inherited methods prioritized
622
+ - **✅ No Task Queue Overhead**: Direct async/sync bridging
623
+
624
+ ## 📚 Best Practices
625
+
626
+ 1. **Use designated pools** - Let repositories automatically target their assigned pools
627
+ 2. **Override pools sparingly** - Only use `alias` parameter when cross-pool operations are needed
628
+ 3. **Follow SQL naming** - `upsert()` for hash operations, `set()` for simple key-value
629
+ 4. **Use context-aware shared state** instead of manual injection
630
+ 5. **Leverage `_current_ss.get()`** in tools for automatic context binding
631
+ 6. **Use specific repositories** over `RedisGeneric` when possible
632
+ 7. **Define BaseModel mappings** once and reuse across operations
633
+ 8. **Set appropriate TTLs** per data type (users: long, handlers: short, triggers: very short)
634
+ 9. **Bind shared state at request level** using middleware
635
+ 10. **Always reset context tokens** in `finally` blocks
636
+ 11. **Use `GlobalSymphony.get().loop`** for async/sync bridging
637
+ 12. **Import from utils** for infrastructure components
638
+ 13. **Cache repository instances** per tenant to avoid repeated initialization
639
+ 14. **Prefer inherited methods** over direct ops calls within repositories
640
+
641
+ ## 🎯 Import Patterns
642
+
643
+ ```python
644
+ # Main Redis functionality
645
+ from mimeiapify.symphony_ai.redis import RedisClient, ops, context, listeners
646
+
647
+ # Repository layer (each targets its designated pool)
648
+ from mimeiapify.symphony_ai.redis.redis_handler import (
649
+ RedisUser, # → "user" pool (DB 11)
650
+ RedisSharedState, # → "symphony_shared_state" pool (DB 9)
651
+ RedisStateHandler, # → "handlers" pool (DB 10)
652
+ RedisTable, # → "handlers" pool (DB 10)
653
+ RedisBatch, # → "handlers" pool (DB 10)
654
+ RedisTrigger, # → "expiry" pool (DB 8)
655
+ RedisGeneric # → "default" pool (DB 15)
656
+ )
657
+
658
+ # TTL-driven workflows
659
+ from mimeiapify.symphony_ai.redis.listeners import (
660
+ expiration_registry, # @on_expire_action decorator
661
+ run_expiry_listener # Background task for keyspace events
662
+ )
663
+
664
+ # Infrastructure utilities
665
+ from mimeiapify.symphony_ai.redis.redis_handler.utils import (
666
+ KeyFactory, dumps, loads, TenantCache
667
+ )
668
+
669
+ # Context-aware shared state
670
+ from mimeiapify.symphony_ai.redis.context import _current_ss, RedisSharedState
671
+
672
+ # GlobalSymphony integration
673
+ from mimeiapify.symphony_ai import GlobalSymphony, GlobalSymphonyConfig
674
+
675
+ # Utilities and logging
676
+ from mimeiapify.utils import logger, setup_logging
677
+ ```
678
+
679
+ ## 🔧 Pool Targeting Examples
680
+
681
+ ```python
682
+ # Repository automatic pool targeting
683
+ user = RedisUser(tenant="mimeia", user_id="user123") # → Uses "user" pool automatically
684
+ await user.upsert({"name": "Alice"})
685
+
686
+ # Direct ops with pool targeting
687
+ await ops.set("temp_key", "value", alias="expiry") # → "expiry" pool
688
+ await ops.hget("user_hash", "name", alias="user") # → "user" pool
689
+
690
+ # Repository pool override (advanced usage)
691
+ shared_state = RedisSharedState(tenant="mimeia", user_id="user123")
692
+ await shared_state.upsert("temp_state", {"data": "temp"}) # → Uses default "symphony_shared_state" pool
693
+ await shared_state._hset_with_ttl(
694
+ shared_state._key("temp_state"),
695
+ {"urgent": "data"},
696
+ ttl=60,
697
+ alias="expiry" # → Override to "expiry" pool for urgent data
698
+ )
699
+ ```