# Silver Oak AI Chatbot System — Architecture & Delivery Blueprint

**Date:** 2026-06-12 · **Status:** Working specification
**Scope:** Extends the live single-audience buyer agent to the full 4-audience
platform (potential clients · internal agents · job seekers · post-sale clients).
**Reads with:** `plans/agent-architecture-review.md` (verified foundation — keep
single agent, 8–12 namespaced tools, modular prompt, hybrid retrieval) and
`plans/agent-capabilities-roadmap.md` (capability × status map). This document
does NOT supersede those; it is the multi-audience superstructure on top.

---

## 0. Standing assumptions & verified facts

1. **DeepSeek V4 via hosted API** — the VPS cannot serve a frontier model.
   "Self-hosted" applies to orchestration, data, state — not inference.
2. **LeadRat API verified to exist** (2026-06-12 web research):
   Enterprise API at https://apidoc.leadrat.info/ · help docs at
   https://apidocs.leadrat.com/. Auth = API key + secret → access token
   (**expires in 60 min**) with a `Tenant` request header; keys generated in
   Global Settings → Developer Resources. Lead POST supports assignment to
   primary/secondary users; **unmapped parameters land as lead notes** (which
   explains why our note-prefix temperature hack works). Open: whether
   `PUT /lead` exposes a real tier/tag field — needs a credentialed test
   (kickoff task K3).
