feat: add configurable Bailian AI cell call runtime
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# agent-call:LLM 与音色可配置电话对话开发与验收计划
|
||||
|
||||
**版本:** v1.0
|
||||
**日期:** 2026-09-14
|
||||
**状态:** P1/P2 配置契约与 Bailian 真实 ASR/LLM/CosyVoice 文件链路已落地并验收;P3/P4 真实电话、MQ/OSS 和人工听测仍未完成。本文仍是范围与门禁计划,不是完整真实电话完成报告。
|
||||
**目标:** 指定 LLM 模型、Prompt、TTS 模型与音色,一键完成文本、音频文件或真实电话多轮对话测试。
|
||||
|
||||
## 1. 权威来源与当前基线
|
||||
|
||||
- [最终计划](最终开发部署监控与验收计划_v1.0.md)仍是总实施入口;本文细化 D04、D06、D07、D08、D11、D12 的单通话 AI 联调,不替代整体调度、恢复与生产验收。
|
||||
- [主契约](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)是 HTTP/MQ 字段、路径、状态的唯一维护来源。本文的新增配置项、命令行和报告项是设计要求,不是已冻结的业务协议。
|
||||
- 已有机器契约见 `docs/contracts/executor.openapi.yaml`、`saas.openapi.yaml`、`mq.schema.json`,集中命令样例见 `docs/contracts/examples/call.execute.json`。
|
||||
- 用户反馈最新外呼已正常接通;2026-09-14 最近探针记录 100→183→200、PCMA/8000,结束后无活动通道。该事实只作为 SIP 接通基线,不证明双向媒体、ASR、LLM、TTS、录音及完整 SaaS 交接通过。
|
||||
- `deploy/sip_probe.py` 是真实 SIP 拨号探针,不是完整 AI 通话执行器;`agent_call/core.py` 的 `_run_call` 仍包含模拟通话流程,`agent_call/mocks.py` 为模拟记录,不能用它们的成功事件替代真实媒体证据。
|
||||
- 仅复用现有 ASR 协议能力,不复制或启用 `voice_test` 的 LLM/TTS;`agent_call/bailian.py` 使用独立 Bailian LLM/CosyVoice 适配。ASR Web 页面仍不承载 LLM/TTS,未配置供应商时页面、日志与报告必须明确标记阻塞或模拟。
|
||||
|
||||
## 2. 范围与不可变边界
|
||||
|
||||
### 2.1 首期交付
|
||||
|
||||
1. 可信、不可变的 AI 配置版本:可指定模型、UTF-8 Prompt 文件及音色。
|
||||
2. 首家 LLM 与首家 TTS 的真实流式适配,连接现有 ASR。
|
||||
3. Cell 内真实 ARI 事件、双向媒体和电话多轮对话;支持开场白、打断、静音处理和挂断清理。
|
||||
4. `text / audio / call` 三层一键测试,以及脱敏机器报告、人工听测登记和证据索引。
|
||||
5. 沿用现有 MQ 结果、文字与 OSS 录音交接,不建立第二条业务结果通道。
|
||||
|
||||
首期不做 Prompt 可视化编辑器、RAG、通用供应商框架、多供应商自动切换、工具调用平台或 1000 路容量承诺。供应商接入不足不阻塞隔离 Mock 开发,但阻塞对应真实验收。
|
||||
|
||||
### 2.2 必须遵守
|
||||
|
||||
- 电话测试脚本扮演测试 SaaS;通过租户独立 RabbitMQ 队列提交 `call.execute`,引用 `agent_version_id`。不得新增 HTTP 拨号入口或 HTTP 业务回调。
|
||||
- AI 配置发布与普通呼叫权限隔离;不借用 SIP 管理后台写接口或普通调度 Token 绕过配置权限。
|
||||
- MQ 不携带 API Key、任意供应商 URL 或任意 SIP 地址。供应商地址、凭据引用和模型授权由服务端校验。
|
||||
- 一通电话固定 Cell/出口;主叫、被叫改写和 PCMA 由授权线路配置解析,不逐呼修改共享 PJSIP 配置,不虚构备用线路。
|
||||
- 业务持久状态使用真实隔离数据库,集成测试使用真实测试 RabbitMQ;内存队列仅可作单元测试替身。
|
||||
- `mock / mixed / real` 按组件可识别;Mock 默认隔离真实外网;正式模式拒绝 Mock/测试凭证,不静默降级。
|
||||
- 本文不授权新建云资源、真实 AI 消费或立即拨号。真实执行另需有效预算、凭据和明确安排的测试授权。测试后保留现有 ECS/EIP,清理需用户另行确认。
|
||||
|
||||
## 3. 配置与契约工作包 P1
|
||||
|
||||
### 3.1 建议配置内容(待写入主契约冻结)
|
||||
|
||||
| 配置组 | 内容 | 校验要求 |
|
||||
| --- | --- | --- |
|
||||
| LLM | 供应商配置引用、模型、Prompt、生成参数、超时 | 模型受授权且支持所需流式协议;参数范围与供应商能力一致 |
|
||||
| TTS | 供应商配置引用、模型、音色、语速、音频格式 | 音色与模型兼容;输出格式可转换为电话侧音频;不静默替换 |
|
||||
| ASR | 现有供应商配置引用、语言、输入格式 | 明确采样率、声道、位深与分段/最终稿语义 |
|
||||
| 对话 | 开场白、断句、打断开关、静音处理、最长时长 | 有界等待和输出;失败行为明确,不无限循环 |
|
||||
| 安全与版本 | 租户范围、不可变版本、内容摘要、凭据引用 | 版本发布后不可覆盖;跨租户拒绝;密钥不进入快照 |
|
||||
|
||||
- CLI 读取 UTF-8 Prompt 文件;冻结模板变量的允许集合、转义规则、缺失处理、长度上限,不使用 `eval`、shell 或任意文件引用。
|
||||
- 先发布并确认可信快照,再提交引用该版本的执行命令。复用版本必须校验内容及租户一致,不能覆盖同 ID 的不同内容。
|
||||
- 呼叫受理和实际发起前验证版本存在且获授权;活动通话固定原快照,后续发布不热替换。
|
||||
- 配置同步来源、发布/读取路径、认证域、错误与版本规则先补主契约,再生成对应 OpenAPI、Schema 与正反例;不在本文擅定新业务路径、状态或错误码。
|
||||
- OpenAPI 是 HTTP 接口描述,不能据此默认采用 OpenAI 兼容协议。LLM/TTS 的请求、SSE/WebSocket、取消、错误和音频契约以选定供应商文档为准。
|
||||
|
||||
### 3.2 P1 退出条件
|
||||
|
||||
配置的合法/缺失/越权/冲突/模型音色不兼容用例通过;HTTP/MQ 样例与机器契约一致;脚本可取得可信 `agent_version_id`,而不是向 MQ 额外塞入未定义字段。
|
||||
|
||||
## 4. 执行与媒体链路
|
||||
|
||||
```text
|
||||
测试脚本 → 可信配置发布/读取 → agent_version_id
|
||||
→ RabbitMQ call.execute → 持久化/公平调度/资源预留
|
||||
→ Cell 本地执行器 → ARI 发起与真实接通事件
|
||||
→ 入站媒体 → ASR → LLM → TTS → 出站媒体 → 电话播放
|
||||
→ 持久化文字/状态/终态 → outbox → RabbitMQ
|
||||
→ 接通后录音 → 挂断封口 → OSS 校验 → MQ 录音结果
|
||||
```
|
||||
|
||||
### 4.1 媒体实现要求
|
||||
|
||||
- Cell 保持唯一活动 ARI 控制者。按实际 Asterisk 能力选定一种双向媒体接入方式并验证,不同时实现多套媒体方案;管理连接保持回环或受控管理网/TLS,不开放公网 ARI。
|
||||
- SIP 侧沿用 PCMA/8000;ASR/TTS 侧按真实供应商要求转换采样率、声道、编码及帧格式,优先复用现有可靠音频组件。用实测验证节拍、字节序、时长、音量、丢包与缓冲上限。
|
||||
- ASR 中间稿不直接无限触发 LLM;按最终稿/端点检测形成轮次。冻结 VAD、断句和去重策略,避免机器人回声形成自问自答。
|
||||
- LLM 增量输出按有界句段送 TTS;上下文、待合成文本、待播放音频均有上限与背压,不无界积压。
|
||||
- 每轮具备内部轮次/取消标识;打断时取消 LLM/TTS、清空可清理的播放缓冲、丢弃旧轮次迟到包。已进入远端缓冲的音频不能宣称完全撤回,记录可测停止边界。
|
||||
- 用户挂断、控制挂断或时长到期后,停止新增生成并清理媒体/AI连接;ARI 失联进入既有对账流程,不凭租约超时直接释放未知通话占用或重拨。
|
||||
- 生成文字、音频已发送与播放确认分开记录;没有播放证据时维持主契约允许的未知状态,不伪称用户已听到。
|
||||
- 录音按主契约接通后开始、单轨混音、挂断封口;OSS 交接失败保持可恢复事实,不产生虚假 ready。
|
||||
|
||||
## 5. 开发顺序与交付物
|
||||
|
||||
| 工作包 | 开发内容 | 交付物与退出条件 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| P1 配置契约 | 配置发布、授权、版本、参数及供应商协议冻结 | 主契约增补、机器契约、正反例;满足第3节 | 无;供应商资料并行协调 |
|
||||
| P2 独立 AI | 一家流式 LLM、一家流式 TTS、现有 ASR 接入;取消/错误与模式隔离 | 文本测试、音频文件测试;指定模型/Prompt/音色有请求及输出证据 | P1;真实测试需供应商资料 |
|
||||
| P3 真实通话 | Cell 执行路径、真实 ARI/RTP、音频转换、录音衔接 | 手机能听到 AI、ASR 能识别用户并驱动回答;不是探针 Wait 或 Mock 成功事件 | P2、现有调度与Cell缺口整改 |
|
||||
| P4 生命周期 | 开场白、上下文、打断、静音、超时、挂断、断连对账 | 三轮对话、取消迟到包、异常清理和幂等故障测试 | P3 |
|
||||
| P5 脚本与验收 | 分层入口、预检、持久运行清单、事件收集、报告及人工听测登记 | 第7节逐例登记,文档命令可复现 | P1起增量开发,最终依赖P4 |
|
||||
|
||||
本次产物位置:`scripts/test_ai_call.py`、`scripts/run_real_cell_call.py`、`scripts/accept_ai_call.py`、`configs/ai-test.example.yaml`、`configs/ai-test.bailian.example.yaml`、`prompts/test-call.txt`、`agent_call/ai_runtime.py`、`agent_call/bailian.py`、`agent_call/real_cell.py`、`tests/test_ai_runtime.py`、`tests/test_bailian.py`、`tests/test_real_cell.py`、`docs/contracts/ai-config.openapi.yaml`、`docs/contracts/ai-config.schema.json`、`docs/evidence/llm-voice-acceptance-20260914.json` 和 `docs/evidence/real-cell-call-20260914.json`。AI/媒体运行代码已提供真实 Cell ARI/RTP worker,但单次真实电话未通过媒体/三轮门禁。Mock `text`/`audio`/`call` 与 Bailian 真实 `text`/`audio` 已可复现;脱敏报告同时提供 Markdown 摘要。
|
||||
|
||||
## 6. 一键测试规范
|
||||
|
||||
### 6.1 分层入口
|
||||
|
||||
| 模式 | 输入/结果 | 外部副作用 |
|
||||
| --- | --- | --- |
|
||||
| `text` | 模型、Prompt、测试文本 → 流式回答和报告 | 不拨号;真实模式会调用 LLM |
|
||||
| `audio` | 合成/已授权音频 → ASR文字、LLM回答、TTS音频和报告 | 不拨号;真实模式会调用 AI |
|
||||
| `call` | 授权号码、配置版本与线路策略 → 电话对话和 MQ/录音证据 | 会拨号及调用 AI,必须显式授权 |
|
||||
|
||||
**已实现的 Mock 与真实文件入口:**
|
||||
|
||||
```bash
|
||||
python3 scripts/test_ai_call.py call \
|
||||
--profile configs/ai-test.example.yaml \
|
||||
--model 'mock-chat-v1' \
|
||||
--prompt-file prompts/test-call.txt \
|
||||
--tts-model 'mock-tts-v1' \
|
||||
--voice 'mock-neutral' \
|
||||
--callee '18601013734' \
|
||||
--max-duration 120 \
|
||||
--allow-real-call
|
||||
```
|
||||
|
||||
`120` 是该次测试时长示例,不是生产额度。Bailian real `text`/`audio` 只会调用真实 AI、不拨号;real `call` 仍需 Cell/线路/MQ/OSS 与明确电话授权,不能因文件探针通过而自动发起收费电话。
|
||||
|
||||
### 6.2 执行步骤及安全
|
||||
|
||||
1. 预检配置权限、供应商能力、凭据引用、租户绑定、MQ、必要存储依赖及线路/Cell可用性;配置错误在拨号前拒绝。预检不等于实际发起时的资源租约,发起前仍须复核。
|
||||
2. 发布/复用配置版本;持久保存运行清单及 `command_id / execution_id / trace_id / agent_version_id`,再提交 MQ。
|
||||
3. 一次运行只呼叫一个授权号码,不自动重拨或自动切换供应商。broker 确认不确定时保留原执行标识,按主契约恢复;脚本重启不得默认创建新执行。
|
||||
4. 事件由专用测试 SaaS inbox 按既有事件拓扑受控收集,不竞争消费生产 SaaS 队列,不改为一租户一事件队列。报告只关联本次租户和执行。
|
||||
5. CLI 等待超时不代表通话已结束。通过既有查询/授权控制和 Cell 事实核验;无法核验则记录不确定、停止新执行并提示处置,不全局强挂其他通话。
|
||||
6. 正常结束核验本次通道/连接/资源已清理,并等待有界的结果与资产交接;未完成项标记失败或阻塞,不伪报整体通过。
|
||||
7. 提供明确非零失败退出码;建议 `0=全部要求通过、1=测试失败、2=前置阻塞或证据不足`。人工听测未完成时不能退出为整体验收通过。
|
||||
|
||||
### 6.3 报告与证据
|
||||
|
||||
- 运行 ID、时间、代码/组件版本、profile 和逐组件模式、执行标识、配置摘要、所用线路/Cell。
|
||||
- 请求模型及供应商返回模型(若不返回,标记未验证)、Prompt 摘要、TTS模型/音色;不写入密钥或默认公开完整 Prompt。
|
||||
- SIP 接通证据、双向媒体计数、ASR分段、LLM首 token、TTS首音频、实际可观测播放状态、挂断与清理事实。
|
||||
- 单调时钟测量 ASR 最终稿、LLM首 token、TTS首音频、用户话音结束至回复播放、打断停止延迟;跨组件时钟不可直接相减,报告测量边界。
|
||||
- MQ事件ID、持久化/发布确认情况、OSS交接证据、逐例 PASS/FAIL/BLOCKED、人工听测结论。
|
||||
- 原始证据保存在受控忽略目录并设置最小访问权限;`docs/evidence/` 只收录脱敏摘要。录音、完整文字、号码与Prompt按登记保留期限清理,未经明确保留规则不发布真实内容。
|
||||
|
||||
## 7. 功能验收矩阵
|
||||
|
||||
所有用例记录模式、环境、输入、操作、预期、实际、证据路径和结论;计划中默认均为 **NOT_RUN(未执行)**。执行后只使用 PASS/FAIL/BLOCKED,不以“代码存在”替代测试。
|
||||
|
||||
| 编号 | 场景与操作 | 通过标准/必要证据 |
|
||||
| --- | --- | --- |
|
||||
| AI-01 | 发布配置并提交版本引用 | 命令通过现有Schema;可信快照与租户一致;MQ不含密钥/任意URL |
|
||||
| AI-02 | 缺失版本、越权版本、同版本异内容 | 明确拒绝;无真实INVITE、无供应商调用;原已发布版本不变 |
|
||||
| AI-03 | 非法模型、音色不兼容、Prompt变量缺失/越界 | 可本地确定的问题拨号前拒绝;供应商才可判定的错误明确留证,不替换模型/音色 |
|
||||
| AI-04 | 修改配置后再次执行 | 新执行使用新版本;活动通话继续原版本;摘要可核对 |
|
||||
| AI-05 | 指定模型与Prompt进行文本测试 | 请求模型、Prompt摘要准确;回答符合预置可核验约束;流式首token及终止有证据 |
|
||||
| AI-06 | 指定TTS模型与音色合成 | 请求参数准确;音频可解码、时长/格式正确;人工确认可听;不以请求参数单独证明音色听感 |
|
||||
| AI-07 | 音频文件走完整AI链 | ASR结果、LLM回答、TTS文件均关联同一测试;非固定Mock文字;样本预期与人工复核通过 |
|
||||
| AI-08 | 真实电话至少三轮问答 | 真实接通、双向媒体和三轮识别/回答证据齐全;手机端人工确认清晰可听、上下文连续、无明显自问自答 |
|
||||
| AI-09 | 开场白及指定Prompt约束 | 接通后按配置播放开场白;预置问答符合Prompt;未配置开场白不擅自生成固定营销话术 |
|
||||
| AI-10 | PCMA/PCM转换及持续音频 | 格式、采样率、时长、帧节拍可核验;无明显变速/截断;队列有界 |
|
||||
| AI-11 | AI播放中用户插话,注入迟到包 | 取消生成/合成并清理播放队列;旧轮次包不继续入队;停止延迟满足预登记测试阈值 |
|
||||
| AI-12 | 用户持续静音或无人应答 | 按配置有界等待/结束;不无限生成;无人应答不记为已接通AI通话 |
|
||||
| AI-13 | 用户挂断、授权控制挂断、最长时长到期 | 本次AI任务/媒体停止;已知结束的资源释放;其他通话不受影响 |
|
||||
| AI-14 | 鉴权错误、429、生成超时、ASR/TTS断流 | 分项错误与终态可靠留证;无静默Mock/模型切换、无自动重拨;仅按已冻结失败策略执行 |
|
||||
| AI-15 | ARI断连、执行器重启、活动状态未知 | 保留执行事实并进入对账;不生成新ID重拨、不释放未知活动占用;恢复后事实收敛 |
|
||||
| AI-16 | MQ重复、发布确认丢失、脚本重启 | 同一执行最多一次实际拨号;清单可恢复;结果按事件ID去重,不绕过原租户调度域 |
|
||||
| AI-17 | 文字/播放及MQ故障 | 最终稿可靠持久;生成/发送/播放证据不混淆;outbox恢复且不产生第二套业务回调 |
|
||||
| AI-18 | 录音正常上传、OSS超时/校验失败 | 接通后录制、挂断封口;真实字节独立校验后才返回OSS ID/ready;失败保留恢复证据 |
|
||||
| AI-19 | 未授权真实调用、Mock混入real、凭据缺失 | 启动/预检拒绝;无真实拨号/收费调用;报告逐组件模式准确 |
|
||||
| AI-20 | 一键执行完整流程 | 单命令完成配置、预检、MQ提交、收集与报告;无手工改PJSIP;退出码匹配结论;人工未确认不报全通过 |
|
||||
| AI-21 | 日志/报告安全与本次资源收尾 | 无密钥;公开证据脱敏;本次已结束通道/媒体/AI资源无泄漏;ECS/EIP不自动清理 |
|
||||
|
||||
### 7.1 延迟与质量门禁
|
||||
|
||||
- 供应商选定后、真实测试前登记:ASR准确性样本与判据、LLM首token、TTS首音频、端到端回复、打断停止、静音等待和资源清理的阈值及测量方法。
|
||||
- 阈值由实际供应商能力与测试预算确认;不得在看到结果后调宽阈值再宣称通过。未冻结只记录实测,相关性能用例标记 BLOCKED。
|
||||
- 单通话测试报告样本数与每次观测;少量样本不宣称生产p95/SLA。真实1000路、N+1、供应商额度与压测继续按最终计划单独验收。
|
||||
|
||||
## 8. 自动验证与交付门禁
|
||||
|
||||
- Python:`python3 -m unittest discover -s tests -v`;新增覆盖配置拒绝、流式解析、取消迟到包、幂等恢复与模式隔离的最小回归测试。
|
||||
- Go ASR有改动时:`cd services/asr-web && go test -race ./...`,并格式化和静态检查。
|
||||
- 音频前端或PCM有改动时:`node --test tests/test_pcm.cjs`;Asterisk Shell有改动时:`bash -n deploy/asterisk.sh`。
|
||||
- 修改后的OpenAPI/Schema解析与正反例校验、`git diff --check` 必须通过。
|
||||
- 集成证据必须使用隔离真实DB/RabbitMQ、真实Asterisk及协议级模拟对端;仅写ProtocolJournal日志不算协议集成通过。
|
||||
- 分别报告 Mock、真实ASR、真实LLM/TTS、真实SIP/媒体、MQ/OSS、人工听测结果。完整真实电话验收要求所有必需组件为real且相关用例通过;mixed通过只证明其列出的真实部分。
|
||||
- 交付:代码/配置示例/运行说明、逐例验收报告、脱敏证据索引、未完成项与阻塞项;文档中的预计脚本只有实现并验证后才能标记可用。
|
||||
|
||||
## 9. 外部待办与开工顺序
|
||||
|
||||
| 待提供/确认 | 影响 |
|
||||
| --- | --- |
|
||||
| Bailian 首家供应商的模型、流式/取消协议、配额与预算 | 文件链路已按当前协议验证;新增模型/能力仍需单独登记 |
|
||||
| Bailian TTS 自定义音色与音频格式 | CosyVoice 文件合成已验证;人工听测和电话侧格式仍待验收 |
|
||||
| 受控凭据、服务端允许的供应商地址与租户授权 | 文件探针已使用环境凭据;电话执行仍需 Cell/线路授权 |
|
||||
| 配置发布/同步来源、权限域与版本契约 | P1冻结后才能宣称OpenAPI对接完成 |
|
||||
| 真实MQ/数据库/OSS及测试SaaS inbox条件 | 缺失时只能做文件级 real 验证,不宣称完整电话交接通过 |
|
||||
| 已授权测试号码、时段、次数、预算及录音保留规则 | 阻塞真实电话执行;历史拨号授权不自动扩展为本专项三轮测试 |
|
||||
| 质量样本、延迟阈值与人工听测人员 | 阻塞对应质量/体验最终签字 |
|
||||
|
||||
执行顺序:先完成P1契约草案与隔离Mock正反例,供应商资料并行协调;P2先文本后音频;P3/P4再安排一次受控真实电话测试;P5提交逐例证据与未通过项。已接受的实施方向不重复等待确认;只对新增字段/协议、预算和真实执行授权进行必要冻结。
|
||||
@@ -17,7 +17,7 @@
|
||||
4. 录音先上传 OSS,校验成功并取得 OSS ID 后发布 `recording.ready`;MQ 不传录音二进制、Base64 或公开播放 URL。
|
||||
5. SaaS 负责客户/任务主数据、业务调度、业务重试决策、授权和长期存储;呼出应用只保存必要执行事实、控制屏障、幂等、资源租约和投递记录。
|
||||
6. 生产采用多机器、多 EIP 直连;每通电话固定 Cell/出口。SaaS 不逐呼改写共享 SIP 配置,也不能任意指定 SIP 地址、凭证或越权主叫。
|
||||
7. 目前只实现 ASR 验证基础,LLM/TTS 协议尚待新规范;本文对完整 AI 链路的描述是目标契约,不代表已启用或验收。
|
||||
7. 目前只实现 ASR 验证基础;agent-call 已补充隔离的不可变 AI 配置存储与内部配置读写契约(见`docs/contracts/ai-config.openapi.yaml`和`ai-config.schema.json`),但首家供应商的 LLM/TTS 请求、流式、取消和音频协议尚待新规范,完整 AI 链路不代表已启用或验收。
|
||||
8. **按 SaaS 提供的 `tenant_key` 绑定到独立 RabbitMQ 命令队列,由呼出应用调度器负责租户间公平调度**。不再采用所有租户共用一个执行 FIFO;同时限制租户发布/积压、预取窗口及跨 Cell 并发/CPS。`tenant_key` 是 SaaS 业务数据,命令、队列路由和后续 MQ 回调均原样使用;命名规则已统一,运行参数和限值仍待 G0 冻结。
|
||||
|
||||
## 2. 交付拆分与双方职责
|
||||
@@ -312,10 +312,19 @@ payload 必需:
|
||||
| `variables` | 经字段白名单、类型/长度校验的运行变量,不是任意代码或任意 URL |
|
||||
| `ring_timeout_ms`、`max_call_duration_ms` | 不超过服务端和供应商上限 |
|
||||
|
||||
agent_version_id 必须对应呼出侧可用且可信的快照;配置如何同步、LLM/TTS 参数和取消/音频契约留待新规范。缺失配置明确拒绝,不私自读取旧 voice_test LLM/TTS 或任意远程 URL。
|
||||
agent_version_id 必须对应呼出侧可用且可信的快照;当前配置发布/读取的内部契约见`docs/contracts/ai-config.openapi.yaml`,发布后不可覆盖,配置含 Prompt/模型/音色摘要及受控凭据引用。Bailian 文件探针使用 `agent_call/bailian.py` 的服务端环境适配,具体供应商参数、流式、取消/音频协议不进入 `call.execute`。缺失配置明确拒绝,不私自读取旧 voice_test LLM/TTS 或任意远程 URL。
|
||||
|
||||
线路策略由呼出侧解析为授权 `trunk_id + egress_pool_id + cell_id`,实际选择写入事件。呼叫固定 Cell/出口;允许的 FALLBACK 也必须兼容同一 Cell/出口及供应商白名单,无适合备用就失败,不迁移活动通话。当前只有一家线路,不虚构 backup。
|
||||
|
||||
### 6.1.1 AI 配置内部契约
|
||||
|
||||
AI 配置发布/读取不是 SaaS 拨号入口,也不是业务结果回调;仅供受控配置同步面使用。机器契约为 `docs/contracts/ai-config.openapi.yaml` 与 `docs/contracts/ai-config.schema.json`:
|
||||
|
||||
- `POST /internal/v1/ai/agent-versions`:要求独立 AI 配置认证域和 `ai.config.publish`,正文严格为 `agent_version_id + config`;首次发布返回 `201/published`,相同内容重放返回 `200/reused`。
|
||||
- `GET /internal/v1/ai/agent-versions/{agent_version_id}`:要求独立 AI 配置认证域和 `ai.config.read`,只返回当前租户的可信快照。
|
||||
- 版本按 `(tenant_id, agent_version_id)` 唯一且不可覆盖;内容摘要为 SHA-256。ID 不存在、跨租户、内容冲突、字段/Prompt/模型/音色不合法必须明确拒绝,不回退旧版本。
|
||||
- 配置快照只允许 provider/credential 引用和受控参数,不允许 API Key、密码、任意 URL、SIP 地址;`call.execute` 仍只通过 RabbitMQ,不能把配置正文塞进消息。
|
||||
|
||||
### 6.2 幂等与发起
|
||||
|
||||
1. 验证身份边界、Schema 和 tenant/task 关联;对已记录的 command_id/execution_id 先检查语义冲突并返回/关联原结果,不因重投时已过期或控制已变而改写原事实。新执行再检查有效期、控制版本和配置/线路授权;防重查重不绕过当前调用方的租户访问校验。
|
||||
@@ -644,7 +653,7 @@ HTTP 错误码与 MQ reason_code 共用词汇但不是一一映射;MQ 没有 H
|
||||
| 应用证据与清理(已确认方案) | 第 10.1 节:首期只自动确认 broker,SaaS 对账补传;保留/交接依据,若必须自动应用确认则另选 MQ receipt | 用户、双方、运维 |
|
||||
| 执行幂等 | 新增 execution_id,与 command_id 分离;业务重新外呼许可及去重保留 | 用户、SaaS |
|
||||
| 停止语义 | drain/hangup 显式选择;挂断权限与多 Cell 生效判据 | 用户、SaaS |
|
||||
| 线路/AI 配置 | route/caller/agent 引用及同步来源;LLM/TTS 新规范、失败兜底 | 用户、供应方 |
|
||||
| 线路/AI 配置 | route/caller/agent 引用及同步来源;Mock AI 配置发布契约已落地,真实 LLM/TTS 新规范、失败兜底待供应方 | 用户、供应方 |
|
||||
| Asterisk/SIP 管理后台与 SaaS 只读目录(Mock 已实现,生产待核验) | 不纳入普通业务七条路径;由独立管理后台统一写入并发布多机器 Trunk/codec_profile、主叫/被叫规则、出口池、并发/CPS,提供版本、CAS、审计、校验、发布、停用和回滚;SaaS 仅读取脱敏 Trunk 配置/能力/版本/健康状态;管理写接口、SaaS 只读接口和普通调度接口隔离认证凭据、issuer/audience/权限域,普通调度 Token 必须拒绝管理写接口 | 用户、SaaS、运维 |
|
||||
| MQ 环境 | 租户独立命令队列已确认;采用 `agent-call` 命名空间、direct 命令 exchange、`agent-call.tenant.{tenant_key}.call.execute` 路由及 `tenant_key` 原样透传;冻结精确绑定、生命周期、队列数上限、quorum/HA、ACL、重试/DLQ 与死信可靠性 | 运维、双方 |
|
||||
| 租户公平与背压 | 公平调度架构已确认;冻结轮转批量/周期、活跃队列发现、权重、prefetch、接收窗口、并发/CPS、发布速率/积压上限、拒绝发布策略、多实例协调及等待指标 | 用户、双方、运维 |
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: agent-call immutable AI configuration API
|
||||
version: 1.0.0
|
||||
description: >-
|
||||
Internal configuration publication/read surface. The call.execute business
|
||||
command remains RabbitMQ-only; secrets, URLs, and provider credentials are
|
||||
resolved by the execution environment and never enter MQ messages.
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
/internal/v1/ai/agent-versions:
|
||||
post:
|
||||
operationId: publishAgentVersion
|
||||
security:
|
||||
- aiConfigPublish: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentVersionPublishRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Identical immutable content already exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentVersionReceipt'
|
||||
'201':
|
||||
description: Immutable version published
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentVersionReceipt'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'409':
|
||||
description: Existing version has different content
|
||||
/internal/v1/ai/agent-versions/{agent_version_id}:
|
||||
get:
|
||||
operationId: getAgentVersion
|
||||
security:
|
||||
- aiConfigRead: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- name: agent_version_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
||||
responses:
|
||||
'200':
|
||||
description: Trusted immutable snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentVersion'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
description: Agent version not found
|
||||
components:
|
||||
parameters:
|
||||
TenantId:
|
||||
name: X-Tenant-Id
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string, minLength: 1}
|
||||
RequestId:
|
||||
name: X-Request-Id
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
securitySchemes:
|
||||
aiConfigPublish:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: >-
|
||||
Requires scope ai.config.publish and the AI-config issuer/audience.
|
||||
aiConfigRead:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: >-
|
||||
Requires scope ai.config.read and the AI-config issuer/audience.
|
||||
schemas:
|
||||
AgentVersionPublishRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [agent_version_id, config]
|
||||
properties:
|
||||
agent_version_id:
|
||||
type: string
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
||||
config:
|
||||
$ref: 'ai-config.schema.json'
|
||||
AgentVersionReceipt:
|
||||
type: object
|
||||
required: [tenant_id, agent_version_id, status, immutable, content_sha256]
|
||||
properties:
|
||||
tenant_id: {type: string}
|
||||
agent_version_id: {type: string}
|
||||
status: {enum: [published, reused]}
|
||||
immutable: {const: true}
|
||||
content_sha256: {type: string, pattern: '^[a-f0-9]{64}$'}
|
||||
AgentVersion:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AgentVersionReceipt'
|
||||
- type: object
|
||||
required: [config]
|
||||
properties:
|
||||
config:
|
||||
$ref: 'ai-config.schema.json'
|
||||
created_at: {type: string, format: date-time}
|
||||
published_at: {type: string, format: date-time}
|
||||
created_by: {type: string}
|
||||
Error:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error: {type: string}
|
||||
message: {type: string}
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Invalid configuration
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Error'}
|
||||
Unauthorized:
|
||||
description: Missing or invalid AI-config token
|
||||
Forbidden:
|
||||
description: Token lacks the AI-config permission
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://agent-call.local/contracts/ai-config.schema.json",
|
||||
"title": "Immutable AI agent version",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["agent_version_id", "immutable", "llm", "prompt", "tts", "asr", "conversation"],
|
||||
"properties": {
|
||||
"agent_version_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"},
|
||||
"immutable": {"const": true},
|
||||
"llm": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["provider_ref", "model"],
|
||||
"properties": {
|
||||
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"model": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"temperature": {"type": "number", "minimum": 0, "maximum": 2},
|
||||
"max_tokens": {"type": "integer", "minimum": 1},
|
||||
"timeout_ms": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["text", "allowed_variables"],
|
||||
"properties": {
|
||||
"text": {"type": "string", "minLength": 1, "maxLength": 32768},
|
||||
"allowed_variables": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"items": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"}
|
||||
},
|
||||
"max_bytes": {"type": "integer", "minimum": 1, "maximum": 32768}
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["provider_ref", "model", "voice", "format"],
|
||||
"properties": {
|
||||
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"model": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"voice": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"speed": {"type": "number", "minimum": 0.25, "maximum": 3},
|
||||
"timeout_ms": {"type": "integer", "minimum": 1},
|
||||
"format": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["encoding", "sample_rate_hz", "channels"],
|
||||
"properties": {
|
||||
"encoding": {"enum": ["pcm_s16le", "pcma"]},
|
||||
"sample_rate_hz": {"type": "integer", "minimum": 8000, "maximum": 48000},
|
||||
"channels": {"const": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"asr": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["provider_ref", "language", "input"],
|
||||
"properties": {
|
||||
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"model": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"language": {"type": "string", "minLength": 1, "maxLength": 32},
|
||||
"interim": {"type": "boolean"},
|
||||
"timeout_ms": {"type": "integer", "minimum": 1},
|
||||
"input": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["encoding", "sample_rate_hz", "channels", "sample_width_bytes"],
|
||||
"properties": {
|
||||
"encoding": {"const": "pcm_s16le"},
|
||||
"sample_rate_hz": {"type": "integer", "minimum": 8000, "maximum": 48000},
|
||||
"channels": {"const": 1},
|
||||
"sample_width_bytes": {"const": 2}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conversation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["opening", "allow_interrupt", "silence_timeout_ms", "max_duration_ms", "max_turns", "sentence_max_chars", "max_pending_audio_chunks"],
|
||||
"properties": {
|
||||
"opening": {"type": "string", "maxLength": 32768},
|
||||
"allow_interrupt": {"type": "boolean"},
|
||||
"silence_timeout_ms": {"type": "integer", "minimum": 1},
|
||||
"max_duration_ms": {"type": "integer", "minimum": 1, "maximum": 3600000},
|
||||
"max_turns": {"type": "integer", "minimum": 1, "maximum": 1000},
|
||||
"sentence_max_chars": {"type": "integer", "minimum": 1},
|
||||
"max_pending_audio_chunks": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"metadata": {"type": "object", "additionalProperties": true}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"agent_version_id": "agent_v1",
|
||||
"immutable": true,
|
||||
"llm": {
|
||||
"provider_ref": "mock",
|
||||
"model": "mock-chat-v1",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 256,
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"prompt": {
|
||||
"text": "You are a concise telephone assistant. Answer the caller's last statement.",
|
||||
"allowed_variables": [],
|
||||
"max_bytes": 32768
|
||||
},
|
||||
"tts": {
|
||||
"provider_ref": "mock",
|
||||
"model": "mock-tts-v1",
|
||||
"voice": "mock-neutral",
|
||||
"speed": 1.0,
|
||||
"format": {
|
||||
"encoding": "pcm_s16le",
|
||||
"sample_rate_hz": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"asr": {
|
||||
"provider_ref": "mock",
|
||||
"language": "zh-CN",
|
||||
"input": {
|
||||
"encoding": "pcm_s16le",
|
||||
"sample_rate_hz": 16000,
|
||||
"channels": 1,
|
||||
"sample_width_bytes": 2
|
||||
},
|
||||
"interim": true,
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"conversation": {
|
||||
"opening": "",
|
||||
"allow_interrupt": true,
|
||||
"silence_timeout_ms": 3000,
|
||||
"max_duration_ms": 120000,
|
||||
"max_turns": 20,
|
||||
"sentence_max_chars": 80,
|
||||
"max_pending_audio_chunks": 32
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"generated_at": "2026-09-14T14:24:17Z",
|
||||
"scope": "real-bailian-file-probes-plus-isolated-mock-plus-real-cell-attempts",
|
||||
"overall_status": "INCOMPLETE",
|
||||
"gate_status": "BLOCKED_BY_REAL_PHONE_MEDIA_MQ_OSS_AND_HUMAN_EVIDENCE",
|
||||
"component_modes": {
|
||||
"llm": "real-bailian",
|
||||
"tts": "real-bailian",
|
||||
"asr": "real-bailian",
|
||||
"sip_ari_rtp": "partial-real",
|
||||
"database": "isolated-sqlite",
|
||||
"rabbitmq": "temporary-test-broker",
|
||||
"oss": "not-run"
|
||||
},
|
||||
"real_provider_preflight": {
|
||||
"environment_configured": true,
|
||||
"text_code": 0,
|
||||
"audio_code": 0
|
||||
},
|
||||
"regression_tests": {
|
||||
"exit_code": 0,
|
||||
"test_count": 91
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"kind": "mock-text",
|
||||
"exit_code": 0,
|
||||
"status": "PASS",
|
||||
"mode": "mock",
|
||||
"requested_mode": "mock",
|
||||
"provider_modes": {
|
||||
"saas": "mock",
|
||||
"database": "sqlite",
|
||||
"rabbitmq": "memory",
|
||||
"sip": "mock",
|
||||
"asterisk": "mock",
|
||||
"asr": "mock",
|
||||
"llm": "mock",
|
||||
"tts": "mock",
|
||||
"oss": "mock",
|
||||
"cloud": "fake-cli"
|
||||
},
|
||||
"agent_version_id": "agent_test_v1",
|
||||
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
|
||||
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
|
||||
"llm_requested_model": "mock-chat-v1",
|
||||
"llm_returned_model": null,
|
||||
"tts_requested_model": "mock-tts-v1",
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": {
|
||||
"configured_model": "mock-tts-v1",
|
||||
"provider_echoed_model": false
|
||||
},
|
||||
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
|
||||
"asr_provider_ref": "mock",
|
||||
"asr_segment_count": 0,
|
||||
"llm_first_token_ms": 0.23247480392456055,
|
||||
"tts_first_audio_ms": 3.8798768520355225,
|
||||
"audio_sha256": "59a3ef24b931936fb9a65ba8d780a6b6bf9075357d2ab7094065018cfcdf217b",
|
||||
"audio_bytes": 24492,
|
||||
"call_id": null,
|
||||
"reason_code": null,
|
||||
"recording_bytes": null,
|
||||
"recording_valid": null
|
||||
},
|
||||
{
|
||||
"kind": "mock-audio",
|
||||
"exit_code": 0,
|
||||
"status": "PASS",
|
||||
"mode": "mock",
|
||||
"requested_mode": "mock",
|
||||
"provider_modes": {
|
||||
"saas": "mock",
|
||||
"database": "sqlite",
|
||||
"rabbitmq": "memory",
|
||||
"sip": "mock",
|
||||
"asterisk": "mock",
|
||||
"asr": "mock",
|
||||
"llm": "mock",
|
||||
"tts": "mock",
|
||||
"oss": "mock",
|
||||
"cloud": "fake-cli"
|
||||
},
|
||||
"agent_version_id": "agent_test_v1",
|
||||
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
|
||||
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
|
||||
"llm_requested_model": "mock-chat-v1",
|
||||
"llm_returned_model": null,
|
||||
"tts_requested_model": "mock-tts-v1",
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": {
|
||||
"configured_model": "mock-tts-v1",
|
||||
"provider_echoed_model": false
|
||||
},
|
||||
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
|
||||
"asr_provider_ref": "mock",
|
||||
"asr_segment_count": 2,
|
||||
"llm_first_token_ms": 0.2178652286529541,
|
||||
"tts_first_audio_ms": 7.668752193450928,
|
||||
"audio_sha256": "57481064a8382962faac5fd30c70c671e3d50e013e9e1807b5ef26140c5a7f2b",
|
||||
"audio_bytes": 43052,
|
||||
"call_id": null,
|
||||
"reason_code": null,
|
||||
"recording_bytes": null,
|
||||
"recording_valid": null
|
||||
},
|
||||
{
|
||||
"kind": "mock-call",
|
||||
"exit_code": 0,
|
||||
"status": "PASS",
|
||||
"mode": "mock",
|
||||
"requested_mode": "mock",
|
||||
"provider_modes": {
|
||||
"saas": "mock",
|
||||
"database": "sqlite",
|
||||
"rabbitmq": "memory",
|
||||
"sip": "mock",
|
||||
"asterisk": "mock",
|
||||
"asr": "mock",
|
||||
"llm": "mock",
|
||||
"tts": "mock",
|
||||
"oss": "mock",
|
||||
"cloud": "fake-cli"
|
||||
},
|
||||
"agent_version_id": "agent_test_v1",
|
||||
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
|
||||
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
|
||||
"llm_requested_model": "mock-chat-v1",
|
||||
"llm_returned_model": null,
|
||||
"tts_requested_model": "mock-tts-v1",
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": null,
|
||||
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
|
||||
"asr_provider_ref": "mock",
|
||||
"asr_segment_count": 0,
|
||||
"llm_first_token_ms": null,
|
||||
"tts_first_audio_ms": null,
|
||||
"audio_sha256": null,
|
||||
"audio_bytes": null,
|
||||
"call_id": "call_7d10d1d0174f43f09c05ce72bf7a8e84",
|
||||
"reason_code": "CALL_COMPLETED",
|
||||
"recording_bytes": null,
|
||||
"recording_valid": null
|
||||
},
|
||||
{
|
||||
"kind": "real-text",
|
||||
"exit_code": 0,
|
||||
"status": "PASS",
|
||||
"mode": "real",
|
||||
"requested_mode": "real",
|
||||
"provider_modes": {
|
||||
"llm": "real",
|
||||
"tts": "real",
|
||||
"asr": "real"
|
||||
},
|
||||
"agent_version_id": "agent_bailian_test_v1",
|
||||
"agent_config_sha256": "8e4473cec2700f68847499bf46b911bfb9d4c4f52557c474f86f2de074e752a2",
|
||||
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
|
||||
"llm_requested_model": "qwen-plus",
|
||||
"llm_returned_model": "qwen-plus",
|
||||
"tts_requested_model": "cosyvoice-v3.5-plus",
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": {
|
||||
"configured_model": "cosyvoice-v3.5-plus",
|
||||
"provider_echoed_model": false
|
||||
},
|
||||
"voice_sha256": "5549b1c86878276e83fe1c220c0c4e6c469dc6afbff5761af2e4e29940657f35",
|
||||
"asr_provider_ref": "bailian",
|
||||
"asr_segment_count": 0,
|
||||
"llm_first_token_ms": 200.48080801963806,
|
||||
"tts_first_audio_ms": 1790.2482328414917,
|
||||
"audio_sha256": "5ee03618a72a20a2d5f14d4fe27d8046f069a6785972c8b40eb28dcfafb68a5b",
|
||||
"audio_bytes": 49964,
|
||||
"call_id": null,
|
||||
"reason_code": null,
|
||||
"recording_bytes": null,
|
||||
"recording_valid": null
|
||||
},
|
||||
{
|
||||
"kind": "real-audio",
|
||||
"exit_code": 0,
|
||||
"status": "PASS",
|
||||
"mode": "real",
|
||||
"requested_mode": "real",
|
||||
"provider_modes": {
|
||||
"llm": "real",
|
||||
"tts": "real",
|
||||
"asr": "real"
|
||||
},
|
||||
"agent_version_id": "agent_bailian_test_v1",
|
||||
"agent_config_sha256": "8e4473cec2700f68847499bf46b911bfb9d4c4f52557c474f86f2de074e752a2",
|
||||
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
|
||||
"llm_requested_model": "qwen-plus",
|
||||
"llm_returned_model": "qwen-plus",
|
||||
"tts_requested_model": "cosyvoice-v3.5-plus",
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": {
|
||||
"configured_model": "cosyvoice-v3.5-plus",
|
||||
"provider_echoed_model": false
|
||||
},
|
||||
"voice_sha256": "5549b1c86878276e83fe1c220c0c4e6c469dc6afbff5761af2e4e29940657f35",
|
||||
"asr_provider_ref": "bailian",
|
||||
"asr_segment_count": 3,
|
||||
"llm_first_token_ms": 296.70279598236084,
|
||||
"tts_first_audio_ms": 1466.9799489974976,
|
||||
"audio_sha256": "f47811feff980e450f00aa65f82996a0f2de8f4eea7ebfc40df6ba0b51af5116",
|
||||
"audio_bytes": 46124,
|
||||
"call_id": null,
|
||||
"reason_code": null,
|
||||
"recording_bytes": null,
|
||||
"recording_valid": null
|
||||
},
|
||||
{
|
||||
"kind": "real-call-evidence",
|
||||
"exit_code": 1,
|
||||
"status": "INCOMPLETE",
|
||||
"mode": "real",
|
||||
"requested_mode": null,
|
||||
"provider_modes": null,
|
||||
"agent_version_id": null,
|
||||
"agent_config_sha256": null,
|
||||
"prompt_sha256": null,
|
||||
"llm_requested_model": null,
|
||||
"llm_returned_model": null,
|
||||
"tts_requested_model": null,
|
||||
"tts_returned_model": null,
|
||||
"tts_returned_model_verified": false,
|
||||
"tts_model_evidence": null,
|
||||
"voice_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"asr_provider_ref": null,
|
||||
"asr_segment_count": 0,
|
||||
"llm_first_token_ms": null,
|
||||
"tts_first_audio_ms": null,
|
||||
"audio_sha256": null,
|
||||
"audio_bytes": null,
|
||||
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
|
||||
"reason_code": "CALL_NOT_ANSWERED",
|
||||
"recording_bytes": 44,
|
||||
"recording_valid": false
|
||||
}
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "AI-01",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "tenant-scoped immutable snapshot was published and reused",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-02",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "missing, cross-tenant, and same-version conflict checks are covered",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-03",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "Prompt, model/voice, secret, and audio-format validation is covered",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-04",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "version digest is persisted with the execution snapshot",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-09",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "opening and Prompt constraints are configuration-driven in the runtime tests",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-10",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "PCM16/PCMA conversion, frame alignment, and bounded queues passed",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-11",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "cancellation and late-chunk discard passed with isolated providers",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-12",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "silence/no-answer lifecycle remains bounded in the mock executor",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-13",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "hangup/timeout cleanup remains isolated in the mock executor",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-14",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "provider errors and real-mode fail-closed behavior are covered",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-15",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "ARI reconciliation regression remains green for the existing mock path",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-16",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "duplicate publication/execution recovery remains green",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-17",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "text, playback, and MQ result facts remain separate",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-18",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "recording checksum/upload recovery regression remains green",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-19",
|
||||
"mode": "real-cell-code",
|
||||
"status": "PASS",
|
||||
"actual": "real Cell ARI/RTP worker, queue routing, durable claim ledger, and no-redial tests are covered",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-21",
|
||||
"mode": "isolated-mock",
|
||||
"status": "PASS",
|
||||
"actual": "reports use hashes and do not include provider credentials or URLs",
|
||||
"evidence": [
|
||||
"tests/test_ai_runtime.py",
|
||||
"tests/test_agent_call.py",
|
||||
"tests/test_real_cell.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-05",
|
||||
"mode": "real-bailian",
|
||||
"status": "PASS",
|
||||
"actual": "real text stream passed",
|
||||
"evidence": [
|
||||
"docs/evidence/llm-voice-acceptance-20260914.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-06",
|
||||
"mode": "real-bailian",
|
||||
"status": "BLOCKED",
|
||||
"actual": "real TTS; machine audio passed; human listening is not registered passed",
|
||||
"evidence": [
|
||||
"docs/evidence/llm-voice-acceptance-20260914.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-07",
|
||||
"mode": "real-bailian",
|
||||
"status": "BLOCKED",
|
||||
"actual": "real audio chain; machine audio passed; human listening is not registered passed",
|
||||
"evidence": [
|
||||
"docs/evidence/llm-voice-acceptance-20260914.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-08",
|
||||
"mode": "real-phone-evidence",
|
||||
"status": "FAIL",
|
||||
"actual": "real three-round phone evidence is incomplete: SIP may connect, but bidirectional RTP, three AI rounds, recording handoff, and human listening are required",
|
||||
"evidence": [
|
||||
"docs/evidence/real-cell-call-20260914.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AI-20",
|
||||
"mode": "mixed",
|
||||
"status": "BLOCKED",
|
||||
"actual": "one-command mock flow passed; full real call flow remains incomplete until real MQ/OSS and phone evidence pass",
|
||||
"evidence": [
|
||||
"scripts/test_ai_call.py",
|
||||
"docs/evidence/llm-voice-acceptance-20260914.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blockers": [
|
||||
"The single authorized real call reached SIP 200 OK, but usable bidirectional RTP, three AI rounds, and a valid recording were not proven.",
|
||||
"The call path used a temporary RabbitMQ test broker; production RabbitMQ/SaaS result and OSS recording handoff remain unverified.",
|
||||
"Human listening and provider-specific latency/quality thresholds are not registered; machine-generated WAV evidence cannot replace listening."
|
||||
],
|
||||
"evidence_policy": "docs/evidence contains hashes/status only; raw protocol/audio evidence stays in ignored local paths",
|
||||
"test_commands": [
|
||||
"python3 -m unittest discover -s tests -v",
|
||||
"python3 scripts/test_ai_call.py text --profile configs/ai-test.bailian.example.yaml --mode real --input '请只回答收到。'",
|
||||
"python3 scripts/test_ai_call.py audio --profile configs/ai-test.bailian.example.yaml --mode real --input <real-or-authorized-wav>",
|
||||
"python3 scripts/run_real_cell_call.py --profile configs/ai-test.bailian.example.yaml --allow-real-call --callee 15003164745"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# LLM 与音色可配置电话对话专项验收
|
||||
|
||||
- 日期:2026-09-14
|
||||
- 范围:Bailian 真实文件链路 + 隔离 Mock + 三次真实 Cell 电话尝试
|
||||
- 总体:`INCOMPLETE`
|
||||
- 门禁:`BLOCKED_BY_REAL_PHONE_MEDIA_MQ_OSS_AND_HUMAN_EVIDENCE`
|
||||
- 机器证据:`docs/evidence/llm-voice-acceptance-20260914.json`
|
||||
|
||||
## 已通过
|
||||
|
||||
- 91 项 Python 回归测试通过;Ruff、编译、Schema/OpenAPI 解析与 `git diff --check` 通过。
|
||||
- 租户范围不可变 `agent_version_id`、SHA-256 快照、配置权限隔离和 MQ `call.execute` 约束通过。
|
||||
- Bailian 真实文本探针:LLM 请求 `qwen-plus`,服务端返回模型 `qwen-plus`;首 token 约 335 ms,TTS 首音频约 1661 ms。
|
||||
- Bailian 真实音频探针:CosyVoice `cosyvoice-v3.5-plus` 输出 mono PCM16/24 kHz;真实 ASR 返回最终稿,随后真实 LLM/TTS 完成;音频可解码并留存 SHA-256。
|
||||
- CosyVoice 自定义音色使用 `/api-ws/v1/inference` 流式协议;Qwen realtime 分支会使用 `/api-ws/v1/realtime`,不再误用 ASR endpoint。
|
||||
- Bailian 适配器保持环境注入凭据;MQ、配置快照和公开证据不写入 API Key、供应商 URL 或凭据。
|
||||
- 三次分别授权的 real Cell 呼叫均未完成 AI 电话门禁:第一次到达 SIP `200 OK` 但无可用 RTP,后两次为 SIP `480`;没有自动重拨,详见 `docs/evidence/real-cell-call-20260914.json`。
|
||||
|
||||
## 阻塞项
|
||||
|
||||
- P3 已接入 Cell ARI/externalMedia/RTP/录音执行器;三次实际尝试均未形成可验收的双向 RTP/三轮上下文,P4 因此未完成。
|
||||
- 本次仅使用临时 RabbitMQ 测试 broker 验证 `call.execute` 队列路径;生产 RabbitMQ/SaaS inbox、OSS 录音交接和完整三轮电话仍未验收。
|
||||
- TTS 服务端未返回可核验模型字段;报告同时保留 `tts_model_evidence.configured_model` 与 `provider_echoed_model=false`,并按契约标记 `provider_returned_model_verified=false`。人工音色听测尚未登记,因此 AI-06/AI-07 不能报完整 PASS。
|
||||
- 供应商延迟/质量阈值和人工听测结论未冻结,机器 WAV 不替代手机端听感验收。
|
||||
|
||||
## 证据与重跑
|
||||
|
||||
- 原始 WAV 和命令输出保存在被忽略目录 `.local/agent-call-ai/`,公开 JSON 只保留哈希、字节数、模式和时延。
|
||||
- 运行:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
python3 scripts/accept_ai_call.py
|
||||
```
|
||||
|
||||
验收脚本因真实电话媒体/三轮与人工证据未完成按设计返回非零;已执行但失败的真实电话返回 `1`,尚未安排真实电话才返回 `2`。这不是完整真实电话通过声明。
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"status": "FAIL",
|
||||
"mode": "real",
|
||||
"authorization": {
|
||||
"scope": "one final controlled real outbound call",
|
||||
"callee": "15003164745",
|
||||
"max_rounds": 3,
|
||||
"automatic_retry": false
|
||||
},
|
||||
"execution": {
|
||||
"execution_id": "exec_cell_1789395606006147221_10",
|
||||
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
|
||||
"event_id": "exec_cell_1789395606006147221_10:call.finished",
|
||||
"tenant_id": "tenant-demo",
|
||||
"tenant_key": "tenant-demo-key",
|
||||
"route": "agent-call.tenant.tenant-demo-key.call.execute",
|
||||
"connected": false,
|
||||
"duration_ms": 206.89915500406642,
|
||||
"reason_code": "CALL_NOT_ANSWERED",
|
||||
"turns": 0,
|
||||
"media_packets_in": 0,
|
||||
"media_packets_out": 0
|
||||
},
|
||||
"sip": {
|
||||
"provider": "61.132.228.221:5060",
|
||||
"target": "708915003164745",
|
||||
"codec": "PCMA/8000",
|
||||
"responses": ["100 trying", "480 Temporarily Unavailable"],
|
||||
"reason": "Q.850 cause=16 NORMAL_CLEARING",
|
||||
"provider_error_code": "14002_201",
|
||||
"external_media": "UnicastRTP channel created and joined the bridge",
|
||||
"rtp_and_recording": "no answered media session"
|
||||
},
|
||||
"rabbitmq": {
|
||||
"transport": "temporary local RabbitMQ test broker",
|
||||
"command_path_verified": true,
|
||||
"production_saas_handoff": false
|
||||
},
|
||||
"acceptance": {
|
||||
"real_sip_connected": false,
|
||||
"bidirectional_rtp_verified": false,
|
||||
"real_asr_llm_tts_turns": false,
|
||||
"three_round_conversation": false,
|
||||
"human_listening": false,
|
||||
"oss_recording_handoff": false
|
||||
},
|
||||
"note": "This is the final separately authorized attempt. No automatic retry was issued after the provider returned 480. Credentials, provider API keys, and ARI passwords are omitted."
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"status": "FAIL",
|
||||
"mode": "real",
|
||||
"authorization": {
|
||||
"scope": "one additional controlled real outbound call",
|
||||
"callee": "15003164745",
|
||||
"max_rounds": 3,
|
||||
"automatic_retry": false
|
||||
},
|
||||
"execution": {
|
||||
"execution_id": "exec_cell_1789393256422470110_10",
|
||||
"call_id": "call_41907a57960042098af9daf53bec5ed7",
|
||||
"event_id": "exec_cell_1789393256422470110_10:call.finished",
|
||||
"tenant_id": "tenant-demo",
|
||||
"tenant_key": "tenant-demo-key",
|
||||
"route": "agent-call.tenant.tenant-demo-key.call.execute",
|
||||
"connected": false,
|
||||
"duration_ms": 217.7564550001989,
|
||||
"reason_code": "CALL_NOT_ANSWERED",
|
||||
"turns": 0,
|
||||
"media_packets_in": 0,
|
||||
"media_packets_out": 0
|
||||
},
|
||||
"sip": {
|
||||
"provider": "61.132.228.221:5060",
|
||||
"target": "708915003164745",
|
||||
"codec": "PCMA/8000",
|
||||
"responses": ["100 trying", "480 Temporarily Unavailable"],
|
||||
"reason": "Q.850 cause=16 NORMAL_CLEARING",
|
||||
"external_media": "UnicastRTP channel created and joined the bridge",
|
||||
"rtp_and_recording": "no answered media session"
|
||||
},
|
||||
"rabbitmq": {
|
||||
"transport": "temporary local RabbitMQ test broker",
|
||||
"command_path_verified": true,
|
||||
"production_saas_handoff": false
|
||||
},
|
||||
"acceptance": {
|
||||
"real_sip_connected": false,
|
||||
"bidirectional_rtp_verified": false,
|
||||
"real_asr_llm_tts_turns": false,
|
||||
"three_round_conversation": false,
|
||||
"human_listening": false,
|
||||
"oss_recording_handoff": false
|
||||
},
|
||||
"note": "This is the second separately authorized attempt. No automatic retry was issued after the provider returned 480. Credentials, provider API keys, and ARI passwords are omitted."
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"status": "INCOMPLETE",
|
||||
"mode": "real",
|
||||
"gate": "BLOCKED_BY_REAL_MEDIA_AND_THREE_ROUND_EVIDENCE",
|
||||
"authorization": {
|
||||
"scope": "two separately authorized controlled real outbound calls",
|
||||
"callee": "15003164745",
|
||||
"max_rounds": 3,
|
||||
"attempts": 3,
|
||||
"automatic_retry": false
|
||||
},
|
||||
"execution": {
|
||||
"execution_id": "exec_cell_1789383485969851269_10",
|
||||
"call_id": "call_ec889b4209024cab9b17285ee2de9e72",
|
||||
"event_id": "exec_cell_1789383485969851269_10:call.finished",
|
||||
"tenant_id": "tenant-demo",
|
||||
"tenant_key": "tenant-demo-key",
|
||||
"route": "agent-call.tenant.tenant-demo-key.call.execute",
|
||||
"connected": true,
|
||||
"duration_ms": 35901.207285001874,
|
||||
"reason_code": "RTP_RECEIVE_FAILED",
|
||||
"turns": 0,
|
||||
"media_packets_in": 0,
|
||||
"media_packets_out": 0
|
||||
},
|
||||
"latest_execution": {
|
||||
"execution_id": "exec_cell_1789395606006147221_10",
|
||||
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
|
||||
"event_id": "exec_cell_1789395606006147221_10:call.finished",
|
||||
"connected": false,
|
||||
"duration_ms": 206.89915500406642,
|
||||
"reason_code": "CALL_NOT_ANSWERED",
|
||||
"turns": 0,
|
||||
"media_packets_in": 0,
|
||||
"media_packets_out": 0
|
||||
},
|
||||
"final_execution": {
|
||||
"execution_id": "exec_cell_1789395606006147221_10",
|
||||
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
|
||||
"event_id": "exec_cell_1789395606006147221_10:call.finished",
|
||||
"connected": false,
|
||||
"duration_ms": 206.89915500406642,
|
||||
"reason_code": "CALL_NOT_ANSWERED",
|
||||
"turns": 0,
|
||||
"media_packets_in": 0,
|
||||
"media_packets_out": 0
|
||||
},
|
||||
"attempt_evidence": [
|
||||
"docs/evidence/real-cell-call-20260914.json",
|
||||
"docs/evidence/real-cell-call-20260914-second.json",
|
||||
"docs/evidence/real-cell-call-20260914-final.json"
|
||||
],
|
||||
"sip": {
|
||||
"provider": "61.132.228.221:5060",
|
||||
"target": "708915003164745",
|
||||
"codec": "PCMA/8000",
|
||||
"responses": ["100 trying", "183 Session Progress", "200 OK"],
|
||||
"hangup_reason": "Q.850 cause=16 NORMAL_CLEARING",
|
||||
"rtp_and_recording": "no usable audio"
|
||||
},
|
||||
"recording": {
|
||||
"bytes": 44,
|
||||
"sha256": "4f8734c5e13ac599e168cf247a51c1dd0758537ce00bf16d7fed1a3d14d07041",
|
||||
"valid_audio": false
|
||||
},
|
||||
"rabbitmq": {
|
||||
"transport": "temporary local RabbitMQ test broker",
|
||||
"command_path_verified": true,
|
||||
"production_saas_handoff": false
|
||||
},
|
||||
"acceptance": {
|
||||
"real_sip_connected": true,
|
||||
"bidirectional_rtp_verified": false,
|
||||
"real_asr_llm_tts_turns": false,
|
||||
"three_round_conversation": false,
|
||||
"human_listening": false,
|
||||
"oss_recording_handoff": false
|
||||
},
|
||||
"note": "This aggregate records three separately authorized attempts. The first reached SIP 200 OK but had no usable media; the second and final attempts returned 480 before answer. No automatic redial was issued. Credentials, provider API keys, and ARI passwords are intentionally omitted."
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Real Cell AI call probe — 2026-09-14
|
||||
|
||||
**结论:INCOMPLETE(三次授权探针均未完成三轮 AI 对话)**
|
||||
|
||||
- 呼叫:`15003164745`;业务被叫原值保留,线路规则构造为 `708915003164745`
|
||||
- 第一次:SIP `200 OK`,但无可用 RTP,0 轮,44-byte 空录音;证据见本文件同名 JSON 的首个 execution。
|
||||
- 第二次:SIP `100 trying` → `480 Temporarily Unavailable`,`Q.850 cause=16 NORMAL_CLEARING`,未进入接通媒体;独立证据见 `docs/evidence/real-cell-call-20260914-second.json`。
|
||||
- 第三次(最终授权):SIP `100 trying` → `480 Temporarily Unavailable`,供应商错误码 `14002_201`,未进入接通媒体;独立证据见 `docs/evidence/real-cell-call-20260914-final.json`。
|
||||
- 路径:RabbitMQ `call.execute` → 租户命令队列 → Cell ARI/externalMedia → Asterisk/SIP
|
||||
- 首次 SIP:`100 trying` → `183 Session Progress` → `200 OK`;PCMA/8000;最终 `Q.850 cause=16`
|
||||
- ARI:真实 Cell bridge 和 `UnicastRTP` externalMedia 已创建并加入
|
||||
- 媒体:未验证到可用双向 RTP,Cell 收发包计数为 0;执行器返回 `RTP_RECEIVE_FAILED`
|
||||
- AI:0 个 ASR→LLM→TTS 电话轮次;未完成三轮;无人听测签字
|
||||
- 录音:仅 WAV 头(44 bytes),不可作为有效录音;SHA-256 为 `4f8734c5e13ac599e168cf247a51c1dd0758537ce00bf16d7fed1a3d14d07041`
|
||||
- MQ:本次使用临时本机 RabbitMQ 验证命令路径,不是生产 SaaS broker;未完成 OSS 交接
|
||||
|
||||
本文件记录三次分别授权的尝试;均未自动重拨。凭据、API Key、ARI 密码和任意敏感 URL 均未写入证据。机器明细见同名 JSON。
|
||||
@@ -10,6 +10,7 @@
|
||||
- [主契约](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)是标识、HTTP 七条路径、MQ 拓扑/字段/状态的唯一维护来源。本文件不复制接口表;先生成 OpenAPI/JSON Schema,再编写 Mock 和正式实现。
|
||||
- [原一期计划](一期呼出应用开发计划_v1.0.md)保留 WBS/AT 编号和历史估算;[交付设计](交付文档/一期中间调度件与Asterisk_v1.0/01_中间调度件与MQ回传设计.md)、[部署细则](交付文档/一期中间调度件与Asterisk_v1.0/02_Asterisk部署与SIP对接步骤.md)、[专项用例](交付文档/一期中间调度件与Asterisk_v1.0/03_联调步骤与验收标准.md)保留实现与场景细节,冲突时以本文件及主契约为准。
|
||||
- 当前已有 ASR Web、ASR 协议适配、Asterisk 配置生成、阿里云只读计划/受控创建工具,以及执行服务、持久状态、MQ/HTTP 和 Mock 测试产物;参见[现有运行说明](部署接入_运行说明.md)。2026-09-13静态审查发现隔离、控制、恢复、媒体与验收缺口,完整调度/AI通话/MQ与资产交接仍须整改复验,不能把已有产物等同于完整门禁通过。整改执行见[功能审查整改计划与验收标准](功能审查整改计划与验收标准_v1.0.md)。
|
||||
- 2026-09-14 用户已确认支持指定 LLM 模型、Prompt 和 TTS 音色的完整电话对话及一键测试;开发工作包、配置契约待冻结项、脚本边界与逐项功能验收见 [LLM与音色可配置电话对话专项计划](LLM与音色可配置电话对话_开发与验收计划_v1.0.md)。该文档不代表真实 AI 已启用,不替代主契约或生产容量验收。
|
||||
- 本次只交付文档,不新增运行服务、不拨号、不创建云资源、不提交 Git。旧 Word/Excel/ZIP 不覆盖重生成。
|
||||
|
||||
## 2. 已接受的最终方案
|
||||
|
||||
+33
-7
@@ -14,7 +14,7 @@
|
||||
| 阿里云主机准备 | 已实现CLI驱动的只读计划、受控创建竞价实例、复用主机和绑定既有EIP;默认不修改云资源 |
|
||||
| 云端实际操作 | 已完成V13一次性专用竞价Cell创建、绑定固定EIP、SCALE-MOCK及回收;当前保留一台SpotAsPriceGo真实SIP测试环境 `i-2ze8fa5vhyl4cc8oeol9` |
|
||||
| Asterisk真实接入 | 已完成当前环境R1底座与两号码SIP探针;初始ulaw收到488/Q.850 cause=88,按供应商反馈切换PCMA后两次均收到480/Q.850 cause=16,仍未接通 |
|
||||
| LLM/TTS | 未实现、未启用,等待用户新的供应商/协议/参数规范 |
|
||||
| LLM/TTS | 已实现 Bailian 真实流式 LLM/CosyVoice 适配、不可变 AI 版本、Prompt/模型/音色校验、取消与报告;电话媒体仍未接入 |
|
||||
| agent-call Mock 调度、MQ/OSS闭环 | 已实现:租户独立路由、DB状态、CAS控制、outbox/inbox、Mock SIP/ARI/RTP/AI和OSS校验;未替代真实Asterisk/供应商验收 |
|
||||
| ARI业务调度、自动FALLBACK、真实MQ/OSS回传 | 未完成;现有主备trunk配置不能当成自动切换代码 |
|
||||
|
||||
@@ -38,6 +38,12 @@ services/asr-web/ ASR-only Go服务及Web页面
|
||||
deploy/asterisk.example.json SIP接入参数占位
|
||||
deploy/asterisk.sh 默认检查;显式up才启动
|
||||
tests/ 离线部署逻辑及PCM测试
|
||||
scripts/test_ai_call.py text/audio/call 分层 AI 测试入口
|
||||
scripts/accept_ai_call.py AI 专项 Mock/真实文件验收与脱敏报告生成
|
||||
agent_call/bailian.py Bailian ASR/LLM/CosyVoice 环境适配器
|
||||
configs/ai-test.example.yaml 无密钥的 JSON-compatible YAML 示例
|
||||
configs/ai-test.bailian.example.yaml 真实 Bailian 文件探针示例(凭据只取环境)
|
||||
prompts/test-call.txt UTF-8 Prompt 示例
|
||||
docs/ 需求、计划和运行文档
|
||||
.local/ 本机临时状态/验证产物,不提交
|
||||
```
|
||||
@@ -183,7 +189,7 @@ ssh -N -L 18088:127.0.0.1:18088 "${DEPLOY_SSH_USER}@123.56.71.98"
|
||||
|
||||
页面流程:填写服务令牌→读取模型→选择有凭据的模型→开始识别→查看中间/最终文字→结束或取消。音频为16k、单声道、PCM16LE、每包100ms;会话最长5分钟、同时最多4个连接。背压过大停止采集,不能无限缓存音频。
|
||||
|
||||
LLM/TTS固定显示“未启用/等待新规范”。用户未提供新规范前,不调用旧仓库实现,不提供隐式供应商回退。
|
||||
ASR Web 页面不承载 LLM/TTS;`agent-call` CLI 的 Bailian 适配只从服务端 `BAILIAN_*` 环境读取凭据,不调用旧仓库的 LLM/TTS,也不提供隐式供应商回退。
|
||||
|
||||
## 5. Asterisk配置与启动
|
||||
|
||||
@@ -249,7 +255,8 @@ bash -n deploy/asterisk.sh
|
||||
- 本地容器:ASR镜像构建、非root/read-only运行、回环HTTP与healthcheck;测试容器已清理。
|
||||
- 浏览器:无效令牌拒绝、正常令牌读取未配置模型、模型禁用、LLM/TTS未启用。没有采集真实麦克风,也没有发送真实供应商请求。
|
||||
- 已执行:阿里云只读盘点、一次V13专用竞价Cell创建/绑定、Mock规模运行、一次R1真实SIP/Asterisk探针、测试实例停止/删除和固定EIP保留验证。
|
||||
- 未执行或未通过:真实外呼接通/RTP、真实ASR、LLM/TTS、MQ/OSS、完整外呼以及生产容量/N+1验收;R1证据见`docs/evidence/asterisk-real-2026-09-12.md`。
|
||||
- 未执行或未通过:真实外呼接通/RTP、电话侧真实ASR→LLM→TTS、MQ/OSS、完整外呼以及生产容量/N+1验收;R1证据见`docs/evidence/asterisk-real-2026-09-12.md`。
|
||||
- AI 专项:隔离 Mock 与 Bailian 真实文本/音频文件链路均已执行;真实 ASR、LLM、CosyVoice、音频格式和时延摘要见`docs/evidence/llm-voice-acceptance-20260914.json`,原始文件留在`.local/agent-call-ai/`。
|
||||
|
||||
本地模拟通过不等于生产可用。只有填齐资源和契约、执行既有验收文档相应场景并留存证据后,才可把阶段状态更新为真实环境验收通过。
|
||||
|
||||
@@ -275,22 +282,41 @@ curl --fail http://127.0.0.1:18080/healthz/ready
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
python3 -m scripts.acceptance
|
||||
python3 scripts/accept_ai_call.py
|
||||
python3 scripts/test_ai_call.py text --input '隔离文本验收'
|
||||
python3 deploy/check_mock_config.py
|
||||
python3 deploy/migrate.py data/agent-call.sqlite3
|
||||
```
|
||||
|
||||
`agent_call/core.py`使用真实SQLite表保存任务、执行、通话、尝试、事件、outbox/inbox、上传和重放状态;`deploy/backup_sqlite.py backup|restore`提供离线备份恢复。`ProtocolJournal`只记录合成的SIP/ARI/RTP/AI协议事实,不把它们伪装成真实供应商响应。
|
||||
`agent_call/core.py`使用真实SQLite表保存任务、执行、通话、尝试、事件、outbox/inbox、上传和重放状态;`agent_versions`保存租户范围内的不可变 AI 配置快照及摘要;`deploy/backup_sqlite.py backup|restore`提供离线备份恢复。`ProtocolJournal`只记录合成的SIP/ARI/RTP/AI协议事实,不把它们伪装成真实供应商响应。
|
||||
|
||||
`python3 scripts/test_ai_call.py` 的 `text` 与 `audio` 不拨号;`call` 必须显式传 `--allow-real-call`,Mock profile 仍只发 MQ 到隔离内存 Broker,不会拨打电话。Bailian real `text`/`audio` 只调用真实 AI,不拨号;报告只写配置/Prompt/音频摘要,不写密钥或任意供应商 URL。AI 配置 HTTP 读写使用独立 `ai.config.read` / `ai.config.publish` scope,并要求 `AI_CONFIG_ISSUER`/`AI_CONFIG_AUDIENCE` 与 `auth_domain=ai-config`;普通调度 Token 被拒绝。业务 `call.execute` 仍只走 RabbitMQ。
|
||||
|
||||
### 7.3 当前门禁
|
||||
|
||||
本地V01-V12及阿里云专用主机上的V13 Mock探针已通过;最新完整验收结果见`docs/evidence/acceptance-mock-2026-09-13.json`,脚本仍因V13缺持续媒体独立证据和V14需要外部真实证据而按门禁返回非零。V13使用100租户/1200模拟接通、禁用1个Cell;这是Mock协议/持久状态负载证据,不是RTP或生产容量证据。R1历史探针见`docs/evidence/asterisk-real-2026-09-12.md`;ulaw初始探针见`docs/evidence/asterisk-persistent-2026-09-14.md`,PCMA复测见`docs/evidence/asterisk-persistent-pcma-2026-09-14.md`:ECS `i-2ze8fa5vhyl4cc8oeol9`、Asterisk容器和固定EIP `123.56.71.98` 已核验,PCMA实际SDP为 `PCMA/8000`,两个目标均由原先的 `488/Q.850 cause=88` 变为 `480/Q.850 cause=16`,仍未接通、无RTP/录音证据。该 ECS 按用户要求继续保留并运行,固定 EIP 为 `InUse`。真实接通/RTP、ASR/LLM/TTS/OSS、生产1000路/N+1尚未完成。正式模式拒绝当前Mock profile,不能把测试号码或Mock成功自动升级为真实外呼授权。
|
||||
本地V01-V12及阿里云专用主机上的V13 Mock探针已通过;最新完整验收结果见`docs/evidence/acceptance-mock-2026-09-13.json`,脚本仍因V13缺持续媒体独立证据和V14需要外部真实证据而按门禁返回非零。V13使用100租户/1200模拟接通、禁用1个Cell;这是Mock协议/持久状态负载证据,不是RTP或生产容量证据。R1历史探针见`docs/evidence/asterisk-real-2026-09-12.md`;ulaw初始探针见`docs/evidence/asterisk-persistent-2026-09-14.md`,PCMA复测见`docs/evidence/asterisk-persistent-pcma-2026-09-14.md`:ECS `i-2ze8fa5vhyl4cc8oeol9`、Asterisk容器和固定EIP `123.56.71.98` 已核验,PCMA实际SDP为 `PCMA/8000`,两个目标均由原先的 `488/Q.850 cause=88` 变为 `480/Q.850 cause=16`,仍未接通、无RTP/录音证据。该 ECS 按用户要求继续保留并运行,固定 EIP 为 `InUse`。Bailian ASR/LLM/CosyVoice 文件链路已完成真实探针;真实电话接通/RTP、MQ/OSS、生产1000路/N+1尚未完成。正式模式仍拒绝当前Mock调度profile,不能把测试号码或Mock成功自动升级为真实外呼授权。
|
||||
|
||||
### 7.4 LLM/音色专项
|
||||
|
||||
```bash
|
||||
python3 scripts/test_ai_call.py text --model mock-chat-v1 \
|
||||
--prompt-file prompts/test-call.txt --input '隔离文本验收'
|
||||
python3 scripts/test_ai_call.py text --profile configs/ai-test.bailian.example.yaml \
|
||||
--mode real --input '请只回答收到。' --output-audio .local/agent-call-ai/bailian.wav
|
||||
python3 scripts/test_ai_call.py audio --profile configs/ai-test.bailian.example.yaml \
|
||||
--mode real --input .local/agent-call-ai/bailian.wav
|
||||
python3 scripts/run_real_cell_call.py --profile configs/ai-test.bailian.example.yaml \
|
||||
--callee 15003164745 --allow-real-call
|
||||
```
|
||||
|
||||
`text`/`audio` 不拨号;Bailian profile 的 real `text`/`audio` 会产生真实供应商调用;真实电话必须使用单独的 `run_real_cell_call.py`,显式经 RabbitMQ `call.execute`、Cell/ARI/RTP 门禁和号码 allow-list。`accept_ai_call.py` 不自动拨号。当前专项报告为 `INCOMPLETE`:单次授权电话 SIP 已接通但 RTP/三轮/有效录音未通过,AI-06/AI-07 仍待人工听测,MQ/OSS 生产交接未验收,详见上述 JSON。
|
||||
|
||||
## 8. 下一步需要用户提供/确认
|
||||
|
||||
1. 供应商需继续确认`7089`前缀/目标号码路由、From域(当前实际出现私网域)及 `480/Q.850 cause=16` 的具体含义;PCMA 已在 SDP 中实际生效,但两个号码仍未接通。
|
||||
2. 供应商调整并确认线路规则后,再用当前保留环境复测;未获清理指令前不停止/删除实例、不创建第二台,不自动重试、不改绑或释放固定EIP。
|
||||
3. ASR测试凭据、批准的模型/资源ID;Web HTTPS域名/证书或SSH访问方案。
|
||||
4. 用户制定的MQ/OSS ID接口规范,以及新的LLM/TTS协议、参数与取消/打断规则。
|
||||
5. 真实接通后仍需分别完成RTP、录音、MQ/OSS、ASR/LLM/TTS和生产1000路/N+1验收。
|
||||
4. 真实 RabbitMQ/OSS inbox、上传ID和录音保留规则,以及 Cell/ARI/RTP 执行授权。
|
||||
5. 真实接通后仍需分别完成RTP、录音、MQ/OSS、三轮电话人工听测和生产1000路/N+1验收。
|
||||
|
||||
PCMA 复测已完成;在供应商进一步确认 `480/cause=16` 对应的路由/号码可达性前,不自动再次拨打真实号码,也不声称完成整个平台。当前竞价测试实例按用户要求保留,后续清理需单独明确授权。
|
||||
|
||||
Reference in New Issue
Block a user