docs(02-agent-features): create phase plan
This commit is contained in:
265
.planning/phases/02-agent-features/02-03-PLAN.md
Normal file
265
.planning/phases/02-agent-features/02-03-PLAN.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
phase: 02-agent-features
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages/gateway/gateway/channels/whatsapp.py
|
||||
- packages/gateway/gateway/normalize.py
|
||||
- packages/gateway/gateway/main.py
|
||||
- packages/shared/shared/models/message.py
|
||||
- packages/shared/shared/config.py
|
||||
- packages/orchestrator/orchestrator/agents/runner.py
|
||||
- packages/orchestrator/orchestrator/tasks.py
|
||||
- migrations/versions/004_phase2_media.py
|
||||
- tests/unit/test_whatsapp_verify.py
|
||||
- tests/unit/test_whatsapp_normalize.py
|
||||
- tests/unit/test_whatsapp_scoping.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- CHAN-03
|
||||
- CHAN-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A WhatsApp message to the AI employee produces a reply in the same WhatsApp conversation"
|
||||
- "Webhook signature is verified via HMAC-SHA256 on raw body bytes before any JSON parsing"
|
||||
- "Per-tenant phone number isolation — each tenant's WhatsApp connection uses its own phone_number_id"
|
||||
- "Clearly off-topic messages get a canned rejection without an LLM call (tier 1 allowlist gate)"
|
||||
- "Borderline messages are handled by the LLM with business-function scoping in the system prompt (tier 2)"
|
||||
- "Media attachments (images, documents) are downloaded, stored in MinIO, and passed to the orchestrator"
|
||||
artifacts:
|
||||
- path: "packages/gateway/gateway/channels/whatsapp.py"
|
||||
provides: "WhatsApp webhook handler, signature verification, message sending"
|
||||
exports: ["whatsapp_router"]
|
||||
- path: "packages/gateway/gateway/normalize.py"
|
||||
provides: "normalize_whatsapp_event function alongside existing Slack normalizer"
|
||||
exports: ["normalize_whatsapp_event"]
|
||||
- path: "packages/shared/shared/models/message.py"
|
||||
provides: "MediaAttachment model added to MessageContent"
|
||||
contains: "class MediaAttachment"
|
||||
key_links:
|
||||
- from: "packages/gateway/gateway/channels/whatsapp.py"
|
||||
to: "gateway/normalize.py"
|
||||
via: "normalize_whatsapp_event called after signature verification"
|
||||
pattern: "normalize_whatsapp_event"
|
||||
- from: "packages/gateway/gateway/channels/whatsapp.py"
|
||||
to: "router/tenant.py"
|
||||
via: "resolve_tenant with phone_number_id as workspace_id"
|
||||
pattern: "resolve_tenant"
|
||||
- from: "packages/gateway/gateway/channels/whatsapp.py"
|
||||
to: "Celery handle_message task"
|
||||
via: "handle_message.delay() after normalization"
|
||||
pattern: "handle_message\\.delay"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the WhatsApp Business Cloud API adapter in the Channel Gateway: webhook verification, signature checking, message normalization to KonstructMessage, business-function scoping gate, media handling, and outbound message delivery. Extend the shared message model with typed media attachments.
|
||||
|
||||
Purpose: Adds the second messaging channel, enabling SMBs to deploy their AI employee on WhatsApp — the most common business communication channel globally.
|
||||
Output: WhatsApp adapter, media model extension, business-function scoping, passing tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-agent-features/02-CONTEXT.md
|
||||
@.planning/phases/02-agent-features/02-RESEARCH.md
|
||||
|
||||
@packages/gateway/gateway/channels/slack.py
|
||||
@packages/gateway/gateway/normalize.py
|
||||
@packages/gateway/gateway/main.py
|
||||
@packages/shared/shared/models/message.py
|
||||
@packages/shared/shared/config.py
|
||||
@packages/router/router/tenant.py
|
||||
@packages/router/router/ratelimit.py
|
||||
@packages/router/router/idempotency.py
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing patterns from Slack adapter that WhatsApp adapter must follow -->
|
||||
|
||||
Channel adapter sequence (from Phase 1):
|
||||
1. Receive webhook -> verify signature
|
||||
2. Normalize to KonstructMessage
|
||||
3. resolve_tenant(workspace_id) -> tenant_id
|
||||
4. check_rate_limit(tenant_id, channel)
|
||||
5. is_duplicate(message_id) -> skip if True
|
||||
6. Post placeholder / typing indicator
|
||||
7. handle_message.delay(msg_payload)
|
||||
|
||||
From packages/shared/shared/models/message.py:
|
||||
- KonstructMessage: id, tenant_id, channel, channel_metadata, sender, content, timestamp, thread_id, reply_to, context
|
||||
- MessageContent: text, html, attachments (list[dict]), mentions (list[str])
|
||||
- SenderInfo: user_id, display_name, role
|
||||
- ChannelType: slack (needs 'whatsapp' added)
|
||||
|
||||
From packages/router/router/tenant.py:
|
||||
- resolve_tenant(workspace_id: str, session) -> str (tenant_id)
|
||||
- workspace_id for WhatsApp = phone_number_id from channel_connections
|
||||
|
||||
From packages/shared/shared/config.py:
|
||||
- Settings class with pydantic-settings
|
||||
- Need to add: whatsapp_app_secret, whatsapp_verify_token
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Media model extension, WhatsApp normalizer, and signature verification with tests</name>
|
||||
<files>
|
||||
packages/shared/shared/models/message.py,
|
||||
packages/gateway/gateway/normalize.py,
|
||||
packages/shared/shared/config.py,
|
||||
tests/unit/test_whatsapp_verify.py,
|
||||
tests/unit/test_whatsapp_normalize.py
|
||||
</files>
|
||||
<behavior>
|
||||
- MediaAttachment has media_type (image|document|audio|video), url, storage_key, mime_type, filename, size_bytes
|
||||
- MessageContent.media is a list[MediaAttachment] (new field, defaults to [])
|
||||
- ChannelType enum includes 'whatsapp'
|
||||
- normalize_whatsapp_event converts Meta webhook payload to KonstructMessage with correct field mapping
|
||||
- normalize_whatsapp_event extracts media attachments (image/document) into MediaAttachment objects
|
||||
- normalize_whatsapp_event sets channel='whatsapp', sender.user_id=wa_id, thread_id=wa_id (WhatsApp has no threads)
|
||||
- verify_whatsapp_signature raises HTTPException(403) when signature is invalid
|
||||
- verify_whatsapp_signature returns raw body bytes when signature is valid
|
||||
- verify_whatsapp_signature uses hmac.compare_digest for timing-safe comparison
|
||||
- WhatsApp webhook GET verification returns hub.challenge when token matches
|
||||
- WhatsApp webhook GET verification returns 403 when token doesn't match
|
||||
</behavior>
|
||||
<action>
|
||||
1. Extend `packages/shared/shared/models/message.py`:
|
||||
- Add MediaType(StrEnum): IMAGE, DOCUMENT, AUDIO, VIDEO
|
||||
- Add MediaAttachment(BaseModel): media_type, url (str|None), storage_key (str|None), mime_type (str|None), filename (str|None), size_bytes (int|None)
|
||||
- Add `media: list[MediaAttachment] = []` field to MessageContent
|
||||
- Add 'whatsapp' to ChannelType enum
|
||||
|
||||
2. Extend `packages/shared/shared/config.py`:
|
||||
- Add whatsapp_app_secret: str = "" and whatsapp_verify_token: str = "" to Settings
|
||||
|
||||
3. Create `normalize_whatsapp_event()` in normalize.py:
|
||||
- Takes: parsed webhook JSON body (dict)
|
||||
- Extracts: entry[0].changes[0].value — this is the Meta Cloud API v20.0 structure
|
||||
- Maps: messages[0].from -> sender.user_id, messages[0].text.body -> content.text
|
||||
- For media messages (type=image/document): extract media_id, set MediaAttachment with media_type and a placeholder URL (actual download happens in the adapter)
|
||||
- Sets channel='whatsapp', thread_id=sender_wa_id (WhatsApp conversations are per-phone-number, not threaded)
|
||||
- Sets channel_metadata with phone_number_id, message_id from webhook
|
||||
|
||||
4. Write tests:
|
||||
- test_whatsapp_verify.py: Valid signature passes, invalid signature raises 403, timing-safe comparison used
|
||||
- test_whatsapp_normalize.py: Text message normalizes correctly, image message normalizes with MediaAttachment, correct field mapping for sender/channel/metadata
|
||||
|
||||
Note: Use `hmac.new()` not `hmac.HMAC()` for signature verification. Read raw body via `await request.body()` BEFORE any JSON parsing (Pitfall 5 from research).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_whatsapp_verify.py tests/unit/test_whatsapp_normalize.py -x -v</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- MediaAttachment model exists with typed media fields
|
||||
- MessageContent has media list field
|
||||
- WhatsApp events normalize to KonstructMessage with correct mapping
|
||||
- Signature verification is timing-safe and works on raw bytes
|
||||
- ChannelType includes 'whatsapp'
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: WhatsApp adapter with business-function scoping, media handling, and outbound delivery</name>
|
||||
<files>
|
||||
packages/gateway/gateway/channels/whatsapp.py,
|
||||
packages/gateway/gateway/main.py,
|
||||
tests/unit/test_whatsapp_scoping.py
|
||||
</files>
|
||||
<behavior>
|
||||
- is_clearly_off_topic returns True for messages with zero keyword overlap with allowed_functions
|
||||
- is_clearly_off_topic returns False for messages containing keywords from allowed_functions
|
||||
- Off-topic messages receive a canned redirect response mentioning the agent name and allowed topics
|
||||
- Borderline messages (not clearly off-topic) pass through to the LLM with scoping in system prompt
|
||||
- WhatsApp webhook POST processes messages through the full adapter sequence (verify -> normalize -> resolve_tenant -> rate_limit -> dedup -> scoping -> dispatch)
|
||||
- send_whatsapp_message sends text via Meta Cloud API POST to /v20.0/{phone_number_id}/messages
|
||||
- send_whatsapp_media sends image/document via Meta Cloud API with media_id or URL
|
||||
- Media download fetches from Meta API (GET /media/{media_id}) and stores to MinIO with tenant-prefixed key
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `packages/gateway/gateway/channels/whatsapp.py`:
|
||||
- whatsapp_router = APIRouter()
|
||||
- GET /whatsapp/webhook — verification handshake: check hub.mode=="subscribe" and hub.verify_token matches settings, return hub.challenge as PlainTextResponse
|
||||
- POST /whatsapp/webhook — inbound message handler:
|
||||
a. Read raw body via request.body() BEFORE parsing
|
||||
b. Verify HMAC-SHA256 signature (X-Hub-Signature-256 header)
|
||||
c. Parse JSON from raw body
|
||||
d. Skip non-message events (status updates, read receipts — check for messages key)
|
||||
e. Normalize via normalize_whatsapp_event()
|
||||
f. Resolve tenant via phone_number_id as workspace_id (same resolve_tenant function as Slack)
|
||||
g. Check rate limit (reuse existing check_rate_limit)
|
||||
h. Check idempotency (reuse is_duplicate/mark_processed)
|
||||
i. Business-function scoping check (see below)
|
||||
j. If media: download from Meta API, upload to MinIO with key {tenant_id}/{agent_id}/{message_id}/{filename}, update MediaAttachment.storage_key and .url (presigned URL)
|
||||
k. Dispatch handle_message.delay() with msg payload + extras (bot_token from channel_connections.config['access_token'], phone_number_id)
|
||||
- Always return 200 OK to Meta (even on errors — Meta retries on non-200)
|
||||
|
||||
2. Business-function scoping (two-tier gate per user decision):
|
||||
- Tier 1: is_clearly_off_topic(text, allowed_functions) — simple keyword overlap check. If zero overlap with any allowed function keywords, return True. Agent's allowed_functions come from Agent model (add `allowed_functions: list[str] = []` field if not present, or use agent.tools as proxy).
|
||||
- If clearly off-topic: send canned redirect via send_whatsapp_message: "{agent.name} is here to help with {', '.join(allowed_functions)}. How can I assist you with one of those?"
|
||||
- Tier 2: Borderline messages pass to the LLM. The scoping is enforced via the system prompt (which already contains the agent's role and persona). Add to system prompt builder: if channel == 'whatsapp', append "You only handle: {allowed_functions}. If a request is outside these areas, politely redirect the user."
|
||||
|
||||
3. Outbound message delivery:
|
||||
- async send_whatsapp_message(phone_number_id, access_token, recipient_wa_id, text) -> None
|
||||
- POST to https://graph.facebook.com/v20.0/{phone_number_id}/messages with messaging_product="whatsapp", to=recipient_wa_id, type="text", text={"body": text}
|
||||
- async send_whatsapp_media(phone_number_id, access_token, recipient_wa_id, media_url, media_type) for outbound media
|
||||
|
||||
4. Wire into orchestrator tasks.py:
|
||||
- In handle_message task, after getting LLM response, check channel type
|
||||
- If 'whatsapp': call send_whatsapp_message via httpx (same pattern as Slack chat.update — bot token from extras)
|
||||
- If 'slack': existing chat.update flow
|
||||
|
||||
5. Install boto3 for MinIO: `uv add boto3` in gateway package. Use endpoint_url=settings.minio_endpoint for S3-compatible MinIO access.
|
||||
|
||||
6. Register whatsapp_router in gateway main.py: `app.include_router(whatsapp_router)`
|
||||
|
||||
7. Write test_whatsapp_scoping.py:
|
||||
- Test is_clearly_off_topic with matching keywords -> False
|
||||
- Test is_clearly_off_topic with zero overlap -> True
|
||||
- Test canned redirect message format includes agent name and allowed functions
|
||||
- Test borderline message passes through (not rejected by tier 1)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_whatsapp_verify.py tests/unit/test_whatsapp_normalize.py tests/unit/test_whatsapp_scoping.py -x -v</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- WhatsApp webhook handler processes inbound messages through full adapter sequence
|
||||
- Signature verification on raw body bytes before JSON parsing
|
||||
- Business-function scoping: tier 1 rejects clearly off-topic, tier 2 scopes via system prompt
|
||||
- Media downloaded from Meta API and stored in MinIO with tenant-prefixed keys
|
||||
- Outbound messages sent via Meta Cloud API
|
||||
- Gateway routes registered and running
|
||||
- Canned redirect includes agent name and allowed topics
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All Phase 1 tests still pass: `pytest tests/ -x`
|
||||
- WhatsApp tests pass: `pytest tests/unit/test_whatsapp_verify.py tests/unit/test_whatsapp_normalize.py tests/unit/test_whatsapp_scoping.py -x`
|
||||
- Gateway starts without errors with new routes registered
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- WhatsApp messages are normalized to KonstructMessage and dispatched through the existing pipeline
|
||||
- Webhook signature verification prevents unauthorized requests
|
||||
- Business-function scoping enforces Meta 2026 policy (tier 1 keyword gate + tier 2 LLM scoping)
|
||||
- Media attachments are downloaded, stored in MinIO, and available for multimodal LLM processing
|
||||
- Per-tenant phone number isolation via phone_number_id in channel_connections
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-agent-features/02-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user