Files
konstruct/packages/shared/shared/redis_keys.py
Adolfo Delorenzo 28a5ee996e feat(02-01): add two-layer memory system — Redis sliding window + pgvector long-term
- ConversationEmbedding ORM model with Vector(384) column (pgvector)
- memory_short_key, escalation_status_key, pending_tool_confirm_key in redis_keys.py
- orchestrator/memory/short_term.py: RPUSH/LTRIM sliding window (get_recent_messages, append_message)
- orchestrator/memory/long_term.py: pgvector HNSW cosine search (retrieve_relevant, store_embedding)
- Migration 002: conversation_embeddings table, HNSW index, RLS with FORCE, SELECT/INSERT only
- 10 unit tests (fakeredis), 6 integration tests (pgvector) — all passing
- Auto-fix [Rule 3]: postgres image updated to pgvector/pgvector:pg16 (extension required)
2026-03-23 14:41:57 -06:00

147 lines
4.7 KiB
Python

"""
Namespaced Redis key constructors.
DESIGN PRINCIPLE: It must be impossible to construct a Redis key without
a tenant_id. Every function in this module requires tenant_id as its first
argument and prepends `{tenant_id}:` to every key.
This ensures strict per-tenant namespace isolation — Tenant A's rate limit
counters, session state, and deduplication keys are entirely separate from
Tenant B's, even though they share the same Redis instance.
Key format: {tenant_id}:{key_type}:{discriminator}
Examples:
rate_limit_key("acme", "slack") → "acme:ratelimit:slack"
idempotency_key("acme", "msg-123") → "acme:dedup:msg-123"
session_key("acme", "thread-456") → "acme:session:thread-456"
engaged_thread_key("acme", "T12345") → "acme:engaged:T12345"
"""
from __future__ import annotations
def rate_limit_key(tenant_id: str, channel: str) -> str:
"""
Redis key for per-tenant, per-channel rate limit counters.
Used by the token bucket rate limiter in the Message Router.
Args:
tenant_id: Konstruct tenant identifier.
channel: Channel type string (e.g. "slack", "whatsapp").
Returns:
Namespaced Redis key: "{tenant_id}:ratelimit:{channel}"
"""
return f"{tenant_id}:ratelimit:{channel}"
def idempotency_key(tenant_id: str, message_id: str) -> str:
"""
Redis key for message deduplication (idempotency).
Prevents duplicate processing when channels deliver events more than once
(e.g. Slack retry behaviour on gateway timeout).
Args:
tenant_id: Konstruct tenant identifier.
message_id: Unique message identifier from the channel.
Returns:
Namespaced Redis key: "{tenant_id}:dedup:{message_id}"
"""
return f"{tenant_id}:dedup:{message_id}"
def session_key(tenant_id: str, thread_id: str) -> str:
"""
Redis key for conversation session state.
Stores sliding window conversation history for a thread, used by the
Agent Orchestrator to maintain context between messages.
Args:
tenant_id: Konstruct tenant identifier.
thread_id: Thread identifier (e.g. Slack thread_ts or DM channel ID).
Returns:
Namespaced Redis key: "{tenant_id}:session:{thread_id}"
"""
return f"{tenant_id}:session:{thread_id}"
def engaged_thread_key(tenant_id: str, thread_id: str) -> str:
"""
Redis key tracking whether an agent is actively engaged in a thread.
An "engaged" thread means the agent has been @mentioned or responded in
this thread — subsequent messages in the thread don't require a new @mention.
Args:
tenant_id: Konstruct tenant identifier.
thread_id: Thread identifier.
Returns:
Namespaced Redis key: "{tenant_id}:engaged:{thread_id}"
"""
return f"{tenant_id}:engaged:{thread_id}"
def memory_short_key(tenant_id: str, agent_id: str, user_id: str) -> str:
"""
Redis key for the short-term conversational memory sliding window.
Stores the last N messages (serialized as JSON) for a specific
tenant + agent + user combination. Used by the Agent Orchestrator to
inject recent conversation history into every LLM prompt.
Key includes all three discriminators to ensure:
- Two users talking to the same agent have separate histories
- The same user talking to two different agents has separate histories
- Two tenants with the same agent/user IDs are fully isolated
Args:
tenant_id: Konstruct tenant identifier.
agent_id: Agent identifier (UUID string).
user_id: End-user identifier (channel-native, e.g. Slack user ID).
Returns:
Namespaced Redis key: "{tenant_id}:memory:short:{agent_id}:{user_id}"
"""
return f"{tenant_id}:memory:short:{agent_id}:{user_id}"
def escalation_status_key(tenant_id: str, thread_id: str) -> str:
"""
Redis key for tracking escalation status of a thread.
Stores the current escalation state for a conversation thread —
whether it has been escalated to a human or another agent.
Args:
tenant_id: Konstruct tenant identifier.
thread_id: Thread identifier.
Returns:
Namespaced Redis key: "{tenant_id}:escalation:{thread_id}"
"""
return f"{tenant_id}:escalation:{thread_id}"
def pending_tool_confirm_key(tenant_id: str, thread_id: str) -> str:
"""
Redis key for tracking pending tool confirmation requests.
Stores the pending tool invocation that requires explicit user
confirmation before execution (e.g. destructive operations).
Args:
tenant_id: Konstruct tenant identifier.
thread_id: Thread identifier.
Returns:
Namespaced Redis key: "{tenant_id}:tool_confirm:{thread_id}"
"""
return f"{tenant_id}:tool_confirm:{thread_id}"