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.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
```
|