3. **Runtime (verified live 2026-06-12):** native PM2 on the InMotion VPS —
   `channels-api` (:3100), `channels-worker`, `n8n`, plus legacy `silver-oak`
   + `silver-oak-reporting`. Postgres/Redis under systemd. Health OK.
   (docs/NEXT-SESSION-PROMPT.md's docker-compose description is STALE.)
4. **WhatsApp number status:** suspended as of the 2026-06-11 capabilities
   roadmap. Everything below that needs live WhatsApp is Playground/eval-first
   until the number (or the production WABA) returns.
5. **Slot-based lead state is a settled operator decision** (2026-06-12):
   structured `LeadState` (intent/budget/area/unitType/timeline/residency/
   buyerType); code decides the next question; free-text memory retired.
   The architecture review's "A/B this" item is closed by fiat.
6. **Cost discipline is a hard rule:** no eval runs / API spend without
   explicit operator go-ahead; ≤3.5k input tokens per turn budget; prompt
   cache prefix must stay stable (static/volatile split already shipped).
7. **Regulatory:** RERA/DLD context. The bot never gives guarantees, legal/
   tax/visa *advice*, or discounts — hard guardrail, eval-enforced.

---

## 1. Audience analysis

| | Potential clients | Internal agents | Job seekers | Post-sale clients |
|---|---|---|---|---|
| **Core need** | "Worth my money? Can I trust you?" — shortlist, payment plans, ROI, Golden Visa | Instant recall: availability, payment plans, commissions, fact sheets, draft client messages | Role reality (commission model, leads provided?, visa), application path | Status + accountability: payments, milestones, handover, snagging, tickets |
| **Intent signals** | budget/area mentions, "payment plan", "ROI", "Golden Visa", brochure asks, language switching | project codenames, "commission on…", terse queries | "vacancy", "salary", "visa", CV attachments | booking ref, "payment due", "handover", frustrated tone |
| **AI leverage** | 24/7 capture, EN/AR (+RU/HI later), qualification, FAQ deflection | fact retrieval, drafting, payment math | expectation-setting, structured screening, CV intake | status lookups, ticket intake, checklists |
| **Mandatory human handoff** | slots complete (by design); negotiation; legal/visa specifics; frustration | KB miss; commission disputes | terms/offers; shortlisted candidates | disputes, refunds, legal threats, frustration — on FIRST signal |

**Journeys (first contact → resolution):**

- **Potential client:** entry w/ source metadata → greet + language detect →
  slot loop interleaved with ≤3-sentence KB answers, always steering to the
  next missing slot → ≥4 core slots → LeadRat lead w/ structured state +
  summary → agent notified → named handoff. Resolution = warm handoff. The
  bot never closes, schedules autonomously, or negotiates.
- **Internal agent:** dedicated number or staff-allowlist check → structured-
  first lookup (tables before chunks) → terse answer + source tag + date.
  Drafting mode returns copy-paste text in the client persona. KB miss →
  `kb.gap` event, never improvisation.
- **Job seeker:** careers entry or router catch → role-reality statement up
  front → fixed slots (role, experience, visa status, languages, CV) →
  `candidates` row → weekly HR digest via n8n; high-signal flagged
  immediately. Never promises interviews or terms.
- **Post-sale:** identity verified (booking ref / registered phone) →
  read-only status from structured rows (LLM formats, never invents) →
  action requests become tickets w/ number + SLA → frustration or 2 failed
  turns → immediate human escalation.

> **Key decision:** the AI's job in every flow is *capture, qualify,
> retrieve, route* — never *close, promise, adjudicate*. The product is a
> clean structured record + faithful summary, handed to the right human fast.

## 2. System architecture

```
WhatsApp (Meta Cloud API)
   │ webhooks
   ▼
channels-api (:3100)  → dedupe → enqueue (Redis)
channels-worker
   ├─ debouncer (≈2.5s per conversation)
   ├─ ROUTER: deterministic first (entry refs, dedicated numbers, staff
   │   allowlist, known-client phone, keywords) → LLM 4-way classifier
   │   fallback only for ambiguous first messages (~15% of traffic)
   ├─ AGENT RUNTIME (the existing single-agent runToolLoop), 4 routed
   │   profiles = prompt-module set + tool subset + retrieval ACL
   ├─ Postgres: conversations, slot state, kb chunks (pgvector + tsv),
   │   tickets, candidates, events
   └─ Redis: hot session, queues, rate limits, debounce
        │ async typed events (lead.qualified, ticket.created, kb.gap,
        ▼  candidate.screened) → HTTP → n8n
n8n: LeadRat adapter · notifications · KB ingestion · weekly digests
```

**Division of labor (load-bearing):** channels_manager owns the synchronous
reply path end-to-end. n8n owns async side-effects ONLY — putting it in the
reply path adds a hop and a second failure domain. Already the live pattern.

**DeepSeek V4 roles per flow:** NLU/slot extraction + tool calling everywhere;
RAG generation for clients/agents (≤3 sentences, source-tagged); handoff/
ticket summarization; drafting for agents. **Post-sale is deliberately the
least generative flow** — status answers are template-formatted from DB rows;
a wrong payment date is an incident, a stale starting price is not. Sentiment
check piggybacks on the main call's JSON output (no extra call).

**KB structure:** one Postgres schema, audience-partitioned by metadata —
`kb_documents (audience text[], doc_type, effective_date, expires_at, owner,
status)` + `kb_chunks (chunk_text, embedding vector, tsv, metadata jsonb)`.
Audience ACL enforced in the retrieval SQL (`WHERE audience @> …`), not the
prompt. Fact sheets stored structured (JSON) + one prose chunk. Commission
matrix and availability are SQL tables queried directly — exact lookups must
be exact, never embedded. Job-seeker knowledge is ~2 pages → in-prompt module,
**no RAG**. Every price-bearing chunk carries `effective_date` + `expires_at`;
expired → "let me get you current pricing" + capture (a better outcome anyway).

**Memory:** Redis hash per conversation (rolling ~30-msg window, slot state,
audience, language; 24h TTL) + Postgres durable log and versioned slot state.
Returning user → inject one-paragraph summary + final slots and RE-CONFIRM
("still looking at 2BR in JVC around 1.5M?") — doubles as data hygiene.
Per-turn budget: routed prompt ~1–1.2k + slots ~150 + window ~800 + top-4
chunks ~1.2k ≈ **≤3.5k input tokens**, cache-prefix-stable, eval-enforced.

**Integrations:** Meta Cloud API via existing gateway (+ interactive
buttons/lists for disambiguation); LeadRat via n8n adapter called async with
retry, **idempotency key = phone + conversation id**, token cache ≤55 min
(60-min expiry, §0.2); DeepSeek direct HTTPS, 12s timeout → retry once →
templated fallback + human-notify, never silent drop.

> **Key decision:** one agent runtime, four routed profiles; Postgres+Redis
> as the only datastores; n8n strictly async. No new services, no second VPS.

## 3. Knowledge audit framework

Asset classes in build order: (1) project fact sheets [structured JSON +
prose chunk; sales ops; weekly price check], (2) payment-plan & buying-process
explainers [staged in kb-pack, pending team verification], (3) company
credentials pack [the trust layer — most underweighted asset], (4) area
guides ≤150 words [9 already written], (5) commission matrix + SOPs
[internal; effective-dated SQL], (6) availability snapshot [even daily CSV →
table beats nothing], (7) hiring pack [in-prompt], (8) post-sale records
[client↔unit, payment schedules, milestones — **most likely to not exist
structured; audit gates Phase 3**], (9) refusal-and-redirect library
[approved phrasings for everything the bot declines — first-class asset].

**Three highest-risk gaps:**
1. **Stale prices quoted to buyers** → date-stamped answers, `expires_at` on
   price chunks, weekly n8n expiry review.
2. **Post-sale data wrong** → post-sale ships intake-only until a data-quality
   audit passes; wrong payment amounts are worse than no bot.
3. **Commission data leaking to clients** → ACL at the SQL layer + a standing
   adversarial eval case that asks the client persona for commissions.

> **Key decision:** KB grows gap-driven from `kb.gap` events, not by bulk
> pre-authoring (standing operator correction). Everything owner-assigned and
> effective-dated.

## 4. Persona & conversation design

| | Client-facing | Internal | Job seeker | Post-sale |
|---|---|---|---|---|
| Persona | **"Sara from Silver Oak"** — disclosed AI (Meta policy + trust) | none; terse utility, source-tagged | "Silver Oak Careers Assistant" | "Silver Oak Care" |
| Tone | warm, concise, mirrors language | telegraphic | candid about role reality | calm, accountable |
| Escalate on | slots complete; negotiation; frustration; legal/visa; "human please" | KB miss ×1 | shortlist flag; any terms question | frustration (1st signal); disputes; 2 failed turns |
| Fallback | "our consultant will cover that on the call" + continue slots | "not in my KB — flagged" + `kb.gap` | "HR will answer that directly" | "ticket #N; [owner] responds within [SLA]" |

**Top-3 flow skeletons per audience:** clients — inbound qualification (the
core flow), project-specific inquiry (fact sheet → 3 sentences + brochure →
pivot to slots), returning-lead re-engagement (phone match → re-confirm →
notify owning agent, no duplicate lead). Agents — fact lookup, client-message
drafting (agent stays accountable for sending), payment-plan math via the
deterministic `calc_mortgage`-style tool (LLMs don't do arithmetic in prod).
Job seekers — role inquiry + screening, process status (honest, no false
hope), misrouted-buyer re-route. Post-sale — payment/handover status
(identity-gated), snagging/complaint ticket intake, document requests.

**Guardrails (all audiences):** no guaranteed returns; no legal/tax/visa
advice (facts + "verify with a consultant" only); no discounts/negotiation;
no fabricated availability/dates/prices — retrieval miss means saying so; no
scarcity pressure unless verifiable in the availability table; **never
collect payment details in chat.** Internal: never speculate beyond KB.
Job seekers: no salary figures, no sponsorship promises, no implied selection.
Post-sale: never extrapolate construction timelines beyond the last official
milestone; never discuss another client's unit.

> **Key decision:** escalation is biased early-and-cheap — a premature
> handoff costs an agent two minutes; a wrong commitment costs a deal.

## 5. Technology decisions

1. **Vector DB: pgvector** (additive column on `knowledge_chunks`). Postgres
   already in prod; corpus <20k chunks for years; SQL-layer ACLs; hybrid
   search in one query; zero new ops surface. Qdrant/Weaviate/Pinecone solve
   scale problems we will never have. (Matches the architecture review.)
2. **Retrieval: hybrid** — pgvector + existing lexical leg, RRF (k≈60).
   Entity-dense queries ("Binghatti Skyrise payment plan") need the lexical
   leg; vocabulary-mismatch and Arabic need the vector leg (Postgres has no
   Arabic stemmer). **No HyDE, no re-ranker at launch** — both eval-gated.
   Structured-first routing: fact-sheet fields / availability / schedules
   answer from SQL and skip retrieval entirely. K=4 chunks.
3. **Embeddings (NEW decision, 2026-06-12): BGE-M3 via hosted API.**
   DeepSeek offers no embeddings endpoint. BGE-M3 is the architecture
   review's top verified Arabic pick (70.99, arXiv 2506.06339) and is hosted
   on OpenRouter at **$0.01/1M tokens** and on SiliconFlow (8192-token
   inputs). At our corpus + query volume this is effectively free; cache
   embeddings by chunk hash. Self-hosting on this pids-capped OpenVZ box is
   rejected. The Arabic mini-eval (BGE-M3 vs mE5-large vs Qwen3-Embedding)
   stands, operator-gated, before bulk re-embedding.
4. **Single model integration, four routed profiles** — "separate
   deployments" is a category error for an API model. Isolation = router +
   tool subsets + SQL ACL. One eval suite gates all four. The DeepSeek-vs-
   Haiku tool-calling comparison at 8–12 tools remains an eval-harness task
   gating the model swap (architecture review item 5).
5. **Session state: Redis hot + Postgres durable.** n8n memory nodes
   rejected — state outside the reply path, uninspectable, unusable by evals.
6. **Latency:** mark-read + typing indicator <500ms (perceived latency
   collapses); 2.5s debounce (kills triple-replies AND triple LLM calls);
   short outputs by design (`max_tokens` ≈ 300); 12s timeout → retry → 
   templated fallback; everything else (CRM, notifications, summaries)
   post-reply async; no streaming (WhatsApp has no surface for it).

> **Key decision:** every choice removes a moving part. The only new
> infrastructure in this entire blueprint is one pgvector column and one
> embeddings API key.

## 6. Build roadmap

**MVP = potential-client qualification, EN+AR, LeadRat handoff** — revenue-
direct, half-built, least new knowledge required. Ship bar: ≥70% of
conversations reach ≥4 slots or clean handoff; zero guardrail violations in
evals; lead in LeadRat with summary <60s.

- **Phase 1 — buyer bot to production (~8 pw):** LeadRat credentialed API
  spike (token flow, tier/tag field — kickoff K3); pgvector migration +
  hybrid retrieval behind `search_knowledge`; prompt modules behind the
  intent route (cache-stable order); tool consolidation to the 8–12 map;
  eval suite to ~60 cases incl. adversarial guardrail probes; team verifies
  the 4 staged figure docs → kb-pack import → enable. Exit: 2-week shadow on
  the returned number → cutover. (While WhatsApp is suspended: everything
  here EXCEPT cutover runs in Playground/evals.)
- **Phase 2 — internal agents + post-sale intake (~9 pw):** staff allowlist
  mode, commission/availability tables, structured-first lookup, drafting
  tool (fastest payback per hour of anything here); post-sale **intake-only**
  (identity, tickets, n8n routing, SLAs); parallel ops track: post-sale data
  audit (gates Phase 3, mostly not engineering).
- **Phase 3 — post-sale status + job seekers + measurement (~6 pw):**
  read-only status flows over audited tables; job-seeker screening (smallest
  build); returning-lead re-engagement; re-ranker iff retrieval evals demand;
  metrics digest matured.

~23 person-weeks over ~16 calendar weeks at realistic 2-person allocation.

**Top 5 risks:** (1) LeadRat API thinner than docs suggest → week-1
credentialed spike; adapter isolation caps blast radius. (2) Hallucinated
facts → structured-first, dated chunks, refusal library, eval gates, shadow
period. (3) VPS contention (pids.max=1000 OpenVZ) → token/latency budgets,
per-conversation rate limits, 20-concurrent load test, VPS upgrade as cheap
escape hatch. (4) DeepSeek outage → timeout/retry/fallback + config-level
OpenAI-compatible secondary endpoint, switchable without deploy.
(5) Meta policy/quality → service-window discipline, approved utility
templates, AI disclosure, weekly quality-rating check (number is suspended
NOW — warm it carefully when it returns).

> **Key decision:** post-sale *status* ships last despite retention value —
> the only flow where bad data is worse than no bot, gated on the ops audit.

## 7. Success metrics

| Audience | Primary KPI (90-day target) | Secondary |
|---|---|---|
| Clients | qualified-lead rate ≥35% of inbound | time-to-qualification; after-hours share; LLM cost/qualified lead <$0.10; handoff-to-contact time |
| Agents | weekly active agents ≥60% | answers without `kb.gap`; monthly 1-question time-saved poll in the bot |
| Job seekers | screened-profile completion ≥50% | HR minutes saved; shortlist precision |
| Post-sale | ticket containment ≥80% | owner response vs SLA; repeat-contact rate |
| System | guardrail violations = 0 | p90 latency <12s; cost/conversation; eval pass trend |

**Framework (no analytics stack):** one `events` table written from the
worker (`conversation.started`, `slot.filled`, `lead.qualified`,
`handoff.completed`, `kb.gap`, `guardrail.refusal`, `ticket.created`,
`llm.call` w/ tokens+latency+cost) → one version-controlled SQL view per KPI
→ n8n Monday digest (ten numbers, one message) → 10 random transcripts/week
reviewed against a 5-point rubric (feeds the eval suite) → monthly LeadRat
export of bot-sourced lead stage progression (the only number management
ultimately cares about).

## 8. Decision log

| # | Decision | Alternatives | Rationale |
|---|---|---|---|
| 1 | AI = capture/qualify/retrieve/route; never close/promise/adjudicate | autonomous sales agent | failure-cost asymmetry; standing qualification principle |
| 2 | Runtime in channels_manager; n8n async-only | n8n as engine; new service | one failure domain on the latency path; already the live pattern |
| 3 | Slot-based deterministic flows; LLM extracts, code decides | free-form LLM dialogue | settled operator decision 2026-06-12; evaluable, cheap, predictable |
| 4 | One disclosed persona "Sara", per-audience modules | per-audience personas; undisclosed | Meta policy + trust; one prompt codebase |
| 5 | pgvector | Qdrant/Pinecone/Weaviate | already running; <20k chunks; SQL ACLs; zero ops; matches arch review |
| 6 | Hybrid RRF, structured-first; no HyDE/re-ranker at launch | naive vector; full advanced RAG | lexical leg needed for entities, vector leg for Arabic/paraphrase; rest is eval-gated |
| 7 | BGE-M3 via OpenRouter/SiliconFlow API | self-host; mE5; Qwen3; OpenAI | DeepSeek has no embeddings API; top verified Arabic score; $0.01/1M; self-hosting rejected on pids-capped VPS; mini-eval before bulk re-embed |
| 8 | One model integration, 4 routed profiles | per-audience deployments | category error for API models; isolation = router + ACL; one eval suite |
| 9 | Redis hot + Postgres durable; no n8n memory | n8n memory; PG-only | both in prod; TTL maps to 24h window; eval-harness compatible |
| 10 | Latency = ack-theater + debounce + lean prompts + 12s fallback | streaming hacks; bigger infra | no streaming surface on WhatsApp; lean prompts are cost AND latency strategy |
| 11 | MVP = buyer qualification; post-sale status last | post-sale first; all at once | revenue-direct + half-built vs the one flow where bad data < no bot |
| 12 | Job-seeker knowledge in-prompt, no RAG | embed hiring docs | ~2 pages; retrieval is pure overhead |
| 13 | KB gap-driven, effective-dated, prices expire | bulk encyclopedia | standing operator correction; expiry converts staleness into capture |
| 14 | Metrics = events table + SQL views + n8n digest + weekly rubric | PostHog/Metabase; nothing | 2-person team; 10 KPIs fit one Monday message; rubric feeds evals |
| 15 | Every prompt/KB/tool change gated by tools/agent-eval, paced, operator-approved | ship-and-watch | standing rules; TPM cap 429s back-to-back runs |
