> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Call Input Smoke

## 0. 这份 smoke 要回答什么

Task Accuracy Baseline v1 证明的是：AI 能不能把 `contact / call / message / lead / task` context 转成正确的 `taskDecisions[]`。

Call Input Smoke 在它前面检查一层：

> 单次 call analysis 写进 `calls` 表的 structured facts 是否够准、够完整、够可解释。

如果 task 错了，先不要马上怪 `taskDecisions[]` prompt。可能是上游 call facts 已经歪了：

- Call analysis 把 `member_package` 误导成别的业务意图。
- `customerType` 写错，导致后续 contact lifecycle 判断跑偏。
- `followUpNeeded` / `followUpReasons` 没写，导致 unresolved objective 被当成已解决。
- `direction` / `fromPhoneNumber` / `toPhoneNumber` / `storeId` 归因不清，导致 AI 用错门店上下文。

这不是完整的 `Call Accuracy Foundation`。它是 demo 前的 lightweight upstream sanity check：确认 Task Accuracy 依赖的 call facts 没有明显错。

## 1. 它在 pipeline 里的位置

```text
Raw call transcript + call metadata
        |
        v
ai-analysis-processor
        |
        v
Neon calls
  direction
  startTime
  duration
  primaryCategory
  primarySubcategory
  primaryOutcomeResult
  creditCardCaptured
  followUpNeeded
  followUpReasons
  customerType
  executiveSummary
        |
        v
contacts-analyzer RECENT CALLS context
        |
        v
contact profile + taskDecisions[]
```

这份 smoke 覆盖的是：

```text
transcript / call metadata
   -> ai-analysis-processor output
   -> calls row
   -> contacts-analyzer RECENT CALLS line
```

它不测试 raw audio transcription 质量，也不测试 Vapi。demo 前可以直接用已转写 transcript 或已落库 call row 来验证。

## 2. 当前 production context 已经喂了什么

`contacts-analyzer` 看到的 `RECENT CALLS` 是半结构化文本：

```text
- [startTime] direction duration |
  primaryCategory/primarySubcategory |
  out=<primaryOutcomeResult> cc=<creditCardCaptured> fu=yes(reason) cust=<customerType> |
  executiveSummary
```

这些字段来自 `ai-analysis-processor` 的 call analysis output，再被 `contacts-analyzer` 当成 Layer 1 evidence 使用。

所以要分开验证：

- Call analysis 是否把 transcript 理解成正确 taxonomy。
- `calls` 表是否持久化了正确字段。
- `contacts-analyzer` prompt 是否真的拿到了这些字段。
- Prompt 里的 enum values 是否来自 `@retaintive/common/taxonomy/call-analysis`，不要手写漂移。

## 3. Smoke 的 review 原则

不要做所有全排列。这里按 call decision point 覆盖：

- Source identity：这是 inbound call、outbound call、voicemail call，还是系统/非客户流量？
- Recency：`startTime` 是否格式正确，是否能和 contact 的 current time / last activity 对上。
- Direction：Inbound / Outbound 是否影响 attribution 和 follow-up 判断。
- Taxonomy：`primaryCategory`、`primarySubcategory`、`outcome`、`customerType`、`followUpReasons` 是否符合 common taxonomy。
- Outcome：客户目标是否已解决，还是 attempted / pending follow-up。
- Safety：wrong number、vendor、corporate inquiry、DNC signal 是否保留。

这个 smoke 不是看模型“会不会编一个合理故事”，而是看 production call facts 是否足够支撑后续 contact / task 判断。

## 4. 第一批 smoke scenario set

每个 scenario 至少检查三件事：

- `DB facts`：`calls` row 里的字段是否写对。
- `Prompt context`：`contacts-analyzer` 的 `RECENT CALLS` line 是否带到了这些字段。
- `Expected implication`：这些 call facts 是否足够支持后续 contact / task decision。

### C-01 Additional class pack / add-on purchase

**业务状态**

现有会员打电话，说自己本月课包用完，想买一次额外 class 或 package。

**必须看到的 call facts**

