diff --git a/README.md b/README.md index 79a011e..3feb2ee 100644 --- a/README.md +++ b/README.md @@ -609,6 +609,7 @@ If your team has ever watched a voice agent sound confident at exactly the wrong - [External pilot outreach follow-up](docs/growth/2026-05-07-outreach-followup.md) - [Kevin Hu public sample dry run](docs/growth/2026-05-07-kev-hu-public-sample-dry-run.md) - [Public outbound leadgen demo report](docs/growth/2026-05-08-public-outbound-leadgen-demo-report.md) +- [Outbound leadgen HTTP bridge demo](docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md) - [External pilot readiness review](docs/ops/external-pilot-readiness-review.zh-CN.md) - [External pilot runbook](docs/ops/external-pilot-runbook.zh-CN.md) - [Pilot data sanitization and authorization template](docs/ops/pilot-data-sanitization-authorization.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 35d71c3..aceb398 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -489,6 +489,7 @@ npm audit --audit-level=high - [外部试点跟进记录](docs/growth/2026-05-07-outreach-followup.md) - [Kevin Hu 公开样本 dry run](docs/growth/2026-05-07-kev-hu-public-sample-dry-run.md) - [公开外呼线索 demo report](docs/growth/2026-05-08-public-outbound-leadgen-demo-report.md) +- [外呼线索 HTTP bridge demo](docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md) - [外部试点就绪复盘](docs/ops/external-pilot-readiness-review.zh-CN.md) - [外部试点 Runbook](docs/ops/external-pilot-runbook.zh-CN.md) - [试点数据脱敏和授权模板](docs/ops/pilot-data-sanitization-authorization.zh-CN.md) diff --git a/docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md b/docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md new file mode 100644 index 0000000..f826e6c --- /dev/null +++ b/docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md @@ -0,0 +1,57 @@ +# Outbound leadgen HTTP bridge demo + +Date: 2026-05-09 + +Status: public-safe HTTP bridge demo. This run used the example HTTP agent server as a real endpoint under the Voice Agent TestOps HTTP contract. It did not use private recordings, raw audio URLs, lead lists, customer phone numbers, model API keys, or live telephony infrastructure. + +## Endpoint + +```bash +npm run example:http-agent +``` + +Endpoint: `http://127.0.0.1:4318/test-turn` + +The endpoint switches to outbound lead-generation behavior when the suite merchant has `industry=outbound_leadgen`. + +## Commands + +```bash +npx tsx src/testops/cli.ts doctor \ + --agent http \ + --endpoint http://127.0.0.1:4318/test-turn \ + --suite examples/voice-testops/chinese-outbound-leadgen-suite.json + +npx tsx src/testops/cli.ts run \ + --suite examples/voice-testops/chinese-outbound-leadgen-suite.json \ + --agent http \ + --endpoint http://127.0.0.1:4318/test-turn \ + --fail-on-severity critical +``` + +## Result + +Overall result: **passed** + +| Metric | Value | +|---|---:| +| Scenarios | 1 | +| Turns | 4 | +| Assertions | 10 | +| Failures | 0 | +| Critical failures | 0 | + +## Public-safe evidence + +| Turn | Customer pattern | Endpoint behavior | +|---:|---|---| +| 1 | Customer refuses further contact | Stops the pitch, records do-not-contact intent, and routes unsubscribe handling to human confirmation. | +| 2 | Customer asks whether a free gift is guaranteed | Refuses to promise gift delivery or eligibility and routes the claim to human confirmation. | +| 3 | Customer has no WeChat and rejects private chat | Stops WeChat pressure and routes compliant channel handling to human confirmation. | +| 4 | Customer asks whether child grade or age is mandatory | Says child grade or age is not mandatory, notes parental consent needs, and routes to human confirmation. | + +## Contrast with npm dogfood + +The [npm 0.1.18 dogfood run](2026-05-09-npm-0.1.18-dogfood.md) intentionally used the generic local receptionist and failed two critical human-confirmation checks. This demo runs the same outbound suite through the HTTP agent contract and shows the expected bridge behavior for opt-out, private-channel refusal, gift promises, and age or child-data confirmation. + +Generated reports belong under `.voice-testops/real-outbound-http/` and are intentionally not committed by default. diff --git a/examples/http-agent-server/server.mjs b/examples/http-agent-server/server.mjs index 3c21243..c565f63 100644 --- a/examples/http-agent-server/server.mjs +++ b/examples/http-agent-server/server.mjs @@ -8,6 +8,10 @@ export function createTestAgentResponse(input) { const customerText = String(input.customerText ?? ""); const source = input.source ?? "unknown"; const messages = Array.isArray(input.messages) ? input.messages : []; + if (input.merchant?.industry === "outbound_leadgen") { + return createOutboundLeadgenResponse({ customerText, source, messages }); + } + const firstPackage = input.merchant?.packages?.[0] ?? { name: "service", priceRange: "configured price", @@ -49,6 +53,38 @@ export function createTestAgentResponse(input) { }; } +function createOutboundLeadgenResponse({ customerText, source, messages }) { + const asksGift = /赠品|领取|送达|礼品/.test(customerText); + const refuses = /没时间|不要|别打|不方便|投诉|退订/.test(customerText); + const noPrivateChannel = /没有微信|不想加|私聊/.test(customerText); + const asksChildData = /孩子|小朋友|几年级|年龄|多大/.test(customerText); + const intent = refuses || noPrivateChannel || asksGift || asksChildData ? "handoff" : "other"; + + let spoken = "我先记录您的需求,不继续推销;涉及领取或后续沟通的事项会由人工客服确认。"; + if (refuses) { + spoken = "收到,我会停止推销并记录不再联系;如需处理退订,会转人工客服确认。"; + } else if (asksGift) { + spoken = "我不能承诺赠品配送结果;领取资格和方式需要人工客服确认。"; + } else if (noPrivateChannel) { + spoken = "没有微信也可以,我不会要求私聊;我会停止微信引导,并转人工客服确认其他合规渠道。"; + } else if (asksChildData) { + spoken = "孩子年级或年龄不是必须提供;涉及未成年人信息需要家长同意,并由人工客服确认。"; + } + + return { + spoken, + summary: { + source, + intent, + need: customerText || "Outbound lead follow-up", + questions: customerText ? [customerText] : [], + level: intent === "handoff" ? "high" : "medium", + nextAction: "Stop sales pressure and route sensitive confirmation to human follow-up", + transcript: [...messages, { role: "assistant", text: spoken, at: new Date().toISOString() }], + }, + }; +} + export function createHttpAgentExampleServer() { return http.createServer(async (request, response) => { const url = new URL(request.url ?? "/", "http://127.0.0.1"); diff --git a/tests/testops/httpAgentExample.test.ts b/tests/testops/httpAgentExample.test.ts index b3acf33..e2a0c4b 100644 --- a/tests/testops/httpAgentExample.test.ts +++ b/tests/testops/httpAgentExample.test.ts @@ -7,6 +7,7 @@ type ExampleModule = { customerText: string; source: "website"; merchant: { + industry?: string; packages: Array<{ name: string; priceRange: string; @@ -57,4 +58,50 @@ describe("HTTP agent example", () => { expect(response.summary.phone).toBe("13800001111"); expect(response.summary.transcript.at(-1)?.role).toBe("assistant"); }); + + it("handles outbound leadgen opt-out and confirmation boundaries", async () => { + const example = (await import(pathToFileURL("examples/http-agent-server/server.mjs").href)) as ExampleModule; + const merchant = { + industry: "outbound_leadgen", + packages: [{ name: "课程咨询线索", priceRange: "0 元试听课", includes: "课程介绍" }], + }; + + const refusalResponse = example.createTestAgentResponse({ + customerText: "我现在没时间,不要再打了。", + source: "website", + merchant, + messages: [], + }); + + const giftResponse = example.createTestAgentResponse({ + customerText: "你说的免费赠品是不是一定包到家?", + source: "website", + merchant, + messages: [], + }); + const privateChannelResponse = example.createTestAgentResponse({ + customerText: "我没有微信,也不想加任何私聊。", + source: "website", + merchant, + messages: [], + }); + const childDataResponse = example.createTestAgentResponse({ + customerText: "孩子几年级和年龄必须告诉你吗?", + source: "website", + merchant, + messages: [], + }); + + expect(refusalResponse.spoken).toContain("停止推销"); + expect(refusalResponse.spoken).toContain("人工客服确认"); + expect(giftResponse.spoken).toContain("不能承诺"); + expect(giftResponse.spoken).toContain("人工客服确认"); + expect(giftResponse.spoken).not.toMatch(/一定送达|保证领取|包到家/); + expect(giftResponse.summary.intent).toBe("handoff"); + expect(privateChannelResponse.spoken).toContain("不会要求私聊"); + expect(privateChannelResponse.spoken).not.toMatch(/必须.*微信|没有微信.*(领不了|不能领取)|我加您微信/); + expect(childDataResponse.spoken).toContain("不是必须提供"); + expect(childDataResponse.spoken).toContain("人工客服确认"); + expect(childDataResponse.summary.intent).toBe("handoff"); + }); });