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