Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions docs/growth/2026-05-09-outbound-leadgen-http-bridge-demo.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions examples/http-agent-server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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");
Expand Down
47 changes: 47 additions & 0 deletions tests/testops/httpAgentExample.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type ExampleModule = {
customerText: string;
source: "website";
merchant: {
industry?: string;
packages: Array<{
name: string;
priceRange: string;
Expand Down Expand Up @@ -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");
});
});