docs(06-01): complete web chat backend infrastructure plan
This commit is contained in:
147
.planning/phases/06-web-chat/06-01-SUMMARY.md
Normal file
147
.planning/phases/06-web-chat/06-01-SUMMARY.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
phase: 06-web-chat
|
||||
plan: 01
|
||||
subsystem: backend
|
||||
tags: [web-chat, websocket, redis-pubsub, rbac, orm, migration]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides:
|
||||
- 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
|
||||
affects:
|
||||
- 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)
|
||||
tech_stack:
|
||||
added:
|
||||
- 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)
|
||||
patterns:
|
||||
- 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
|
||||
key_files:
|
||||
created:
|
||||
- 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
|
||||
modified:
|
||||
- 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)
|
||||
decisions:
|
||||
- "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"
|
||||
metrics:
|
||||
duration: "~8 minutes"
|
||||
completed_date: "2026-03-25"
|
||||
tasks_completed: 2
|
||||
files_created: 6
|
||||
files_modified: 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
|
||||
Reference in New Issue
Block a user