Files
konstruct/.planning/phases/06-web-chat/06-01-SUMMARY.md

7.6 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
06-web-chat 01 backend
web-chat
websocket
redis-pubsub
rbac
orm
migration
requires provides affects
WebSocket endpoint at /chat/ws/{conversation_id}
REST API at /api/portal/chat/* for conversation CRUD
web_conversations and web_conversation_messages tables with RLS
Redis pub-sub response delivery for web channel
ChannelType.WEB in shared message model
packages/orchestrator/orchestrator/tasks.py (new web channel routing)
packages/shared/shared/api/__init__.py (chat_router added)
packages/gateway/gateway/main.py (Phase 6 routers mounted)
added patterns
gateway/channels/web.py (FastAPI WebSocket + normalize_web_event)
shared/api/chat.py (conversation CRUD REST API)
shared/models/chat.py (WebConversation + WebConversationMessage ORM)
migrations/versions/008_web_chat.py (DB tables + RLS + CHECK constraint update)
WebSocket auth via first JSON message (browser cannot send custom headers)
Redis pub-sub for async response delivery from Celery to WebSocket
thread_id = conversation_id for agent memory scoping
try/finally around all Redis connections to prevent leaks
TEXT+CHECK for role column (not sa.Enum) per Phase 1 ADR
SQLAlchemy 2.0 Mapped[]/mapped_column() style
require_tenant_member RBAC guard on all REST endpoints
created modified
packages/gateway/gateway/channels/web.py
packages/shared/shared/api/chat.py
packages/shared/shared/models/chat.py
migrations/versions/008_web_chat.py
tests/unit/test_web_channel.py
tests/unit/test_chat_api.py
packages/shared/shared/models/message.py (ChannelType.WEB added)
packages/shared/shared/redis_keys.py (webchat_response_key added)
packages/shared/shared/api/__init__.py (chat_router exported)
packages/gateway/gateway/main.py (Phase 6 routers mounted)
packages/orchestrator/orchestrator/tasks.py (web channel extras + routing)
WebSocket auth via first JSON message after connection — browser WebSocket API cannot send custom HTTP headers
thread_id = conversation_id in normalize_web_event — scopes agent memory to one web conversation (consistent with WhatsApp wa_id scoping)
Redis pub-sub response delivery: orchestrator publishes to webchat_response_key, WebSocket handler subscribes with 60s timeout
TEXT+CHECK for role column ('user'/'assistant') per Phase 1 ADR — not sa.Enum
dependency_overrides used in tests instead of patching shared.db.get_session — FastAPI dependency injection doesn't follow module-level patches
duration completed_date tasks_completed files_created files_modified
~8 minutes 2026-03-25 2 6 5

Phase 6 Plan 01: Web Chat Backend Infrastructure Summary

One-liner: WebSocket endpoint + Redis pub-sub response bridge + RBAC REST API providing complete web chat plumbing from portal UI to the agent pipeline.

What Was Built

This plan establishes the complete backend for web chat — the "web" channel that lets portal users talk to AI employees directly from the Konstruct portal UI without setting up Slack or WhatsApp.

ChannelType.WEB and Redis key

ChannelType.WEB = "web" added to the shared message model. webchat_response_key(tenant_id, conversation_id) added to redis_keys.py following the established namespace pattern ({tenant_id}:webchat:response:{conversation_id}).

DB Schema (migration 008)

Two new tables with FORCE ROW LEVEL SECURITY:

  • web_conversations — one per (tenant_id, agent_id, user_id) triple with UniqueConstraint for get-or-create semantics
  • web_conversation_messages — individual messages with TEXT+CHECK role column ('user'/'assistant') and CASCADE delete
  • channel_connections.channel_type CHECK constraint replaced to include 'web'

WebSocket Endpoint (/chat/ws/{conversation_id})

Full message lifecycle in gateway/channels/web.py:

  1. Accept connection
  2. Auth handshake via first JSON message (browser limitation)
  3. For each message: typing indicator → save to DB → Celery dispatch → Redis subscribe → save response → send to client
  4. try/finally cleanup on all Redis connections

REST API (/api/portal/chat/*)

Four endpoints in shared/api/chat.py:

  • GET /conversations — list with RBAC (platform_admin sees all, others see own)
  • POST /conversations — get-or-create with IntegrityError race condition handling
  • GET /conversations/{id}/messages — paginated history with cursor support
  • DELETE /conversations/{id} — message reset keeping conversation row

Orchestrator Integration

tasks.py updated:

  • handle_message pops conversation_id and portal_user_id before model_validate
  • _build_response_extras handles "web" case returning {conversation_id, tenant_id}
  • _send_response handles "web" case with Redis pub-sub publish and try/finally cleanup
  • webchat_response_key imported at module level

Test Coverage

19 unit tests written (TDD, all passing):

Test Covers
test_webchat_response_key_format Key format correct
test_webchat_response_key_isolation Tenant isolation
test_channel_type_web_exists ChannelType.WEB
test_normalize_web_event_* (5 tests) Message normalization CHAT-01
test_send_response_web_publishes_to_redis Redis pub-sub publish CHAT-02
test_send_response_web_connection_cleanup try/finally Redis cleanup
test_send_response_web_missing_conversation_id_logs_warning Error handling
test_typing_indicator_sent_before_dispatch Typing indicator CHAT-05
test_chat_rbac_enforcement 403 for non-member CHAT-04
test_platform_admin_cross_tenant Admin bypass CHAT-04
test_list_conversation_history Paginated messages CHAT-03
test_create_conversation Get-or-create CHAT-03
test_create_conversation_rbac_forbidden 403 for non-member
test_delete_conversation_resets_messages Message reset

Full 313-test suite passes.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Test dependency injection: patch vs dependency_overrides

  • Found during: Task 1 test implementation
  • Issue: patch("shared.db.get_session") doesn't work for FastAPI endpoint testing because FastAPI's dependency injection resolves Depends(get_session) at function definition time, not via module attribute lookup
  • Fix: Used app.dependency_overrides[get_session] = _override_get_session pattern in test helper _make_app_with_session_override() — consistent with other test files in the project
  • Files modified: tests/unit/test_chat_api.py

2. [Rule 2 - Missing functionality] session.refresh mock populating server defaults

  • Found during: Task 1 create_conversation test
  • Issue: Mocked session.refresh() was a no-op, leaving created_at/updated_at as None on new ORM objects (server_default not applied without real DB)
  • Fix: Test uses an async side_effect function that populates datetime fields on the object passed to refresh()
  • Files modified: tests/unit/test_chat_api.py

Self-Check: PASSED

All key artifacts verified:

  • ChannelType.WEB = "web" — present in message.py
  • webchat_response_key() — present in redis_keys.py
  • WebConversation ORM class — present in models/chat.py
  • chat_websocket WebSocket endpoint — present in gateway/channels/web.py
  • chat_router — exported from shared/api/init.py
  • web_conversations table — created in migration 008
  • Commits c72beb9 and 56c11a0 — verified in git log
  • 313/313 unit tests pass