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)
This commit is contained in:
2026-03-23 09:49:28 -06:00
parent d611a07cc2
commit 5714acf741
19 changed files with 3935 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
"""
KonstructMessage — the unified internal message format.
All channel adapters (Slack, WhatsApp, Mattermost, etc.) normalize inbound
events into this format before passing them to the Message Router. The core
business logic never depends on which messaging platform a message came from.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
class ChannelType(StrEnum):
"""Supported messaging channels."""
SLACK = "slack"
WHATSAPP = "whatsapp"
MATTERMOST = "mattermost"
ROCKETCHAT = "rocketchat"
TEAMS = "teams"
TELEGRAM = "telegram"
SIGNAL = "signal"
class SenderInfo(BaseModel):
"""Information about the message sender."""
user_id: str = Field(description="Channel-native user ID (e.g. Slack user ID U12345)")
display_name: str = Field(description="Human-readable display name")
email: str | None = Field(default=None, description="Sender email if available")
is_bot: bool = Field(default=False, description="True if sender is a bot/automation")
class MessageContent(BaseModel):
"""The content of a message — text and optional attachments."""
text: str = Field(description="Plain text content of the message")
html: str | None = Field(default=None, description="HTML-formatted content if available")
attachments: list[dict[str, Any]] = Field(
default_factory=list,
description="File attachments, images, or structured payloads",
)
mentions: list[str] = Field(
default_factory=list,
description="List of user/bot IDs mentioned in the message",
)
class KonstructMessage(BaseModel):
"""
Unified internal message format for Konstruct.
All channel adapters normalize events into this format. Downstream services
(Router, Orchestrator) operate exclusively on KonstructMessage — they never
inspect channel-specific fields directly.
`tenant_id` is None immediately after normalization. The Message Router
populates it via channel_connections lookup before forwarding.
"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique message ID (UUID)",
)
tenant_id: str | None = Field(
default=None,
description="Konstruct tenant ID — populated by Message Router after resolution",
)
channel: ChannelType = Field(description="Source messaging channel")
channel_metadata: dict[str, Any] = Field(
description="Channel-specific identifiers: workspace_id, channel_id, bot_user_id, etc."
)
sender: SenderInfo = Field(description="Message sender information")
content: MessageContent = Field(description="Message content")
timestamp: datetime = Field(description="Message timestamp (UTC)")
thread_id: str | None = Field(
default=None,
description="Thread identifier for threaded conversations (e.g. Slack thread_ts)",
)
reply_to: str | None = Field(
default=None,
description="Parent message ID if this is a reply",
)
context: dict[str, Any] = Field(
default_factory=dict,
description="Extracted intent, entities, sentiment — populated by downstream processors",
)