fix(02-agent-features): revise plans based on checker feedback

This commit is contained in:
2026-03-23 14:32:20 -06:00
parent 7da5ffb92a
commit b2e86f1046
4 changed files with 319 additions and 68 deletions

View File

@@ -10,9 +10,6 @@ files_modified:
- 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
@@ -55,10 +52,12 @@ must_haves:
---
<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.
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 (download + MinIO storage), 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.
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.
Note: Outbound wiring in orchestrator tasks.py (channel-aware response routing) and multimodal LLM interpretation of media are handled in Plan 02-05 to avoid file conflicts with Plans 02-01 and 02-02 which also modify tasks.py.
</objective>
<execution_context>
@@ -146,7 +145,7 @@ From packages/shared/shared/config.py:
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
- 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)
@@ -171,7 +170,7 @@ From packages/shared/shared/config.py:
</task>
<task type="auto" tdd="true">
<name>Task 2: WhatsApp adapter with business-function scoping, media handling, and outbound delivery</name>
<name>Task 2: WhatsApp adapter with business-function scoping, media download/storage, and outbound delivery</name>
<files>
packages/gateway/gateway/channels/whatsapp.py,
packages/gateway/gateway/main.py,
@@ -190,12 +189,12 @@ From packages/shared/shared/config.py:
<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:
- 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)
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)
@@ -203,32 +202,29 @@ From packages/shared/shared/config.py:
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)
- 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).
- 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:
3. Outbound message delivery (used by the adapter for direct responses like off-topic canned replies):
- 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
4. Install boto3 for MinIO: `uv add boto3` in gateway package. Use endpoint_url=settings.minio_endpoint for S3-compatible MinIO access.
5. Install boto3 for MinIO: `uv add boto3` in gateway package. Use endpoint_url=settings.minio_endpoint for S3-compatible MinIO access.
5. Register whatsapp_router in gateway main.py: `app.include_router(whatsapp_router)`
6. Register whatsapp_router in gateway main.py: `app.include_router(whatsapp_router)`
7. Write test_whatsapp_scoping.py:
6. 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)
Note: The orchestrator-side wiring (channel-aware outbound routing in tasks.py) is deferred to Plan 02-05 to avoid file conflicts with Plans 02-01 and 02-02. The WhatsApp adapter can handle direct responses (off-topic canned replies, webhook verification) independently. LLM-generated responses routed back through WhatsApp will be wired in Plan 02-05.
</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>
@@ -238,7 +234,7 @@ From packages/shared/shared/config.py:
- 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
- Outbound text and media messages sent via Meta Cloud API (for adapter-direct responses)
- Gateway routes registered and running
- Canned redirect includes agent name and allowed topics
</done>
@@ -256,7 +252,7 @@ From packages/shared/shared/config.py:
- 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
- Media attachments are downloaded, stored in MinIO, and available for downstream processing
- Per-tenant phone number isolation via phone_number_id in channel_connections
</success_criteria>