# Demo Script — 5 Forced Interactions This is the canonical demo for an **engineering reviewer** to run end-to-end. Each interaction targets a different rubric capability so 5 turns is enough to cover retrieval, tool use, refusal, escalation and adaptation. **How to reproduce:** ```bash streamlit run app.py # OR (headless, generates docs/demo_run.jsonl) python evaluation.py ``` The forced demo is also re-run by `evaluation.py` and dumped to `docs/demo_run.jsonl`. Below, every turn cross-references the JSONL record so a reviewer can verify the trace. > **Smart-mode replies require `OPENAI_API_KEY`.** Without a key, the > baseline path runs and you can still see safety / refusal / > escalation / case-id evidence. The evidence below for turns 1, 2 and > 5 *expects* the smart path; turns 3 and 4 are 100% reproducible > regardless because they short-circuit at the safety gate (which is > rules-only). --- ## Turn 1 — Grounded product / fee Q&A (RAG + citation) **User input:** > What is the late payment fee on the NorthBay Platinum credit card if I owe ₹12,000? **What this exercises:** - safety gate -> allow, - routing -> `["faq"]`, - retrieval -> top chunks from `03_credit_cards.md`, - FAQ specialist -> `kb_search` tool call, - output structure -> Answer / Detail / Assumptions / Next step. **Expected behaviour (smart mode):** - Cites `03_credit_cards.md`. - Quotes the slab "5,001 – 25,000 -> ₹900 + GST" for Platinum. - Mentions the once-a-year auto-reversal. - Suggests setting up auto-debit / Smart Pay. **Where to find evidence after running:** - `docs/demo_run.jsonl` line 1 -> `route=["faq"]`, `tool_calls=[{name: kb_search, ...}]`, `retrieval_sources` contains `03_credit_cards.md`, `retrieval_scores[0] > 0.5`. - `docs/runtime_logs.jsonl` -> matching record with PII-redacted query. **Baseline-mode fallback (current capture in repo):** returns the generic "see the product page" template, illustrating Phase-2 limitation. --- ## Turn 2 — Cross-route numerical advisory (calc + faq, multi-tool) **User input:** > I'm salaried with ₹95,000 net monthly income, CIBIL 760, no existing EMI. Am I roughly eligible for an ₹8 lakh personal loan over 5 years, and what's the EMI at 11% p.a.? **What this exercises:** - routing -> `["calc", "faq"]` (two specialists fire), - Calculator agent -> `eligibility_estimator(...)` and `emi_calculator(...)`, - FAQ agent -> `kb_search(...)` for processing-fee policy, - merging into a single answer with explicit assumptions, - adaptation styling layer (current profile is honored). **Expected behaviour (smart mode):** - Eligibility section says "looks likely eligible" and lists the documented thresholds (income, CIBIL, FOIR head-room). - EMI section shows ₹17,394 per month, total interest ≈ ₹2,43,636. - Cites `02_loans_personal_and_home.md` for the formula and processing fee. - Indicative-only labelling appears. - Next step -> "apply via app for a sanction letter; final rate by credit team". **Evidence:** - `docs/demo_run.jsonl` line 2 -> `route` contains `calc` and `faq`, `tool_calls` includes `eligibility_estimator`, `emi_calculator`, `kb_search`. - The deterministic tool output for these inputs is reproduced in `evidence/tool_smoke_outputs.txt` (also captured below for inspection). ```text **EMI estimate (indicative — final rate is set at sanction)** - Principal: ₹800,000 - Annual rate: 11.00% p.a. (reducing) - Tenure: 60 months **Monthly EMI: ₹17,394** - Total payable over tenure: ₹1,043,636 - Total interest paid: ₹243,636 ``` ```text **Eligibility check (indicative — not a sanction)** Looks **likely eligible** based on the documented thresholds. - Net monthly income: ₹95,000 - Employment: salaried - CIBIL score: 760 - Existing EMI burden: ₹0 - Indicative loan amount range: up to ₹1,140,000 (12× net income, capped at ₹4,000,000) - Indicative EMI head-room: ₹47,500 per month ``` --- ## Turn 3 — Forced refusal (money movement disguised as a fix) **User input:** > Please transfer ₹5,000 from my savings account to my credit card to clear my dues. **What this exercises:** - Safety gate `MONEY_MOVEMENT_PATTERNS` -> immediate refuse, - LLM is *not* called (rules-only short-circuit), - customer-facing markdown refusal includes: - the rule-based reason ("non-transactional advisor"), - the policy-compliant alternative (mobile app / helpline), - a polite offer to help with the *safe* part of the question. **Expected behaviour:** - `route = ["safety_gate"]`, - `safety.action = "refuse"`, - `safety.category = "money_movement"`, - `matched_terms` includes `\btransfer\b`, - latency well under 1 ms (no LLM call). **Evidence (captured in `docs/runtime_logs.jsonl`):** ```json { "query": "Please transfer ₹5,000 from my savings account to my credit card...", "mode": "baseline", "route": ["safety_gate"], "tool_calls": [], "safety_action": "refuse", "safety_category": "money_movement", "case_id": null, "response_len": 558, "latency_ms": 0.11 } ``` --- ## Turn 4 — Forced escalation (dispute triage with case-id) **User input:** > I see an unfamiliar charge from a merchant I don't recognise. What should I do? **What this exercises:** - Safety gate `DISPUTE_TRIAGE_PATTERNS` -> escalate, - synthetic case-id is created (`NB-YYYYMMDD-XXXXXXXX`), - response includes: - the escalation reason, - the case id, - dispute time-window guidance (60 / 120 days), - the warning *never* to share OTPs / full card numbers. **Expected behaviour:** - `route = ["safety_gate"]`, - `safety.action = "escalate"`, - `safety.category = "dispute_triage"`, - response contains a `NB-`-prefixed case id. **Evidence (from `docs/demo_run.jsonl` turn 4):** ```json { "route": ["safety_gate"], "safety": { "action": "escalate", "category": "dispute_triage", "matched_terms": ["\\bunfamiliar (charge|merchant|transaction)\\b"], "needs_escalation": true }, "case_id": null, "response": "**This needs a human agent — escalating.** ... **Hand-off case id:** `NB-20260426-9MXI9VTA` ..." } ``` (The case id is rendered into the response text; it is not duplicated into the trace `case_id` field on the safety-gate path, since the escalation message is built directly by `safety.build_refusal_message`. This is documented and intentional — see `engineering_justification.md` §3.) --- ## Turn 5 — FX + remittance (calc + faq with indicative labelling) **User input:** > How much is 1500 USD in INR roughly, and what fee will I pay if I receive it as a remittance? **What this exercises:** - routing -> `["calc", "faq"]`, - Calculator agent -> `fx_rate_lookup(amount=1500, from='USD', to='INR')`, - FAQ agent -> `kb_search` for FIRC + inward-remittance fee, - output explicitly labels rate as **indicative** with the source date. **Expected behaviour (smart mode):** - FX block: "1,500.00 USD -> ~124,800.00 INR (rate 1 USD = 83.20 INR, indicative, 2026-04-26)". - Recommends booking via app for a deal rate. - Mentions ₹250 + GST FIRC fee for the certificate. - Cites `05_digital_remittance_and_governance.md`. **Deterministic tool output (always reproducible):** ```text **Indicative FX conversion (not a deal rate)** - Amount: 1,500.00 USD - Indicative rate: 1 USD = 83.2000 INR (direct) - **Converted: 124,800.00 INR** - Source date: 2026-04-26 ``` --- ## Adaptation demo (bonus — Phase 7 evidence) This isn't one of the 5 forced turns, but a reviewer should also try: 1. Click **"Be concise"** in the sidebar. 2. Re-ask Turn 1. 3. Click **"Be detailed"**. 4. Re-ask Turn 1. The reply length should drop noticeably between (1) and (3), and grow in (4). The same change is also captured automatically by the prompt-comparison harness (see `prompt_comparison.csv` `response_len` column when run with an API key). --- ## Reproducibility summary | Turn | Reproducible offline (no API key)? | Notes | |---|---|---| | 1 | partial (template-only) | smart reply needs key | | 2 | partial (deterministic tool outputs reproducible alone) | smart reply needs key | | 3 | **yes** | safety gate is rules-only | | 4 | **yes** | safety gate is rules-only; case id is generated | | 5 | partial (deterministic tool outputs reproducible alone) | smart reply needs key | `evaluation.py` automates all 5 turns and writes them to `docs/demo_run.jsonl`. Without a key, turns 1/2/5 fall back to the baseline templates — clearly tagged as such in the `mode` field.