- `calls.primaryCategory = revenue_impacting`
- `calls.primarySubcategory = member_package`
- `calls.customerType = existing_member`
- `calls.primaryOutcomeResult = attempted` 或 `success`
- `RECENT CALLS` 里出现 `revenue_impacting/member_package`
- `executiveSummary` 说明是 package / add-on / extra class，不是 membership renewal

**Expected implication**

Task taxonomy 上更接近 `upgrade`，不是 `renewal`。短期 UI label 可以显示成 `Upgrade / Add-on`；长期再把 `typeCategory` 抽象成更通用的 `objectiveType`。

### C-02 Billing / failed payment recovery

**业务状态**

会员的付款方式失败、卡过期，或需要补付款来恢复服务连续性。

**必须看到的 call facts**

- `calls.primarySubcategory = billing_issue`
- `calls.customerType = existing_member`
- `calls.followUpNeeded = yes` when unresolved
- `followUpReasons` 只能来自 common taxonomy，例如 `needs_manager_approval` 或 `complaint_feedback`
- `executiveSummary` 说明是 payment / billing continuity，不只是普通 service note

**Expected implication**

这类更接近 `renewal` / continuity recovery。AI 不应该把它当作普通 service note 忽略。

### C-03 Downgrade / freeze / cancel risk

**业务状态**

会员表达想 downgrade、freeze 或 cancel，但还没最终确认。

**必须看到的 call facts**

- `primarySubcategory` 是 `membership_downgrade`、`membership_freeze` 或 `membership_cancel`
- cancellation 语义要进 `topic_type = cancellation` when customer explicitly wants to cancel
- `outcome.result = pending_follow_up` when unresolved
- `executiveSummary` 说明客户的真实意图和 unresolved next step

**Expected implication**

如果 objective 未解决，后续 task 应该偏向 `cancellation_risk` 或 `retention`，而不是 `renewal`。

### C-04 Existing lead books intro

**业务状态**

Lead 接通电话并确认预约 intro / first class。

**必须看到的 call facts**

- Call direction 和 summary 能证明是 booking，不只是 generic inquiry。
- `primarySubcategory = intro_booking`
- `customerType = prospective_client`
- `primaryOutcomeResult = success` when booking is confirmed
- `executiveSummary` 说明已经 booked，不只是 interested

**Expected implication**

已有 lead task 应该 close as `booked`。不要因为已经 booked 再创建 intake、waiver、arrival logistics task。

### C-05 Outbound no-answer

**业务状态**

员工对一个 open objective 打电话，没人接。

**必须看到的 call facts**

- `calls.direction = Outbound`
- `callState = no_answer` 或 summary 能证明 no answer
- 没有客户新表达的 objection / DNC / close outcome
- 如果没有 transcript 或只有 no-answer metadata，summary 不应该伪造客户意图

**Expected implication**

通常只是给 existing task `record_progress`，不是 create new task，也不是随便 close。

### C-06 Wrong number / non-customer business inquiry

**业务状态**

来电是 wrong number、vendor、corporate sponsorship、partner、marketing pitch，和健身 customer lifecycle 无关。

**必须看到的 call facts**

- `customerType = other`，或历史 row 可能仍是 legacy `wrong_number`
- category/subcategory 能体现 `corporate_inquiry`、`other` 或 wrong-number 语义
- `executiveSummary` 保存有用事实，但不要伪造成 lead/member

**Expected implication**

后续 `contacts-analyzer` 应该把普通 fitness task no-op，或 close invalid existing task。历史 `cust=wrong_number` 也必须被当作 hard stop。

### C-07 Referral / bring-a-friend opportunity

**业务状态**

现有会员主动说要带朋友、问 guest pass、推荐家人，或留下 referral opportunity。

**必须看到的 call facts**

- `primarySubcategory = referral_program` 或 summary 明确提 referral / guest / friend
- `customerType = existing_member` 或有明确 customer identity
- `executiveSummary` 说明 referral opportunity 是否需要 staff follow-up

**Expected implication**

