diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7aeae49..aa11e43 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -13,7 +13,7 @@ Konstruct ships in three coarse phases ordered by dependency: first build the se Decimal phases appear between their surrounding integers in numeric order. - [x] **Phase 1: Foundation** - Secure multi-tenant pipeline with Slack end-to-end and basic agent response (completed 2026-03-23) -- [ ] **Phase 2: Agent Features** - Persistent memory, tool framework, WhatsApp integration, and human escalation (gap closure in progress) +- [x] **Phase 2: Agent Features** - Persistent memory, tool framework, WhatsApp integration, and human escalation (gap closure in progress) (completed 2026-03-24) - [ ] **Phase 3: Operator Experience** - Admin portal, tenant onboarding, and Stripe billing ## Phase Details @@ -80,7 +80,7 @@ Phases execute in numeric order: 1 → 2 → 3 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Foundation | 4/4 | Complete | 2026-03-23 | -| 2. Agent Features | 5/6 | Gap closure | - | +| 2. Agent Features | 6/6 | Complete | 2026-03-24 | | 3. Operator Experience | 0/2 | Not started | - | --- diff --git a/.planning/STATE.md b/.planning/STATE.md index 492e48c..6f5fd25 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: planning -stopped_at: Completed 02-agent-features/02-05-PLAN.md -last_updated: "2026-03-23T21:35:00.000Z" -last_activity: 2026-03-23 — Roadmap created, ready for Phase 1 planning +status: executing +stopped_at: Completed 02-agent-features/02-06-PLAN.md +last_updated: "2026-03-24T01:16:40.964Z" +last_activity: 2026-03-23 — Completed 02-05 multimodal media support and WhatsApp outbound routing progress: total_phases: 3 - completed_phases: 1 - total_plans: 9 - completed_plans: 8 - percent: 0 + completed_phases: 2 + total_plans: 10 + completed_plans: 10 + percent: 78 --- # Project State @@ -59,6 +59,7 @@ Progress: [████████░░] 78% | Phase 02-agent-features P04 | 5m | 2 tasks | 7 files | | Phase 02-agent-features P02 | 12m 22s | 3 tasks | 19 files | | Phase 02-agent-features P05 | ~25m | 2 tasks | 6 files | +| Phase 02-agent-features P06 | 9m 53s | 2 tasks | 3 files | ## Accumulated Context @@ -100,6 +101,9 @@ Recent decisions affecting current work: - [Phase 02-agent-features]: boto3 patched at import site patch('boto3.client') not patch('module.boto3') — local imports inside async functions require patching the actual module, not the module attribute - [Phase 02-agent-features]: build_messages_with_media() wraps build_messages_with_memory() — media enrichment is additive, all memory context preserved alongside image_url blocks - [Phase 02-agent-features]: AUDIO/VIDEO attachments text-referenced only in v1 — OpenAI image_url blocks support images only, not audio/video +- [Phase 02-agent-features]: Module-level imports in tasks.py for testability — patchable at orchestrator.tasks.* +- [Phase 02-agent-features]: Unified extras dict carries channel-specific metadata (Slack + WhatsApp) through entire pipeline +- [Phase 02-agent-features]: wa_id extracted from sender.user_id in handle_message after model_validate and injected into extras ### Pending Todos @@ -111,6 +115,6 @@ None yet. ## Session Continuity -Last session: 2026-03-23T21:35:00.000Z -Stopped at: Completed 02-agent-features/02-05-PLAN.md +Last session: 2026-03-24T01:16:40.962Z +Stopped at: Completed 02-agent-features/02-06-PLAN.md Resume file: None diff --git a/.planning/phases/02-agent-features/02-06-SUMMARY.md b/.planning/phases/02-agent-features/02-06-SUMMARY.md new file mode 100644 index 0000000..e347873 --- /dev/null +++ b/.planning/phases/02-agent-features/02-06-SUMMARY.md @@ -0,0 +1,146 @@ +--- +phase: 02-agent-features +plan: "06" +subsystem: orchestrator +tags: [escalation, whatsapp, outbound-routing, pipeline-wiring, system-prompt, tdd] +dependency_graph: + requires: + - 02-agent-features/02-02 (tasks.py pipeline rewrite) + - 02-agent-features/02-04 (escalation handler) + - 02-agent-features/02-05 (WhatsApp outbound + multimodal) + provides: + - Escalation handler wired into _process_message pre/post-check + - WhatsApp outbound routing wired end-to-end (handle_message -> _send_response) + - Tier-2 WhatsApp business-function scoping in system prompt builder + affects: + - packages/orchestrator/orchestrator/tasks.py + - packages/orchestrator/orchestrator/agents/builder.py + - tests/unit/test_pipeline_wiring.py +tech_stack: + added: [] + patterns: + - Module-level imports in tasks.py for testability (patchable at orchestrator.tasks.*) + - _build_response_extras() helper for channel-aware extras assembly + - _build_conversation_metadata() helper for billing keyword v1 detection + - extras dict pattern: unified channel metadata dict passed through pipeline +key_files: + created: + - tests/unit/test_pipeline_wiring.py + modified: + - packages/orchestrator/orchestrator/tasks.py + - packages/orchestrator/orchestrator/agents/builder.py +decisions: + - "Module-level imports in tasks.py: moved key imports to module level so unit tests can patch at orchestrator.tasks.* rather than source modules" + - "extras dict unified: Slack (placeholder_ts, channel_id, bot_token) and WhatsApp (phone_number_id, bot_token, wa_id) both carried in one dict through the pipeline" + - "_build_response_extras(): Slack path injects DB-loaded slack_bot_token; WhatsApp path uses gateway-injected bot_token (no DB lookup needed)" + - "Escalation pre-check before pending_tool_confirm check: already-escalated convos skip ALL processing including tool confirmations" + - "wa_id extracted in handle_message from msg.sender.user_id after model_validate, injected into extras dict" +metrics: + duration: "9m 53s" + completed_date: "2026-03-24" + tasks_completed: 2 + files_modified: 3 +--- + +# Phase 2 Plan 06: Escalation and WhatsApp Routing Re-Wire Summary + +Re-wired orphaned escalation handler and broken WhatsApp outbound routing into the orchestrator pipeline; added tier-2 WhatsApp business-function scoping to the system prompt builder. + +## What Was Built + +### Task 1: Escalation + Outbound Routing Re-wire in tasks.py + +**Problem:** Plans 02-02 and 02-05 rewrote tasks.py but dropped two integrations: +- `check_escalation_rules` / `escalate_to_human` were never called (orphaned) +- All responses went to `_update_slack_placeholder` directly — WhatsApp replies silently lost +- `handle_message` didn't pop WhatsApp extras before `model_validate` (unknown field errors) + +**Solution:** + +1. **Module-level imports:** Moved `aioredis`, `check_escalation_rules`, `escalate_to_human`, `build_messages_with_memory`, `run_agent`, `AuditLogger`, `embed_text`, `retrieve_relevant`, `append_message`, `get_recent_messages`, `get_tools_for_agent`, `settings`, `async_session_factory`, `engine`, `escalation_status_key`, `configure_rls_hook`, `current_tenant_id` to module level for testability. + +2. **WhatsApp extra extraction in `handle_message`:** Now pops `phone_number_id` and `bot_token` (WhatsApp access_token) before `model_validate`. Extracts `wa_id` from `msg.sender.user_id` after validation. Builds unified `extras` dict. + +3. **`_process_message` signature change:** Now accepts `extras: dict | None = None` instead of `placeholder_ts` and `channel_id` separately. + +4. **`_build_response_extras()` helper:** Assembles channel-specific response extras: + - Slack: `{bot_token: db_token, channel_id: ..., placeholder_ts: ...}` + - WhatsApp: `{phone_number_id: ..., bot_token: gateway_token, wa_id: ...}` + +5. **Escalation pre-check:** Before the pending tool confirmation block, checks Redis `escalation_status_key`. If `b"escalated"`, returns early with assistant-mode reply — no LLM call. + +6. **All response delivery via `_send_response()`:** Replaced all three `_update_slack_placeholder` calls in `_process_message` with `_send_response(msg.channel, text, response_extras)`. `_update_slack_placeholder` remains the implementation detail inside `_send_response`. + +7. **Escalation post-check:** After `run_agent()` returns, calls `check_escalation_rules()` with `_build_conversation_metadata()` output. If a rule matches AND `agent.escalation_assignee` is set, calls `escalate_to_human()` and replaces `response_text` with the confirmation message. + +8. **`_build_conversation_metadata()` helper:** Scans recent messages + current text for billing keywords (`billing`, `invoice`, `charge`, `refund`, `payment`, `subscription`). Returns `{billing_dispute: bool, attempts: int}`. + +### Task 2: Tier-2 WhatsApp Scoping in builder.py + +Added `channel: str = ""` parameter to `build_system_prompt()`, `build_messages_with_memory()`, and `build_messages_with_media()`. + +When `channel == "whatsapp"` and `agent.tool_assignments` is non-empty, `build_system_prompt()` appends: + +``` +You are responding on WhatsApp. You only handle: {topics}. +If the user asks about something outside these topics, +politely redirect them to the allowed topics. +``` + +`_process_message` now passes `str(msg.channel)` to `build_messages_with_memory()`. + +## Test Results + +26 new tests in `tests/unit/test_pipeline_wiring.py` — all passing: +- 4 tests: `handle_message` WhatsApp extra extraction +- 3 tests: `_send_response` channel routing +- 2 tests: `_process_message` uses `_send_response` (not `_update_slack_placeholder`) +- 1 test: escalation pre-check skips LLM +- 3 tests: escalation post-check (rule match, assignee set/unset) +- 4 tests: `_build_conversation_metadata` billing keyword detection +- 5 tests: `build_system_prompt` WhatsApp scoping +- 4 tests: `build_messages_with_memory` channel parameter + +Existing tests: 66 passing (no regressions): +- `test_escalation.py` (26 tests) +- `test_whatsapp_scoping.py` (14 tests) +- `test_whatsapp_normalize.py` (17 tests) +- `test_whatsapp_verify.py` (9 tests) + +## Verification + +``` +check_escalation_rules wired at: tasks.py:71 (import), :504 (call) +escalate_to_human wired at: tasks.py:71 (import), :514 (call) +_send_response called at: tasks.py:355, :395, :438, :556 +_update_slack_placeholder: only inside _send_response (not directly in _process_message) +"You only handle": builder.py:187 +``` + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Architecture] Module-level imports for testability** +- **Found during:** Task 1 GREEN phase +- **Issue:** plan specified local imports matching existing pattern, but local imports in `_process_message` make it impossible to patch at `orchestrator.tasks.*` — patching requires module-level binding +- **Fix:** Moved all key imports to module level; kept only `Agent`/`ChannelConnection` ORM models as local imports (to avoid circular import risks at module load time) +- **Files modified:** `packages/orchestrator/orchestrator/tasks.py` +- **Commit:** bd217a4 + +**2. [Rule 1 - Bug] wa_id set in extras dict from handle_message** +- **Found during:** Task 1 test verification +- **Issue:** Plan specified extracting `wa_id` from `msg.sender.user_id` inside `_process_message`, but `extras` dict was already built in `handle_message` before `_process_message` is called — need to set wa_id in handle_message after model_validate +- **Fix:** `handle_message` extracts `wa_id = msg.sender.user_id` after `model_validate` when `channel == "whatsapp"` and injects into extras dict +- **Files modified:** `packages/orchestrator/orchestrator/tasks.py` +- **Commit:** bd217a4 + +## Self-Check: PASSED + +- FOUND: packages/orchestrator/orchestrator/tasks.py +- FOUND: packages/orchestrator/orchestrator/agents/builder.py +- FOUND: tests/unit/test_pipeline_wiring.py +- FOUND: .planning/phases/02-agent-features/02-06-SUMMARY.md +- FOUND commit: bd217a4 (feat implementation) +- FOUND commit: 77c9cfc (test RED phase) +- 92 tests pass (26 new + 66 existing regressions)