Files
konstruct/packages/shared/shared/redis_keys.py
Adolfo Delorenzo 5714acf741 feat(01-foundation-01): monorepo scaffolding, Docker Compose, and shared data models
- pyproject.toml: uv workspace with 5 member packages (shared, gateway, router, orchestrator, llm-pool)
- docker-compose.yml: PostgreSQL 16 + Redis 7 + Ollama services on konstruct-net
- .env.example: all required env vars documented, konstruct_app role (not superuser)
- scripts/init-db.sh: creates konstruct_app role at DB init time
- packages/shared/shared/config.py: Pydantic Settings loading all env vars
- packages/shared/shared/models/message.py: KonstructMessage, ChannelType, SenderInfo, MessageContent
- packages/shared/shared/models/tenant.py: Tenant, Agent, ChannelConnection SQLAlchemy 2.0 models
- packages/shared/shared/models/auth.py: PortalUser model for admin portal auth
- packages/shared/shared/db.py: async SQLAlchemy engine, session factory, get_session dependency
- packages/shared/shared/rls.py: current_tenant_id ContextVar and configure_rls_hook with parameterized SET LOCAL
- packages/shared/shared/redis_keys.py: tenant-namespaced key constructors (rate_limit, idempotency, session, engaged_thread)
2026-03-23 09:49:28 -06:00

89 lines
2.8 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}"