这可以支持后续 `referral` task。若同一个 contact 还有另一个 open task，Contact / Task UI 要能让 staff 看见 related open tasks。

### C-08 Churned member asks to rejoin

**业务状态**

前会员主动来电，说想重新加入或回来上课。

**必须看到的 call facts**

- `customerType = former_member`
- inbound customer-initiated re-engagement evidence
- `primarySubcategory = reactivation_purchase`
- 如果 interaction 当场完成 rejoin，outcome 要能显示 success；否则 follow-up remains open

**Expected implication**

后续 task 应该偏向 `win_back`。这不是 cold outbound win-back；是 customer-initiated reactivation。

## 5. 当前已发现的 call prompt contract 风险

### 5.1 Call taxonomy prompt drift

`ai-analysis-processor` 的 Zod schema 已经大量引用 `@retaintive/common/taxonomy/call-analysis`。但 classify base prompt 曾经还有手写 enum 文案，例如 customer type、category、outcome、follow-up reason、secondary topic forbidden list。

这类手写列表的风险不是 DB 不认，而是 prompt contract 和 common taxonomy 漂移：模型读到旧定义，Zod 却按新 schema 校验。

处理原则：

- enum values / allowed values / forbidden list 必须从 common taxonomy 生成。
- 业务判断规则和例子可以留在 prompt，但值本身不要手写成第二份 source of truth。
- Prompt snapshot 必须让 assembled system prompt diff 可 review。

### 5.2 Legacy call rows 要保留解释

历史 call rows 可能还带旧的 `customerType` value，例如 `existing`、`prospect`、`non_member`、`wrong_number`。在完成数据迁移或 renderer normalization 前，prompt 必须告诉模型这些 legacy aliases 怎么理解。

最重要的是：`cust=wrong_number` 必须还是 hard-stop non-customer，不能因为新 taxonomy 只写 `other` 就失去保护。

### 5.3 Call Accuracy Foundation v1 的长期形态

`#1132` 里讨论的 `subcategory -> primary category` 方向长期是对的，但它不应该做成 prompt 里又一份手写 if-else。更稳的形态是：

```text
Transcript + call metadata
  ↓
LLM 只提出 topic_candidates
  - subcategory
  - outcome
  - evidence
  - confidence / reasoning
  ↓
Deterministic canonicalizer
  - subcategory 是否在 common taxonomy
  - primaryCategory 从 taxonomy 反推
  - topic_type 是否需要特殊处理，例如 membership_cancel -> cancellation
  - outcome 是否允许
  - primary / secondary 用业务优先级决定
  ↓
旧 shape 输出
  - primaryCategory
  - primarySubcategory
  - secondaryTopics
  - primaryOutcomeResult
  ↓
后续 contacts-analyzer / DB / UI 不需要大改
```

这和 AI Agent Foundation 的关系是：

- `Facts`：transcript、direction、from/to phone、storeId、duration、recording metadata。
- `Tenant / Vertical Taxonomy Registry`：当前是 gym taxonomy；未来 fraud、医疗、BPO 可以换自己的 taxonomy pack。
- `AI Decision`：LLM 提出候选 topic，不直接拥有最终 canonical category。
- `Deterministic Canonicalizer`：用 `@retaintive/common/taxonomy/call-analysis` 做 source of truth，避免 prompt 和 schema 漂移。
- `Eval / Replay`：Call Accuracy smoke / golden eval 验证 candidate extraction 和 canonical output 是否稳定。

## 6. Demo 前怎么用这份 smoke

每个 demo candidate 至少检查这一条链：

```text
transcript / call metadata
  -> ai-analysis-processor output
  -> calls row
  -> contacts-analyzer RECENT CALLS context
  -> contact / task decision
  -> Task UI / timeline
```

建议最小操作：

1. 从 8-12 个 demo candidate 里选 3 个真实端到端 case。
2. 对每个 case 保存一份 prompt context 或 OpenRouter log。
3. 在 DB 里确认 `calls` 的关键 structured fields。
4. 如果错了，先归因：call transcript 错、call taxonomy 错、prompt contract 错，还是 downstream contact / task decision 错。
