Update project and configurations
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -39,3 +39,14 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# models
|
||||
*.pt
|
||||
*.pth
|
||||
*.safetensors
|
||||
*.onnx
|
||||
*.bin
|
||||
*.h5
|
||||
*.ckpt
|
||||
*.tflite
|
||||
*.pb
|
||||
|
||||
55
ai_assistant_features.md
Normal file
55
ai_assistant_features.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# AI 智能助理系统功能点与架构梳理
|
||||
|
||||
## 一、 核心功能点清单
|
||||
|
||||
### 1. 权限驱动的 AI 技能系统 (AI Skills & Query)
|
||||
* **联系人与群组权限管控**:支持配置 AI 对特定微信联系人和微信群组的读取和回复权限,确保隐私安全。
|
||||
* **历史聊天检索 Skill**:AI 具备调用本地工具的能力,可按需查询和检索授权范围内的历史聊天内容,作为精准回复和数据分析的上下文。
|
||||
|
||||
### 2. 增强型三方协同对话 (Tripartite Collaborative Chat)
|
||||
* **本地 Web 交互终端**:提供用户与 AI 专属对话的本地可视化 Web 界面。
|
||||
* **全局上下文静默监听**:AI 能够静默监听“我”与“微信联系人”的实时聊天记录,确保 AI 在被唤醒或介入时,已经掌握完整的对话背景。
|
||||
* **灵活的消息代发与审批工作流**:
|
||||
* **模式 A(人审机发)**:我起草或修改合同/文案 -> 发给 AI 检查、润色 -> AI 直接将最终版发送给客户。
|
||||
* **模式 B(机审人发)**:AI 自动起草或修改合同/文案 -> 发给我 -> 我进行人工二次修改和确认 -> 由我发送给客户。
|
||||
|
||||
### 3. AI 行为审计与溯源机制 (Audit & Traceability)
|
||||
* **全量行为事件日志**:详细记录 AI 的每一次工具调用(Skill 执行)、系统决策及状态流转。
|
||||
* **外部交互记录存档**:独立且完整地记录 AI 与所有外部微信联系人的对话明细。
|
||||
* **安全评估与熔断**:基于日志进行溯源,用于评估 AI 行为是否符合预期;支持在出现偏差时的人工介入和及时补救(如消息撤回、服务阻断等)。
|
||||
|
||||
### 4. 记忆库人工维护 (Memory Base Management)
|
||||
* **Hermes Agent 集成**:采用 Hermes 架构理念来管理和沉淀 AI 智能体的状态与经验。
|
||||
* **人工介入知识库**:提供记忆库管理机制,允许人工查阅、修正、添加或删除 AI 的长短期记忆,确保 AI 的业务知识和认知能够准确演进。
|
||||
|
||||
---
|
||||
|
||||
## 二、 本地与远端架构切分方案评估与建议
|
||||
|
||||
您目前的方案非常合理,既保证了本地数据处理的执行效率和操作隐私,又充分利用了云端大模型的算力、中心化管控和数据汇总能力。以下是针对该方案的详细拆解与优化建议:
|
||||
|
||||
### 1. 本地服务 (Local) - 侧重于“感知、执行、交互”
|
||||
* **本地 Web 交互界面**:保障您日常操作的响应速度和最高的数据隐私。
|
||||
* **Skill 与工具执行引擎**:由于工具通常需要操作本地文件、调用本地微信客户端 RPA 接口或访问局域网资源,将其配置和执行放在本地是最佳且必须的实践。
|
||||
* **微信协议/客户端监听模块**:负责实时抓取微信聊天记录并发送消息,此模块强依赖本地环境。
|
||||
* **上下文聚合与组装**:在本地将抓取到的微信聊天记录、本地执行的 Skill 结果打包组装成 Prompt,再向上游远端发起请求。
|
||||
|
||||
### 2. 远端服务 (Remote) - 侧重于“大脑、管控、资产”
|
||||
* **大模型 API 路由网关**:统一对接外部 LLM,隐藏真实的 API Key,便于统一计费和限流。
|
||||
* **AI 数字员工身份管理 (ID System)**:
|
||||
* 统一定义和分配不同数字员工的 AI ID。
|
||||
* 集中配置、版本化管理并下发不同 ID 对应的 **系统提示词 (System Prompt)**。
|
||||
* **中心化记忆库 (Hermes Agent Memory)**:
|
||||
* **优势**:放在远端可以实现跨设备、跨终端的同步。未来在其他设备登录,AI 依然拥有同样的上下文和记忆。
|
||||
* 提供统一的后台界面,供人工集中维护和调优记忆。
|
||||
* **行为审计中心 (Log Server)**:
|
||||
* 接收来自本地异步上报的日志数据。
|
||||
* 通过 AI ID 进行区分,提供数据可视化面板,方便集中进行溯源、统计、安全审查和模型能力评估。
|
||||
|
||||
### 3. 架构切分建议与注意事项
|
||||
1. **私有化安全与本地独享模式 (Privacy & Local-exclusive AI)**:由于远端平台部署在局域网内,仅处理工作内容,无需进行额外的数据脱敏。若用户需要处理极度涉密数据或纯个人隐私数据,系统支持配置“本地独享 AI”模式:该模式下,AI 仅在本地运行和可见,不向远端平台上传任何聊天记录或行为日志,实现彻底的本地物理隔离。
|
||||
2. **弱网容灾机制**:本地应有简易的日志队列缓存。当局域网网络波动时,日志可先落盘本地,网络恢复后自动补传,保证审计数据的完整性。
|
||||
3. **指令与配置下发通道**:如果远端平台修改了 System Prompt 或人工更新了记忆库,需要有一个机制(如 WebSocket 长连接或本地定时轮询)让本地应用能够实时感知并更新,避免配置滞后。
|
||||
|
||||
### 总结
|
||||
您设计的这套架构是典型的 **“云端大脑 + 边缘四肢(端云结合)”** 架构。远端统筹认知、记忆与合规管控,本地负责敏捷感知(监听微信)、行动(工具调用)和即时交互。整体方案逻辑清晰、健壮,非常有利于后续的持续迭代与多数字员工扩展。
|
||||
2
archive/test-ai.js
Normal file
2
archive/test-ai.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const { streamText } = require('ai');
|
||||
console.log(Object.keys(streamText({})));
|
||||
9
archive/test-ai.mjs
Normal file
9
archive/test-ai.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
import { streamText } from 'ai';
|
||||
const mockModel = {
|
||||
provider: 'mock',
|
||||
specificationVersion: 'v1',
|
||||
defaultObjectGenerationMode: 'json',
|
||||
doStream: async () => ({ stream: new ReadableStream(), rawCall: { rawPrompt: '', rawSettings: {} } })
|
||||
};
|
||||
const result = streamText({ model: mockModel, messages: [] });
|
||||
console.log(Object.keys(result).filter(k => k.includes('to')));
|
||||
473
docs/DBUS_API.md
Normal file
473
docs/DBUS_API.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 线切割控制系统 DBus 接口说明文档
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| **服务名** | `com.wirecut.service` |
|
||||
| **对象路径** | `/com/wirecut/control` |
|
||||
| **接口名** | `com.wirecut.IControl` |
|
||||
| **总线类型** | System Bus(系统总线) |
|
||||
| **设计用途** | 供"小龙虾"等外部程序对接线切割控制系统 |
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [运动控制接口](#1-运动控制接口)
|
||||
2. [加工参数接口](#2-加工参数接口)
|
||||
3. [状态查询接口](#3-状态查询接口)
|
||||
4. [NC 文件操作接口](#4-nc-文件操作接口)
|
||||
5. [工件坐标接口](#5-工件坐标接口)
|
||||
6. [放电设置接口](#6-放电设置接口)
|
||||
7. [信号(推送通知)](#7-信号推送通知)
|
||||
8. [状态码说明](#8-状态码说明)
|
||||
9. [命令行调用示例](#9-命令行调用示例)
|
||||
10. [注意事项](#10-注意事项)
|
||||
|
||||
---
|
||||
|
||||
## 1. 运动控制接口
|
||||
|
||||
### startRun() - 启动加工
|
||||
- **描述**: 启动加工程序,等价于点击界面"运行"按钮
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```powershell
|
||||
# 启动加工
|
||||
qdbus com.wirecut.service /com/wirecut/control startRun
|
||||
```
|
||||
|
||||
### stopRun() - 停止加工
|
||||
- **描述**: 停止当前加工,等价于点击"停止"按钮
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control stopRun
|
||||
```
|
||||
|
||||
### pauseRun() - 暂停加工
|
||||
- **描述**: 暂停加工(变频),等价于点击"变频暂停"
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus mock_qdbus.py com.wirecut.service /com/wirecut/control pauseRun
|
||||
```
|
||||
|
||||
### homeAll() - 全轴回零
|
||||
- **描述**: 所有轴执行回零操作
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control homeAll
|
||||
```
|
||||
|
||||
### startKongZou() - 开始空走
|
||||
- **描述**: 启动空走模式(不放电测试)
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control startKongZou
|
||||
```
|
||||
|
||||
### stopKongZou() - 停止空走
|
||||
- **描述**: 停止空走模式
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control stopKongZou
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 加工参数接口
|
||||
|
||||
### setSpeed(speed: int) -> int - 设置加工速度
|
||||
- **描述**: 设置加工速度
|
||||
- **参数**:
|
||||
- `speed`: 速度值,单位 mm/min,范围 1-9999
|
||||
- **返回**: 实际设置的速度值
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setSpeed 80
|
||||
```
|
||||
|
||||
### getSpeed() -> int - 获取设定速度
|
||||
- **描述**: 获取当前设定的加工速度
|
||||
- **参数**: 无
|
||||
- **返回**: 设定速度值 (mm/min)
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getSpeed
|
||||
```
|
||||
|
||||
### setVoltage(vol: int) - 设置放电电压
|
||||
- **描述**: 设置放电电压值
|
||||
- **参数**:
|
||||
- `vol`: 电压值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setVoltage 90
|
||||
```
|
||||
|
||||
### setCurrent(cur: int) - 设置放电电流
|
||||
- **描述**: 设置放电电流值
|
||||
- **参数**:
|
||||
- `cur`: 电流值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setCurrent 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 状态查询接口
|
||||
|
||||
### getStatus() -> QVariantMap - 获取完整状态
|
||||
- **描述**: 获取系统完整状态信息(字典格式)
|
||||
- **参数**: 无
|
||||
- **返回字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `running` | int | 运行状态:0=停止 1=运行中 2=暂停 |
|
||||
| `is_homed` | int | 是否已回零:0=否 1=是 |
|
||||
| `is_homing` | int | 是否正在回零:0=否 1=是 |
|
||||
| `pos_x` | double | X轴位置 (mm) |
|
||||
| `pos_y` | double | Y轴位置 (mm) |
|
||||
| `pos_z` | double | Z轴位置 (mm) |
|
||||
| `pos_u` | double | U轴位置 (mm) |
|
||||
| `pos_v` | double | V轴位置 (mm) |
|
||||
| `vol` | int | 当前电压值 |
|
||||
| `cur` | int | 当前电流值 |
|
||||
| `daohao` | int | 导号(加工段号) |
|
||||
| `speed` | int | 设定速度 (mm/min) |
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getStatus
|
||||
```
|
||||
|
||||
### getAxisPos() -> QString - 获取各轴位置
|
||||
- **描述**: 轻量级接口,获取各轴当前位置
|
||||
- **参数**: 无
|
||||
- **返回**: `"x,y,z,u,v"` 格式字符串,保留3位小数,单位 mm
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getAxisPos
|
||||
# 返回示例: "10.500,20.321,0.000,0.123,0.000"
|
||||
```
|
||||
|
||||
### isRunning() -> bool - 是否正在加工
|
||||
- **描述**: 查询是否处于加工状态
|
||||
- **参数**: 无
|
||||
- **返回**: true=运行中 false=已停止
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control isRunning
|
||||
```
|
||||
|
||||
### isHomed() -> bool - 是否已回零
|
||||
- **描述**: 查询是否已完成回零
|
||||
- **参数**: 无
|
||||
- **返回**: true=已回零 false=未回零
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control isHomed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. NC 文件操作接口
|
||||
|
||||
### loadNC(path: QString) -> bool - 加载 NC 文件
|
||||
- **描述**: 加载 NC 程序文件
|
||||
- **参数**:
|
||||
- `path`: NC 文件的**绝对路径**
|
||||
- **返回**: true=成功 false=失败
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control loadNC "/home/user/test.ngc"
|
||||
```
|
||||
|
||||
### getCurrentNC() -> QString - 获取当前 NC 文件
|
||||
- **描述**: 获取当前已加载的 NC 文件路径
|
||||
- **参数**: 无
|
||||
- **返回**: NC 文件路径
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getCurrentNC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 工件坐标接口
|
||||
|
||||
### clearAxisOffset(axis: int) - 清除单轴工件坐标
|
||||
- **描述**: 清除指定轴的工件坐标偏移
|
||||
- **参数**:
|
||||
- `axis`: 轴编号:0=X 1=Y 2=Z 3=U 4=V 5=C
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 清除 X 轴工件坐标
|
||||
qdbus com.wirecut.service /com/wirecut/control clearAxisOffset 0
|
||||
```
|
||||
|
||||
### clearAllAxisOffset() - 清除所有轴工件坐标
|
||||
- **描述**: 清除全部6个轴的工件坐标偏移
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control clearAllAxisOffset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 放电设置接口
|
||||
|
||||
### ShowDischargeSetting() - 打开放电设置界面
|
||||
- **描述**: 弹出放电设置窗口
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control ShowDischargeSetting
|
||||
```
|
||||
|
||||
### HideDischargeSetting() - 关闭放电设置界面
|
||||
- **描述**: 隐藏放电设置窗口
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control HideDischargeSetting
|
||||
```
|
||||
|
||||
### SetWorkpieceId(workpieceId: int) - 设置当前工件号
|
||||
- **描述**: 切换到指定工件编号
|
||||
- **参数**:
|
||||
- `workpieceId`: 工件编号,范围 0-8
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 切换到工件3
|
||||
qdbus com.wirecut.service /com/wirecut/control SetWorkpieceId 3
|
||||
```
|
||||
|
||||
### SetDischargePara(workpieceId, knifeId, paramType, value) - 设置放电参数
|
||||
- **描述**: 设置指定工件、指定刀号的放电参数
|
||||
- **参数**:
|
||||
- `workpieceId`: 工件编号 (0-8)
|
||||
- `knifeId`: 刀号 (1-11)
|
||||
- `paramType`: 参数类型字符串:
|
||||
- `"voltage"` / `"放电码"` - 放电码
|
||||
- `"current"` / `"跟踪值"` - 跟踪值
|
||||
- `"servo"` / `"速度"` - 速度上限
|
||||
- `value`: 参数值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 设置工件0,刀号1,放电码为80
|
||||
qdbus com.wirecut.service /com/wirecut/control SetDischargePara 0 1 voltage 80
|
||||
```
|
||||
|
||||
### GetDischargePara(workpieceId, knifeId, paramType) -> int - 获取放电参数
|
||||
- **描述**: 获取指定工件、指定刀号的放电参数值
|
||||
- **参数**: 同 SetDischargePara
|
||||
- **返回**: 参数值
|
||||
|
||||
```bash
|
||||
# 获取工件0,刀号1的放电码
|
||||
qdbus com.wirecut.service /com/wirecut/control GetDischargePara 0 1 voltage
|
||||
```
|
||||
|
||||
### CopyToAllWorkpieces() - 复制到所有工件
|
||||
- **描述**: 将当前工件的放电参数复制到所有工件
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control CopyToAllWorkpieces
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 信号(推送通知)
|
||||
|
||||
DBus 服务会主动推送以下信号,外部程序可以监听:
|
||||
|
||||
### runStateChanged(state: int) - 运行状态变化
|
||||
- **触发时机**: 运行状态改变时
|
||||
- **参数**:
|
||||
- `state`: 0=停止 / 1=运行 / 2=暂停
|
||||
|
||||
### alarmMessage(type: int, msg: QString) - 报警/提示
|
||||
- **触发时机**: 系统产生报警或提示时
|
||||
- **参数**:
|
||||
- `type`: 0=提示 / 1=警告 / 2=错误
|
||||
- `msg`: 消息内容
|
||||
|
||||
### axisPosUpdated(x, y, z, u, v: double) - 轴位置刷新
|
||||
- **触发时机**: 每 500ms 周期推送一次
|
||||
- **参数**: 各轴当前位置 (mm)
|
||||
|
||||
### machiningFinished() - 加工完成
|
||||
- **触发时机**: 加工程序执行完成时
|
||||
- **参数**: 无
|
||||
|
||||
---
|
||||
|
||||
## 8. 状态码说明
|
||||
|
||||
### 运行状态码 (running 字段)
|
||||
|
||||
| 值 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| 0 | 停止 | 未运行或已停止 |
|
||||
| 1 | 运行中 | 正在加工 |
|
||||
| 2 | 暂停 | 已暂停(变频) |
|
||||
|
||||
### 轴编号
|
||||
|
||||
| 值 | 轴 | 说明 |
|
||||
|----|-----|------|
|
||||
| 0 | X | X轴 |
|
||||
| 1 | Y | Y轴 |
|
||||
| 2 | Z | Z轴 |
|
||||
| 3 | U | U轴 |
|
||||
| 4 | V | V轴 |
|
||||
| 5 | C | C轴 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 命令行调用示例
|
||||
|
||||
### 完整加工流程
|
||||
|
||||
```bash
|
||||
# 1. 查看当前状态
|
||||
qdbus com.wirecut.service /com/wirecut/control getStatus
|
||||
|
||||
# 2. 加载 NC 文件(必须绝对路径)
|
||||
qdbus com.wirecut.service /com/wirecut/control loadNC "/home/pi/sample.ngc"
|
||||
|
||||
# 3. 设置加工速度
|
||||
qdbus com.wirecut.service /com/wirecut/control setSpeed 80
|
||||
|
||||
# 4. 开始加工
|
||||
qdbus com.wirecut.service /com/wirecut/control startRun
|
||||
|
||||
# 5. 实时查看轴位置
|
||||
qdbus com.wirecut.service /com/wirecut/control getAxisPos
|
||||
|
||||
# 6. 暂停加工
|
||||
qdbus com.wirecut.service /com/wirecut/control pauseRun
|
||||
|
||||
# 7. 停止加工
|
||||
qdbus com.wirecut.service /com/wirecut/control stopRun
|
||||
```
|
||||
|
||||
### 空走测试流程
|
||||
|
||||
```bash
|
||||
# 1. 回零
|
||||
qdbus com.wirecut.service /com/wirecut/control homeAll
|
||||
|
||||
# 2. 开始空走(不放电)
|
||||
qdbus com.wirecut.service /com/wirecut/control startKongZou
|
||||
|
||||
# 3. 停止空走
|
||||
qdbus com.wirecut.service /com/wirecut/control stopKongZou
|
||||
```
|
||||
|
||||
### Python 调用示例(使用 dbus-python)
|
||||
|
||||
```python
|
||||
import dbus
|
||||
|
||||
# 连接系统总线
|
||||
bus = dbus.SystemBus()
|
||||
|
||||
# 获取服务对象
|
||||
obj = bus.get_object('com.wirecut.service', '/com/wirecut/control')
|
||||
iface = dbus.Interface(obj, 'com.wirecut.IControl')
|
||||
|
||||
# 调用方法
|
||||
status = iface.getStatus()
|
||||
print("运行状态:", status['running'])
|
||||
print("X轴位置:", status['pos_x'])
|
||||
|
||||
# 加载 NC 文件
|
||||
iface.loadNC("/home/pi/test.ngc")
|
||||
|
||||
# 启动加工
|
||||
iface.startRun()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
### ⚠️ 重要提醒
|
||||
|
||||
1. **CNC 软件必须运行**
|
||||
- 所有 DBus 接口只有在 CNC 主程序启动后才能使用
|
||||
- 软件未启动时调用会返回 DBus 错误
|
||||
|
||||
2. **NC 文件路径必须为绝对路径**
|
||||
- `loadNC()` 的 path 参数必须使用完整绝对路径
|
||||
- 相对路径会导致加载失败
|
||||
|
||||
3. **速度设置范围**
|
||||
- 速度值范围:1-9999 mm/min
|
||||
- 超出范围会被截断或忽略
|
||||
|
||||
4. **System Bus 权限**
|
||||
- 服务运行在 System Bus 上,可能需要 root 权限
|
||||
- 普通用户调用可能需要配置 DBus 权限规则
|
||||
|
||||
5. **线程安全**
|
||||
- 所有接口调用都是异步队列执行(QueuedConnection)
|
||||
- 调用后不会阻塞,实际执行由主界面事件循环处理
|
||||
|
||||
6. **共享内存依赖**
|
||||
- 部分参数(电压、电流、位置)依赖共享内存
|
||||
- 共享内存未就绪时返回 0 或默认值
|
||||
|
||||
---
|
||||
|
||||
## 附录:DBus 权限配置
|
||||
|
||||
如果普通用户无法调用,需要在 `/etc/dbus-1/system.d/` 下添加权限配置文件 `com.wirecut.conf`:
|
||||
|
||||
```xml
|
||||
<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="pi">
|
||||
<allow own="com.wirecut.service"/>
|
||||
<allow send_destination="com.wirecut.service"/>
|
||||
<allow receive_sender="com.wirecut.service"/>
|
||||
</policy>
|
||||
<policy context="default">
|
||||
<allow send_destination="com.wirecut.service"/>
|
||||
<allow receive_sender="com.wirecut.service"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**生成日期**: 2026-05-20
|
||||
**对应源码**: `/usr/share/runf/wirecutdbus.h` / `wirecutdbus.cpp`
|
||||
642
docs/architecture_overview.html
Normal file
642
docs/architecture_overview.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>工业 AI 交互画布:流程总览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f0f2f5;
|
||||
--card: #fff;
|
||||
--ink: #111827;
|
||||
--muted: #667085;
|
||||
--line: #d8dde6;
|
||||
--soft: #f1f4f8;
|
||||
--blue: #2563eb;
|
||||
--green: #059669;
|
||||
--amber: #d97706;
|
||||
--violet: #7c3aed;
|
||||
--dark: #111827;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif; font-size: 14px; }
|
||||
button { font: inherit; cursor: pointer; border: none; }
|
||||
|
||||
.page { max-width: 1500px; margin: 0 auto; padding: 24px 24px 60px; }
|
||||
|
||||
/* Hero */
|
||||
.hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
||||
h1 { font-size: 22px; font-weight: 800; }
|
||||
.subtitle { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.6; }
|
||||
|
||||
/* Layout */
|
||||
.main-grid { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||
|
||||
/* Left panel */
|
||||
.l-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 18px; }
|
||||
.panel-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; color: var(--ink); }
|
||||
.stack { display: grid; gap: 8px; }
|
||||
.layer { display: grid; grid-template-columns: 30px 1fr; gap: 10px; align-items: center; border: 1px solid var(--line); border-left: 4px solid var(--dark); border-radius: 7px; padding: 9px 12px; }
|
||||
.layer[data-c="blue"] { border-left-color: #2563eb; }
|
||||
.layer[data-c="cyan"] { border-left-color: #0891b2; }
|
||||
.layer[data-c="violet"] { border-left-color: #7c3aed; }
|
||||
.layer[data-c="green"] { border-left-color: #059669; }
|
||||
.layer[data-c="amber"] { border-left-color: #d97706; }
|
||||
.layer[data-c="dark"] { border-left-color: #111827; }
|
||||
.ln { display: flex; height: 24px; width: 24px; align-items: center; justify-content: center; border-radius: 50%; background: var(--soft); font-weight: 800; font-size: 12px; color: #334155; }
|
||||
.lname { font-size: 12px; font-weight: 700; }
|
||||
.ldesc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
||||
|
||||
/* Right: flow panel */
|
||||
.r-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Controls bar */
|
||||
.controls { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fafbfc; flex-wrap: wrap; }
|
||||
.tg { display: inline-flex; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: 9px; background: var(--card); }
|
||||
.tbtn { background: transparent; color: #475467; padding: 8px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; transition: all .15s; white-space: nowrap; }
|
||||
.tbtn.active-flow { background: var(--dark); color: #fff; }
|
||||
.tbtn.active-ux { background: #4f46e5; color: #fff; }
|
||||
.tbtn.active-tech { background: #0d9488; color: #fff; }
|
||||
|
||||
/* View indicator bar */
|
||||
.view-bar { height: 4px; width: 100%; }
|
||||
.view-bar.ux { background: linear-gradient(90deg, #818cf8, #a78bfa); }
|
||||
.view-bar.tech { background: linear-gradient(90deg, #0d9488, #059669); }
|
||||
|
||||
/* Flow body */
|
||||
.flow-body { padding: 22px 26px 30px; overflow-x: auto; }
|
||||
.flow-meta { margin-bottom: 18px; }
|
||||
.flow-meta h2 { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.flow-meta p { font-size: 13px; color: var(--muted); margin-top: 6px; line-height: 1.6; }
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; }
|
||||
.badge-a { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-b { background: #d1fae5; color: #065f46; }
|
||||
.badge-ux { background: #ede9fe; color: #4c1d95; }
|
||||
.badge-tech { background: #ccfbf1; color: #134e4a; }
|
||||
.badge-plan { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.plan-banner { margin: 0 0 16px; padding: 10px 16px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px; font-size: 12px; color: #78350f; line-height: 1.6; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; gap: 10px; flex-wrap: wrap; padding: 9px 14px; background: var(--soft); border-radius: 8px; margin-bottom: 18px; }
|
||||
.lg { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #475467; }
|
||||
.lg-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
|
||||
/* ═══ FLOWCHART BASE ═══ */
|
||||
.fc { display: flex; flex-direction: column; align-items: center; min-width: 580px; }
|
||||
|
||||
/* Nodes */
|
||||
.nd { border-radius: 8px; padding: 9px 16px; text-align: center; border: 1.5px solid var(--line); background: #fff; }
|
||||
.nd .nt { font-size: 13px; font-weight: 700; line-height: 1.4; }
|
||||
.nd .ns { font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.35; }
|
||||
|
||||
/* Node type variants */
|
||||
.nd-start { background: var(--dark); border: none; border-radius: 22px; min-width: 180px; }
|
||||
.nd-start .nt { color: #fff; font-size: 14px; }
|
||||
.nd-start .ns { color: rgba(255,255,255,0.6); }
|
||||
|
||||
/* UX node types */
|
||||
.nd-show { background: #f5f3ff; border-color: #a78bfa; } /* 画面显示 */
|
||||
.nd-show .nt { color: #4c1d95; }
|
||||
.nd-do { background: #ecfdf5; border-color: #34d399; } /* 用户操作 */
|
||||
.nd-do .nt { color: #065f46; }
|
||||
.nd-end { background: #f0fdf4; border-color: #86efac; } /* 结果/结束 */
|
||||
.nd-end .nt { color: #14532d; }
|
||||
.nd-trig { background: #fff7ed; border-color: #fdba74; } /* 触发条件/场景 */
|
||||
.nd-trig .nt { color: #9a3412; }
|
||||
.nd-tab { background: #f0f9ff; border-color: #7dd3fc; border-style: dashed; }
|
||||
.nd-tab .nt { color: #0c4a6e; }
|
||||
|
||||
/* Tech node types */
|
||||
.nd-route { background: #fffbeb; border-color: #fbbf24; }
|
||||
.nd-route .nt { color: #78350f; }
|
||||
.nd-bert { background: #f5f3ff; border-color: #c4b5fd; }
|
||||
.nd-bert .nt { color: #5b21b6; }
|
||||
.nd-llm { background: #fff7ed; border-color: #fdba74; }
|
||||
.nd-llm .nt { color: #9a3412; }
|
||||
.nd-tool { background: #dbeafe; border-color: #60a5fa; }
|
||||
.nd-tool .nt { color: #1e3a8a; }
|
||||
.nd-kb { background: #ecfdf5; border-color: #6ee7b7; }
|
||||
.nd-kb .nt { color: #064e3b; }
|
||||
.nd-art { background: #faf5ff; border-color: #c4b5fd; border-style: dashed; }
|
||||
.nd-art .nt { color: #5b21b6; }
|
||||
.nd-snap { background: #f0fdf4; border-color: #86efac; border-style: dashed; }
|
||||
.nd-snap .nt { color: #14532d; }
|
||||
.nd-minor { background: var(--soft); border-color: var(--line); }
|
||||
.nd-minor .nt { color: #475467; }
|
||||
|
||||
/* Tags inside nodes */
|
||||
.tag { display: inline-block; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; margin: 3px 2px 0; }
|
||||
.t-bert { background: #ede9fe; color: #5b21b6; }
|
||||
.t-llm { background: #fff7ed; color: #9a3412; }
|
||||
.t-rule { background: #dcfce7; color: #166534; }
|
||||
.t-tool { background: #dbeafe; color: #1e40af; }
|
||||
.t-art { background: #faf5ff; color: #5b21b6; }
|
||||
|
||||
/* Connectors */
|
||||
.dn { width: 2px; background: #c9d0db; height: 20px; flex-shrink: 0; }
|
||||
.dn.tall { height: 30px; }
|
||||
.arr { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #b0b9c8; flex-shrink: 0; }
|
||||
|
||||
/* Branch spread link */
|
||||
.slink { position: relative; width: 100%; height: 24px; flex-shrink: 0; }
|
||||
|
||||
/* Branch columns */
|
||||
.brow { display: flex; width: 100%; align-items: flex-start; }
|
||||
.bcol { display: flex; flex-direction: column; align-items: center; flex: 1; padding: 0 7px; min-width: 0; }
|
||||
|
||||
/* Branch labels */
|
||||
.blbl { padding: 4px 11px; border-radius: 20px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||
.bl-b { background: #dbeafe; color: #1d4ed8; border: 1px solid #93c5fd; }
|
||||
.bl-g { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-a { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.bl-v { background: #ede9fe; color: #4c1d95; border: 1px solid #a78bfa; }
|
||||
.bl-gr { background: var(--soft); color: #475467; border: 1px solid var(--line); }
|
||||
.bl-hit { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-miss { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
|
||||
/* Notes */
|
||||
.fnote { font-size: 11px; color: #64748b; margin-top: 4px; text-align: center; line-height: 1.4; }
|
||||
.fopt { font-size: 11px; color: #6d28d9; margin-top: 4px; text-align: center; font-style: italic; }
|
||||
.fwarn { font-size: 11px; color: #b91c1c; margin-top: 4px; text-align: center; }
|
||||
|
||||
/* Divider */
|
||||
.fc-sep { width: 100%; border: none; border-top: 1px dashed var(--line); margin: 4px 0; }
|
||||
|
||||
@media (max-width: 1080px) { .main-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<h1>工业 AI 交互画布 · 操作流程与技术链路</h1>
|
||||
<p class="subtitle">选择视角,分别查看"用户界面交互路径"或"背后技术判断逻辑"。语音输入经过四阶段前置拦截后再进入 BERT NLU。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="main-grid">
|
||||
<!-- Left: Architecture Layers -->
|
||||
<aside class="l-panel">
|
||||
<div class="panel-title">主链路层级</div>
|
||||
<div class="stack">
|
||||
<div class="layer" data-c="blue">
|
||||
<div class="ln">1</div>
|
||||
<div><div class="lname">输入层</div><div class="ldesc">文字 / 语音(ASR) / 点击</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="cyan">
|
||||
<div class="ln">2</div>
|
||||
<div><div class="lname">前置拦截层</div><div class="ldesc">停止词 → UI语音点击 → Slot填写 → BERT</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="violet">
|
||||
<div class="ln">3</div>
|
||||
<div><div class="lname">路由层</div><div class="ldesc">decision: execute / clarify / route_to_cloud</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="green">
|
||||
<div class="ln">4</div>
|
||||
<div><div class="lname">编排层</div><div class="ldesc">工具选择 / 知识检索 / Artifact 生成</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="amber">
|
||||
<div class="ln">5</div>
|
||||
<div><div class="lname">执行层</div><div class="ldesc">PLC / HMI / 知识库调用</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="dark">
|
||||
<div class="ln">6</div>
|
||||
<div><div class="lname">画布层</div><div class="ldesc">渲染 Artifact + 状态快照</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right: Flow Panel -->
|
||||
<section class="r-panel">
|
||||
<div class="controls">
|
||||
<div class="tg" id="flowTabs">
|
||||
<button class="tbtn active-flow" data-flow="A">大流程 A · 工控对话</button>
|
||||
<button class="tbtn" data-flow="B">大流程 B · 调机流程 <span style="font-size:10px;opacity:.7;">🚧 下一版本</span></button>
|
||||
</div>
|
||||
<div class="tg" id="viewTabs">
|
||||
<button class="tbtn active-ux" data-view="ux">👁 交互流程</button>
|
||||
<button class="tbtn" data-view="tech">⚙️ 技术流程</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-bar ux" id="viewBar"></div>
|
||||
<div class="flow-body" id="flowBody"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ─── State ──────────────────────────────────────────────────────────────
|
||||
const S = { flow: 'A', view: 'ux' };
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
const dn = (h = 20) => `<div class="dn" style="height:${h}px"></div>`;
|
||||
const arr = () => `<div class="arr"></div>`;
|
||||
const da = (h = 20) => dn(h) + arr();
|
||||
|
||||
function nd(cls, title, sub = '', tags = []) {
|
||||
const ts = tags.map(t => `<span class="tag t-${t}">${t.toUpperCase()}</span>`).join('');
|
||||
return `<div class="nd ${cls}"><div class="nt">${title}</div>${sub ? `<div class="ns">${sub}</div>` : ''}${ts ? `<div>${ts}</div>` : ''}</div>`;
|
||||
}
|
||||
|
||||
function blbl(text, cls) {
|
||||
return `<div class="blbl ${cls}">${text}</div>`;
|
||||
}
|
||||
|
||||
// Horizontal branch spread link: n = 2 or 3
|
||||
function slink(n) {
|
||||
if (n === 3) {
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:16.67%;right:16.67%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(50% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:25%;right:25%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─── Legends ─────────────────────────────────────────────────────────────
|
||||
const legendUX = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #a78bfa;"></div> 界面展示内容</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #34d399;"></div> 用户可做的操作</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0fdf4;border:1px solid #86efac;"></div> 最终结果/结束态</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0f9ff;border:1px solid #7dd3fc;border-style:dashed;"></div> Tab 分页展示(不覆盖主卡)</div>
|
||||
</div>`;
|
||||
|
||||
const legendTech = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#fffbeb;border:1px solid #fbbf24;"></div> 路由判断节点</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #c4b5fd;"></div> BERT / NLU</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#fff7ed;border:1px solid #fdba74;"></div> LLM 推断</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#dbeafe;border:1px solid #60a5fa;"></div> 工具调用</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #6ee7b7;"></div> 知识库检索</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#faf5ff;border:1px solid #c4b5fd;border-style:dashed;"></div> Artifact 生成</div>
|
||||
</div>`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · UX VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">无调机流程</span> <span class="badge badge-ux">👁 交互流程</span></h2>
|
||||
<p>操作员从零开始发起一次输入。界面根据输入内容呈现三种不同的画布组件,操作员按提示进行确认或查阅。</p>
|
||||
</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '👤 操作员输入文字或语音', '当前画布无激活流程')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '系统识别输入类型,分三种场景响应', '用户感知到的是画布出现了不同内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 1 · 操控设备', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现操控确认卡', '参数变更卡 或 设备动画卡')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员选择:确认或取消', '点击按钮 或 输入"确认/取消"')}
|
||||
${da()}
|
||||
${nd('nd-end', '画布显示执行结果', '成功 / 失败 / 重试')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 2 · 询问设备问题', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现知识教学卡', 'SOP / 报警处理 / 说明书内容')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅内容', '可展开详情、查看步骤')}
|
||||
${da()}
|
||||
${nd('nd-end', '内容展示完毕')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 3 · 通用 / 无关', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '对话框返回文字回答', '打招呼、天气、闲聊等内容')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕', '不影响工业主流程')}
|
||||
<div class="fwarn">⚠ 非核心场景</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · TECH VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">工控对话</span> <span class="badge badge-tech">⚙️ 技术流程</span></h2>
|
||||
<p>语音输入经过四阶段前置拦截(停止词→UI语音点击→Slot填写→BERT),只有前三阶段全部未命中的输入才进入 BERT NLU,再由 decision 字段驱动工具调用、知识检索或 LLM 作答。</p>
|
||||
</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '语音 / 文字输入', '来自 ASR 转录文本 / 直接文字')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 0 · 停止词检测', '命中 cancel 词表 → 直接生成 stop_action,流程终止', ['rule'])}
|
||||
<div class="fnote">词表来自 voice_aliases.yml · cancel_words(静态构建)</div>
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 1 · UI 可见元素语音点击匹配', '优先级:waiting_confirmation affirm/deny > 当前Artifact按钮 > 全局固定操作', ['rule'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 语音点击', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', '生成 ActionEvent', 'actionId / artifactId / sourceText', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布状态机直接响应', '不调用 BERT,不产生新 Artifact')}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 继续', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-route', '阶段 1.5 · waiting_slot + inform 检测', 'session.status=waiting_slot AND 输入为数字/数值', ['rule'])}
|
||||
${slink(2)}
|
||||
<div class="brow" style="width:100%;">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 填槽', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', 'fill_slots 接口', '直接补全当前 slot,不走 BERT', ['rule'])}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入 BERT', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', '阶段 2 · BERT NLU(intelligent_cabin)', 'POST /api/v1/agent/chat\n返回 intent_id + decision + slots', ['bert'])}
|
||||
<div class="fnote">inference 报错直接抛出,不降级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${da(30)}
|
||||
${nd('nd-route', 'decision 字段路由', 'execute / clarify / route_to_cloud / reject', ['rule'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('execute · 设备控制域', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = machine_control\nconfidence_grade = high\nintent_id = wirecut_*')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工业工具调用', 'DBus 写参 / 设备控制指令', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 Artifact', 'ParameterChangeArtifact\nDeviceActionArtifact', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染 + 等待 ActionEvent')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('route_to_cloud · 知识域', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = equipment_knowledge\n或 confidence 偏低')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 语义兜底分析', '提取检索关键词', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索', '说明书 / SOP / 报警手册', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 组织检索结果', '生成教学结构', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 KnowledgeLessonArtifact', '', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('reject · smalltalk / fallback', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = smalltalk\n或无法匹配工业 domain')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '文字回复,不生成 Artifact')}
|
||||
<div class="fnote">不写入 ArtifactStore</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW B · UX VIEW (🚧 下一版本计划中,暂未实现)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderB_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 B · 调机流程中 <span class="badge badge-b">Guided Procedure 激活</span> <span class="badge badge-ux">👁 交互流程</span> <span class="badge badge-plan">🚧 下一版本 · 计划中</span></h2>
|
||||
<p>操作员确认进入调机向导后,调机流程卡占据画布主位。后续输入优先驱动当前步骤,其他内容以 Tab 或浮层展示,调机卡不被覆盖。</p>
|
||||
</div>
|
||||
<div class="plan-banner">⚠️ 此流程为下一版本设计规划,当前版本暂未实现。此图仅用于交互设计参考。</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-show', '调机向导卡在画布主位展示', '步骤列表 · 当前步骤高亮 · 步骤指令可见')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '操作员输入或点击', '文字 / 语音 / 直接点击步骤按钮')}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('✅ 场景 1 · 操作当前步骤', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-do', '输入"已完成/下一步/确认"或直接点击', '等同于用语音/文字替代手点击')}
|
||||
${da()}
|
||||
${nd('nd-show', '当前步骤标记完成,流程卡更新', '进入下一步 · 调机卡不离开主位')}
|
||||
${da()}
|
||||
${nd('nd-end', '所有步骤完成,调机流程收尾', '显示操作汇总')}
|
||||
</div>
|
||||
|
||||
<!-- NOT HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('🔀 场景 2 · 其他内容输入', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-trig', '输入与当前步骤无关', '系统识别为新的操控/知识/通用内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow" style="width:100%;">
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('操控工具', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-tab', 'Tab 出现工具执行结果', '调机 | 工具结果', [])}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查看后可确认')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('知识查询', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-tab', 'Tab 出现知识卡', '调机 | 知识参考', [])}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅后继续流程')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('通用提问', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '临时浮层文字回答', '不占画布主位')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕自动收起')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW B · TECH VIEW (🚧 下一版本计划中,暂未实现)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderB_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 B · 调机流程中 <span class="badge badge-b">Guided Procedure 激活</span> <span class="badge badge-tech">⚙️ 技术流程</span> <span class="badge badge-plan">🚧 下一版本 · 计划中</span></h2>
|
||||
<p>GuidedProcedure Artifact 激活后,输入路由优先做 textAliases 规则匹配(类汽车语音点击),命中则直接更新状态机;未命中再走 BERT 新意图识别,结果以 Tab 渲染,不修改主 Artifact 实例。</p>
|
||||
</div>
|
||||
<div class="plan-banner">⚠️ 此流程为下一版本设计规划,当前版本暂未实现。此图仅用于技术设计参考。</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-art', 'GuidedProcedure Artifact 激活', 'currentStepId · steps[] · actions[textAliases]')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-start', '文本输入', '来自文字 / ASR / 点击')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '路由层:优先匹配当前步骤 textAliases', '规则匹配为主,BERT 置信度辅助确认', ['rule', 'bert'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('命中 · textAliases 匹配成功', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-route', '生成 ArtifactActionEvent', 'actionId / source="text" / transcript', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-tool', '执行步骤对应工具动作', '写入测量値 / 推进步骤状态')}
|
||||
${da()}
|
||||
${nd('nd-bert', 'Reducer 更新 Artifact 状态', 'currentStepId → nextStepId\nstep.status → "completed"')}
|
||||
${da()}
|
||||
${nd('nd-snap', '快照保存(snapshotPolicy: persistent)', '步骤记录存档,支持复盘')}
|
||||
</div>
|
||||
|
||||
<!-- NOT HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入新意图识别', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', 'BERT NLU 识别新意图', '在当前调机上下文中重新分类 domain/intent', ['bert'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow" style="width:100%;">
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('tool_call', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工具执行', '不修改主 Artifact', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', 'Tab 渲染结果', '主 Artifact 不受影响', ['art'])}
|
||||
<div class="fnote">ArtifactStore 主实例不变</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('knowledge_query', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索\n+ LLM 组织', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', 'Tab 渲染知识卡', '主 Artifact 不受影响', ['art'])}
|
||||
<div class="fnote">ArtifactStore 主实例不变</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('smalltalk', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '临时浮层,不写入\nArtifactStore')}
|
||||
<div class="fnote">主 Artifact 完全不受影响</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Render Dispatch ──────────────────────────────────────────────────────
|
||||
const renderers = {
|
||||
A: { ux: renderA_UX, tech: renderA_Tech },
|
||||
B: { ux: renderB_UX, tech: renderB_Tech },
|
||||
};
|
||||
|
||||
function render() {
|
||||
const { flow, view } = S;
|
||||
|
||||
// Flow tabs
|
||||
document.querySelectorAll('#flowTabs .tbtn').forEach(b => {
|
||||
b.classList.toggle('active-flow', b.dataset.flow === flow);
|
||||
});
|
||||
|
||||
// View tabs
|
||||
document.querySelectorAll('#viewTabs .tbtn').forEach(b => {
|
||||
b.classList.remove('active-ux', 'active-tech');
|
||||
if (b.dataset.view === view) {
|
||||
b.classList.add(view === 'ux' ? 'active-ux' : 'active-tech');
|
||||
}
|
||||
});
|
||||
|
||||
// Color bar
|
||||
const bar = document.getElementById('viewBar');
|
||||
bar.className = `view-bar ${view}`;
|
||||
|
||||
// Content
|
||||
document.getElementById('flowBody').innerHTML = renderers[flow][view]();
|
||||
}
|
||||
|
||||
// ─── Events ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('flowTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.flow) { S.flow = btn.dataset.flow; render(); }
|
||||
});
|
||||
|
||||
document.getElementById('viewTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.view) { S.view = btn.dataset.view; render(); }
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
371
docs/bert_integration_analysis.md
Normal file
371
docs/bert_integration_analysis.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# intelligent_cabin BERT 接入 · 冲突分析与整合方案
|
||||
|
||||
> 作者:AI 分析
|
||||
> 日期:2026-05-26
|
||||
> 状态:待你确认
|
||||
|
||||
---
|
||||
|
||||
## 背景速览
|
||||
|
||||
| 侧 | 项目 | 技术栈 | 角色 |
|
||||
|---|---|---|---|
|
||||
| **Canvas(前端/编排侧)** | ai-canvas / Next.js | TypeScript + React | 画布渲染、Artifact 管理、LLM 编排、工业工具调用(DBus/PLC)|
|
||||
| **BERT 服务侧** | intelligent_cabin | Python / FastAPI | 本地 NLU:intent + slot,语音控制场景 |
|
||||
|
||||
这两个项目目前是独立的,尚未对接。
|
||||
|
||||
---
|
||||
|
||||
## 一、冲突点(需要明确决策的地方)
|
||||
|
||||
### 冲突 1:Domain 语义完全不匹配 ⚠️ 重要
|
||||
|
||||
**问题**:`intelligent_cabin/config/domain.yml` 里的 intent 全部来自车机场景(导航、空调、音乐、车窗、车灯……),与 `DBUS_API.md` 描述的线切割工控场景(电压、电流、速度、NC 加工、空走、回零)**毫无重叠**。
|
||||
|
||||
**后果**:如果直接把 BERT 服务接进来,意图识别的类别集合完全覆盖不了工控指令,识别结果几乎全是 unknown/reject。
|
||||
|
||||
**需确认**:
|
||||
- [ ] 你们会用智能车机的这套预训练模型做迁移学习,还是重新训练工控专属的 BERT 模型?
|
||||
- [ ] 还是说这个 BERT 服务只是作为基础推理框架,`domain.yml` 需要被替换成工控版 domain?
|
||||
|
||||
---
|
||||
|
||||
### 冲突 2:置信度阈值体系 vs. Canvas 路由体系
|
||||
|
||||
**Canvas 流程**(参考 `architecture_overview.html`)的技术视图里:
|
||||
```
|
||||
输入 → BERT NLU → confidence 判断 → [高置信+设备控制域] / [知识域/低置信] / [smalltalk]
|
||||
```
|
||||
|
||||
**intelligent_cabin 的路由体系**(`router.py`):
|
||||
```
|
||||
输入 → Classifier → FusionGrader →
|
||||
decision = execute / clarify / route_to_cloud / reject
|
||||
阈值:execute_score=0.55, execute_margin=0.18, route_to_cloud=0.75
|
||||
```
|
||||
|
||||
两套路由各有自己的 confidence 分级,**如果串联,会出现双重过滤**:
|
||||
- BERT 服务输出的 `score` 未必能直接映射到 Canvas 的 `confidence ≥ 阈值` 判断
|
||||
- Canvas 目前只依赖 BERT 输出的 `domain/intent/slot/confidence`,但 BERT 服务输出的是 `intent_id + score + decision`
|
||||
|
||||
**需确认**:
|
||||
- [ ] Canvas 侧是否需要直接消费 `decision` 字段(execute/clarify/route_to_cloud/reject),还是只取 `intent_id + score`,Canvas 自己做二次路由?
|
||||
|
||||
---
|
||||
|
||||
### 冲突 3:`textAliases`(大流程 B)vs. BERT 意图识别的竞争
|
||||
|
||||
**Canvas 大流程 B(调机流程中)的路由逻辑**:
|
||||
```
|
||||
输入 → 优先匹配 textAliases(如"完成"/"下一步"/"确认")
|
||||
→ 未命中 → BERT NLU 重新识别
|
||||
```
|
||||
|
||||
**intelligent_cabin** 里的 `DialogActEngine`(`dialog_act.py`)也在做相似的事情:
|
||||
```python
|
||||
affirm: ["确认", "好的", "继续", "可以", "确定"]
|
||||
cancel: ["取消", "算了", "不用了", "停止"]
|
||||
```
|
||||
|
||||
**冲突**:
|
||||
- `textAliases` 是每个 Artifact Step 私有的,属于 Canvas 状态机层面的规则匹配
|
||||
- `DialogActEngine` 是系统级对话行为分类,优先级/触发顺序未对齐
|
||||
- 如果两套都跑,"确认"这个词可能被 `DialogActEngine` 吃掉后不再传给 Canvas 的步骤推进逻辑
|
||||
|
||||
**整合建议**:
|
||||
- 大流程 B 中,textAliases 匹配应在 BERT 服务之前,Canvas 侧处理,不交给 BERT 服务
|
||||
- 把 `dialog_acts.yml` 的 `affirm/deny/cancel/modify` 从 BERT 服务中剥离,改为 Canvas 路由层的前置规则
|
||||
- 或者让 BERT 服务的 dialog_act 结果作为辅助 metadata 透传,Canvas 侧自行决策
|
||||
|
||||
---
|
||||
|
||||
### 冲突 4:slot 语义不兼容
|
||||
|
||||
**BERT 服务的 slot 体系**(按车机场景设计):
|
||||
- `temperature`:空调温度(16-30°C)
|
||||
- `destination`:导航目的地
|
||||
- `order_id`:订单号
|
||||
- `song`:歌曲名
|
||||
|
||||
**线切割工控的 slot 需求**(从 `DBUS_API.md` 推导):
|
||||
- `speed`:加工速度(mm/min,1-9999)
|
||||
- `voltage`:放电码/电压
|
||||
- `current`:跟踪值/电流
|
||||
- `workpiece_id`:工件编号(0-8)
|
||||
- `knife_id`:刀号(1-11)
|
||||
- `param_type`:参数类型(voltage/current/servo)
|
||||
- `nc_path`:NC 文件绝对路径
|
||||
- `axis`:轴编号(0-5,X/Y/Z/U/V/C)
|
||||
|
||||
两套 slot 定义**完全不同**,需要重写 `domain.yml` 和 `rewrite_engine.py` 里的工控版本。
|
||||
|
||||
---
|
||||
|
||||
### 冲突 5:rewrite_engine 的上下文改写逻辑是车机特化的
|
||||
|
||||
`rewrite_engine.py` 里写死了:
|
||||
```python
|
||||
_AC_CONTEXT_INTENTS = {"cabin_set_ac", "cabin_ac_on", "cabin_ac_off", ...}
|
||||
_AC_DEFAULT_TEMPERATURE = 24
|
||||
_AC_STEP = 2
|
||||
_AC_MIN_TEMPERATURE = 16
|
||||
_AC_MAX_TEMPERATURE = 30
|
||||
```
|
||||
|
||||
这些都是车机空调的业务逻辑,工控场景需要换成类似:
|
||||
```python
|
||||
_PARAM_CONTEXT_INTENTS = {"wirecut_set_speed", "wirecut_set_voltage", "wirecut_set_current"}
|
||||
_SPEED_STEP = 5 # mm/min
|
||||
_VOLTAGE_STEP = 5 # 放电码
|
||||
```
|
||||
|
||||
**但这恰恰也是最有价值可以复用的能力**:多轮相对调节("再快一点"→ 改写为 "速度设为 85mm/min")在工控场景同样刚需。
|
||||
|
||||
---
|
||||
|
||||
## 二、可以直接整合的能力(不需要改动或改动很小)
|
||||
|
||||
### ✅ 整合点 1:FastAPI 服务直接作为 NLU 微服务接入
|
||||
|
||||
Canvas(Next.js 侧)可以通过 HTTP 调用 `POST /api/v1/agent/chat` 或 `POST /api/v1/agent/fill-slots`,作为 Canvas 的 "感知语义层"。
|
||||
|
||||
接入位置:Canvas 的"BERT NLU 意图识别"节点(技术流程视图第 2 层)
|
||||
|
||||
```
|
||||
Canvas 输入 → HTTP POST → intelligent_cabin :8000/api/v1/agent/chat
|
||||
← {intent_id, score, slots, decision}
|
||||
Canvas 路由层根据 intent_id + score 继续分发
|
||||
```
|
||||
|
||||
### ✅ 整合点 2:分级融合决策逻辑(router.py)可以直接复用
|
||||
|
||||
`MultiStageIntentMatcher` 里的 `execute / clarify / route_to_cloud / reject` 四态决策逻辑,与 Canvas 流程中"高置信执行 / 知识域兜底 / smalltalk" 的三路分发思路高度一致,可以直接用。
|
||||
|
||||
Canvas 只需要消费 BERT 服务返回的 `decision` 字段,然后:
|
||||
- `execute` → Canvas 走工具调用路径(DBus)
|
||||
- `clarify` → Canvas 展示澄清确认卡
|
||||
- `route_to_cloud` → Canvas 走 LLM 兜底路径
|
||||
- `reject` → Canvas 走 smalltalk / fallback
|
||||
|
||||
### ✅ 整合点 3:会话状态管理(session_store.py)
|
||||
|
||||
`SessionState` 里的 `context_memory` 和 `slots` 持久化机制,可以支持工控场景的多轮短句恢复(比如 "再快一点" → 记住上次速度值)。
|
||||
|
||||
### ✅ 整合点 4:多命令拆分(planner.py)
|
||||
|
||||
现有的 `sequence workflow` 和 `conditional workflow` 对工控多命令("先回零,再开始加工")直接可用。
|
||||
|
||||
### ✅ 整合点 5:高风险确认机制(dialog_rules.py)
|
||||
|
||||
`requires_confirmation` + `confirmation_required_risk_levels` 这套机制,完全可以映射到 Canvas 流程中"操控确认卡"的场景(Canvas 大流程 A 的场景 1),直接支持危险操作二次确认。
|
||||
|
||||
---
|
||||
|
||||
## 三、需要新建或重写的工控专属配置
|
||||
|
||||
### 3.1 新建 `config/domain_wirecut.yml`
|
||||
|
||||
需要把 DBUS_API.md 里的每个接口映射成 intent:
|
||||
|
||||
| intent_id | 对应 DBus 方法 | 示例语句 | 需要 slot |
|
||||
|---|---|---|---|
|
||||
| `wirecut_start_run` | `startRun()` | "开始加工" / "启动" | 无 |
|
||||
| `wirecut_stop_run` | `stopRun()` | "停止加工" / "停机" | 无 |
|
||||
| `wirecut_pause_run` | `pauseRun()` | "暂停" / "变频暂停" | 无 |
|
||||
| `wirecut_home_all` | `homeAll()` | "全轴回零" / "回零" | 无 |
|
||||
| `wirecut_start_kongzou` | `startKongZou()` | "开始空走" | 无 |
|
||||
| `wirecut_stop_kongzou` | `stopKongZou()` | "停止空走" | 无 |
|
||||
| `wirecut_set_speed` | `setSpeed(speed)` | "速度调到80" / "加快一点" | `speed:int` |
|
||||
| `wirecut_set_voltage` | `setVoltage(vol)` | "电压设为90" | `voltage:int` |
|
||||
| `wirecut_set_current` | `setCurrent(cur)` | "电流设为5" | `current:int` |
|
||||
| `wirecut_get_status` | `getStatus()` | "查一下状态" / "当前状态" | 无 |
|
||||
| `wirecut_load_nc` | `loadNC(path)` | "加载NC文件" | `nc_path:str` |
|
||||
| `wirecut_set_discharge` | `SetDischargePara(...)` | "设置工件0刀1放电码80" | `workpiece_id, knife_id, param_type, value` |
|
||||
|
||||
### 3.2 重写 `rewrite_engine.py` 里的工控上下文改写规则
|
||||
|
||||
```python
|
||||
# 工控版示例
|
||||
_SPEED_CONTEXT_INTENTS = {"wirecut_set_speed"}
|
||||
_SPEED_STEP = 5 # mm/min 每步调节
|
||||
_SPEED_MIN = 1
|
||||
_SPEED_MAX = 9999
|
||||
|
||||
_VOLTAGE_CONTEXT_INTENTS = {"wirecut_set_voltage"}
|
||||
_VOLTAGE_STEP = 5
|
||||
|
||||
# 改写规则
|
||||
"再快一点" → "速度设为 {last_speed + 5} mm/min"
|
||||
"再慢一点" → "速度设为 {last_speed - 5} mm/min"
|
||||
"电压再高一点" → "电压设为 {last_voltage + 5}"
|
||||
```
|
||||
|
||||
### 3.3 更新 `dialog_acts.yml` 适配工控确认场景
|
||||
|
||||
当前 `dialog_acts.yml` 的 `affirm/deny/cancel` 词表可以保留,但需要补充工控场景的特化语句:
|
||||
|
||||
```yaml
|
||||
# 工控场景补充
|
||||
affirm:
|
||||
+ "执行" / "开始" / "运行" / "启动"
|
||||
cancel:
|
||||
+ "停机" / "急停" / "中止"
|
||||
inform:
|
||||
+ 纯数字(速度值/电压值/电流值的slot填写)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、关于 dialog_act 和 rewrite_engine 能否通过配置文件驱动
|
||||
|
||||
**直接回答:当前已经部分支持,但工控场景需要扩展。**
|
||||
|
||||
### 4.1 dialog_act 配置化现状
|
||||
|
||||
`dialog_act.py` 里的 `DialogActEngine` 是数据驱动的:
|
||||
```python
|
||||
@dataclass
|
||||
class DialogActEngine:
|
||||
patterns: dict[str, tuple[str, ...]] = field(default_factory=...)
|
||||
```
|
||||
|
||||
`config_loader.py` 里已经实现了从 `dialog_acts.yml` 加载:
|
||||
```python
|
||||
def _load_dialog_act_engine(self) -> DialogActEngine:
|
||||
...
|
||||
return DialogActEngine(patterns={item.act_id: tuple(item.phrases) for item in parsed.acts})
|
||||
```
|
||||
|
||||
**✅ 结论**:`dialog_acts.yml` 已经是配置文件,直接修改 `config/dialog_acts.yml` 就能修改 act 词表,**无需改代码**。
|
||||
|
||||
**但是有个缺口**:当前 `dialog_acts.yml` 只支持 `字符串包含` 匹配(`phrase in normalized`),没有支持正则或数值范围匹配。工控场景里 "85" 这样的纯数字(作为速度 slot 的 `inform`)目前是靠 Python 代码里的 `if re.search(r"\d", normalized): return "inform"` 处理的,这个逻辑是硬编码在 `dialog_act.py` 里的,暂时没有配置化。
|
||||
|
||||
**建议优化**(详见第五节)。
|
||||
|
||||
### 4.2 rewrite_engine 配置化现状
|
||||
|
||||
`rewrite_engine.py` 里的上下文改写逻辑**全部硬编码**,没有配置文件支持:
|
||||
|
||||
```python
|
||||
_AC_CONTEXT_INTENTS = {"cabin_set_ac", ...} # 硬编码
|
||||
_AC_DEFAULT_TEMPERATURE = 24 # 硬编码
|
||||
_AC_STEP = 2 # 硬编码
|
||||
```
|
||||
|
||||
**❌ 问题**:不同设备(线切割 / 激光切割 / 注塑机 / 其他)的参数名、步长、范围都不同,每台设备都要改代码是不合理的。
|
||||
|
||||
**建议优化**(详见第五节)。
|
||||
|
||||
---
|
||||
|
||||
## 五、建议优化:将 rewrite_engine 和 dialog_act 配置化
|
||||
|
||||
这是你关心的核心问题,以下是具体方案。
|
||||
|
||||
### 5.1 新增 `config/context_rewrite.yml`
|
||||
|
||||
```yaml
|
||||
# context_rewrite.yml
|
||||
param_contexts:
|
||||
- intent_ids: ["wirecut_set_speed"]
|
||||
slot_name: "speed"
|
||||
unit: "mm/min"
|
||||
step: 5
|
||||
min_value: 1
|
||||
max_value: 9999
|
||||
default_value: 80
|
||||
up_phrases: ["再快一点", "加快", "速度调高", "快一点"]
|
||||
down_phrases: ["再慢一点", "减慢", "速度调低", "慢一点"]
|
||||
rewrite_template: "速度设为 {value} mm/min"
|
||||
|
||||
- intent_ids: ["wirecut_set_voltage"]
|
||||
slot_name: "voltage"
|
||||
unit: ""
|
||||
step: 5
|
||||
min_value: 0
|
||||
max_value: 200
|
||||
default_value: 90
|
||||
up_phrases: ["电压高一点", "电压调高"]
|
||||
down_phrases: ["电压低一点", "电压调低"]
|
||||
rewrite_template: "电压设为 {value}"
|
||||
|
||||
- intent_ids: ["wirecut_set_current"]
|
||||
slot_name: "current"
|
||||
unit: ""
|
||||
step: 1
|
||||
min_value: 0
|
||||
max_value: 30
|
||||
default_value: 5
|
||||
up_phrases: ["电流大一点", "跟踪值高一点"]
|
||||
down_phrases: ["电流小一点", "跟踪值低一点"]
|
||||
rewrite_template: "电流设为 {value}"
|
||||
```
|
||||
|
||||
然后修改 `rewrite_engine.py`,从 `context_rewrite.yml` 驱动逻辑,而不是硬编码。
|
||||
|
||||
### 5.2 扩展 `config/dialog_acts.yml` 支持 inform 的数值捕获
|
||||
|
||||
```yaml
|
||||
# dialog_acts.yml 新增字段
|
||||
acts:
|
||||
- act_id: inform
|
||||
phrases: []
|
||||
# 新增:数值模式检测
|
||||
numeric_patterns: ["\\d+"] # 包含数字就认为是 inform
|
||||
```
|
||||
|
||||
并在 `dialog_act.py` 里支持 `numeric_patterns` 字段,这样 "85" / "22度" 是否算 inform 就可以通过配置控制。
|
||||
|
||||
### 5.3 `Settings`(config.py)里增加工控设备配置项
|
||||
|
||||
`app/core/config.py` 里的 `Settings` 类目前通过环境变量驱动,建议增加工控配置路径:
|
||||
|
||||
```python
|
||||
# 新增到 Settings 类
|
||||
context_rewrite_config_path: str = "config/context_rewrite.yml"
|
||||
device_domain_config_path: str = "config/domain_wirecut.yml" # 可按设备切换
|
||||
```
|
||||
|
||||
这样不同设备(线切割、激光机)只需要切换 `.env` 文件里的 `AGENT_DEVICE_DOMAIN_CONFIG_PATH` 就能换一套 domain + rewrite 配置,**不改代码**。
|
||||
|
||||
---
|
||||
|
||||
## 六、整合架构建议(整体链路)
|
||||
|
||||
```
|
||||
语音输入
|
||||
↓ ASR(前端 / 设备侧)
|
||||
文本输入
|
||||
↓
|
||||
Canvas 路由层(Next.js / TypeScript)
|
||||
├── [大流程 B 激活] textAliases 匹配 → 直接推进 Artifact 步骤(不过 BERT)
|
||||
└── [其他] HTTP → intelligent_cabin :8000
|
||||
↓
|
||||
context rewrite(工控版配置化)
|
||||
↓
|
||||
BERT NLU(工控 domain)
|
||||
↓
|
||||
fusion decision
|
||||
├── execute → Canvas 走 DBus 工具调用
|
||||
├── clarify → Canvas 渲染 Artifact 确认卡
|
||||
├── route_to_cloud → Canvas 走 LLM 兜底 + 知识库
|
||||
└── reject → Canvas smalltalk / fallback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、待你确认的清单
|
||||
|
||||
| # | 问题 | 选项 |
|
||||
|---|---|---|
|
||||
| 1 | BERT 模型是否会重训成工控版,还是继续用车机模型迁移? | A. 重训工控 BERT B. 车机→工控迁移 C. 暂时用 mock/keyword |
|
||||
| 2 | Canvas 是消费 BERT 的 `decision` 字段,还是只取 `intent_id+score` 自己路由? | A. 消费 decision B. 自路由 |
|
||||
| 3 | 大流程 B 的 textAliases 匹配,确认在 Canvas 侧做,不经过 BERT 服务? | A. 是 B. 也经过 BERT 但 BERT 优先级低 |
|
||||
| 4 | rewrite_engine 配置化的优先级?现在要做还是等工控 domain 确定后一起做? | A. 现在就做 B. 等 domain 定了再做 |
|
||||
| 5 | dialog_acts.yml 的 `inform` 数值识别是否需要配置化? | A. 配置化 B. 保持硬编码即可 |
|
||||
| 6 | 不同设备部署时,是一个 BERT 服务实例 + 不同 `.env` 配置,还是多个实例? | A. 单实例多配置 B. 多实例 |
|
||||
|
||||
---
|
||||
|
||||
*本文档由 Antigravity 生成,请确认后继续推进实现。*
|
||||
1332
docs/industrial_ai_interaction_plan.md
Normal file
1332
docs/industrial_ai_interaction_plan.md
Normal file
File diff suppressed because it is too large
Load Diff
310
docs/nlu_integration_design.md
Normal file
310
docs/nlu_integration_design.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# NLU 接入设计方案
|
||||
|
||||
> 状态:**已确认,进入实现阶段**
|
||||
> 关联文档:`bert_integration_analysis.md`、`architecture_overview.html`
|
||||
|
||||
---
|
||||
|
||||
## 第一部分:概念解释 — 两套术语怎么对应
|
||||
|
||||
### 1.1 Canvas 设计里用的词
|
||||
|
||||
在 `architecture_overview.html` 的技术流程视图里,BERT NLU 节点描述为:
|
||||
|
||||
```
|
||||
BERT NLU 意图识别
|
||||
输出 domain / intent / slot / confidence 置信度
|
||||
```
|
||||
|
||||
这是一套**面向业务语义**的描述,每个词的含义:
|
||||
|
||||
| 词 | 含义 | 例子 |
|
||||
|---|---|---|
|
||||
| `domain` | 所属业务域,意图的分组 | `machine_control`、`equipment_knowledge`、`smalltalk` |
|
||||
| `intent` | 用户想做什么,域内的细分动作 | `wirecut_set_speed`、`wirecut_start_run`、`query_alarm` |
|
||||
| `slot` | 动作的具体参数,从句子里提取的关键值 | `speed=80`、`voltage=90`、`axis=X` |
|
||||
| `confidence` | 模型对这次识别结果的置信程度,0~1 | `0.94`(高)、`0.61`(中)、`0.32`(低)|
|
||||
|
||||
Canvas 的路由逻辑就是:拿到这四个值之后,判断 `confidence ≥ 阈值 AND domain = machine_control` → 走工具调用路径。
|
||||
|
||||
---
|
||||
|
||||
### 1.2 intelligent_cabin NLU 服务里用的词
|
||||
|
||||
`intelligent_cabin` 后端分两层输出:
|
||||
|
||||
#### 层 A:JointBertNLU 的原始输出(`joint_nlu.py`)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class JointNluResult:
|
||||
intent_id: str | None # 识别出的意图 id,如 "wirecut_set_speed"
|
||||
intent_score: float # softmax 后的概率,就是置信度,0~1
|
||||
candidates: list[JointCandidate] # Top-K 候选意图及其概率
|
||||
slots: dict[str, Any] # 从句子里提取的 slot,如 {"speed": 80}
|
||||
slot_items: list[JointSlot] # slot 在原文中的精确位置和得分
|
||||
```
|
||||
|
||||
这里的 `intent_id + intent_score + slots` 对应 Canvas 描述里的 `intent + confidence + slot`。
|
||||
`domain` 不是模型直接输出的,而是根据 `intent_id` 在 `domain.yml` 里查到的(`wirecut_set_speed` → domain `machine_control`)。
|
||||
|
||||
#### 层 B:Router / FusionGrader 的决策输出(`router.py`)
|
||||
|
||||
```python
|
||||
# MultiStageIntentMatcher._build_fusion_stage() 里
|
||||
decision = "execute" | "clarify" | "route_to_cloud" | "reject"
|
||||
```
|
||||
|
||||
这是在原始 NLU 结果基础上做的**二次路由判断**,加入了:
|
||||
- 置信度是否够高(`score ≥ execute_score_threshold=0.55`)
|
||||
- 头两名候选的分差是否足够大(`margin ≥ execute_margin_threshold=0.18`)
|
||||
- 是否有多义性(ambiguous)
|
||||
|
||||
它告诉上层"这个识别结果能不能直接执行",而不只是"模型认为是哪个意图"。
|
||||
|
||||
---
|
||||
|
||||
### 1.3 两套词汇的完整映射关系
|
||||
|
||||
```
|
||||
Canvas 的描述 ←→ intelligent_cabin 的实际字段
|
||||
─────────────────────────────────────────────────────────────
|
||||
domain ←→ intent_def.domain (从 domain.yml 查)
|
||||
intent ←→ JointNluResult.intent_id
|
||||
slot ←→ JointNluResult.slots (dict)
|
||||
confidence ←→ JointNluResult.intent_score (0~1)
|
||||
|
||||
(以上是 NLU 层的概念对应)
|
||||
|
||||
Canvas 的路由逻辑 ←→ Router 层的 decision 字段
|
||||
"高置信 + 设备控制域" ←→ decision="execute" AND domain="machine_control"
|
||||
"知识域/低置信兜底" ←→ decision="route_to_cloud" 或 domain="equipment_knowledge"
|
||||
"smalltalk" ←→ decision="reject" 或 social_router 处理
|
||||
```
|
||||
|
||||
> **关键点**:Canvas 当前用 Mock NLU(`src/lib/nlu/mock.ts`),它直接输出 `domain + intent + confidence + routeHint`。
|
||||
> 接入真实 NLU 后,两个项目**原生打通,当成一个项目**,不做兼容适配层,接口可以随时改。
|
||||
|
||||
---
|
||||
|
||||
## 第二部分:Canvas ↔ NLU 服务的统一路由方案
|
||||
|
||||
### 2.1 两个项目合并为一个项目(已确认)
|
||||
|
||||
`intelligent_cabin` 不作为独立服务做适配,而是直接作为 `ai-canvas` 的后端子模块。
|
||||
原来 `src/lib/nlu/mock.ts` 的格式**可以废弃**,不需要保持向后兼容。
|
||||
|
||||
### 2.2 真实 NLU 服务的 HTTP 响应
|
||||
|
||||
调用 `POST /api/v1/agent/chat` 后,服务返回 `ChatResponse`,与路由相关的核心字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"intent": "wirecut_set_speed",
|
||||
"domain": "machine_control",
|
||||
"decision": "execute",
|
||||
"status": "completed",
|
||||
"filled_slots": { "speed": 80 },
|
||||
"routing_debug": {
|
||||
"confidence_grade": "high",
|
||||
"stages": [
|
||||
{ "stage": "classifier", "score": 0.87, "candidates": [...] },
|
||||
{
|
||||
"stage": "fusion",
|
||||
"metadata": { "decision": "execute", "grade": "high",
|
||||
"classifier_signal": 0.87, "classifier_margin": 0.34 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `domain` 字段需要在 intelligent_cabin 的 `ChatResponse` schema 里加上(从 `IntentDefinition.domain` 填充),改动极小。
|
||||
|
||||
### 2.3 Canvas 侧的 NluResult 类型(替换 mock.ts)
|
||||
|
||||
```typescript
|
||||
// src/lib/nlu/types.ts (新建,替换 mock.ts 里的类型)
|
||||
|
||||
export type RouteHint =
|
||||
| "tool_call" // decision=execute + machine_control 域
|
||||
| "knowledge_query" // decision=route_to_cloud 或 equipment_knowledge 域
|
||||
| "smalltalk" // decision=reject
|
||||
| "fallback";
|
||||
|
||||
export type NluResult = {
|
||||
modelVersion: string;
|
||||
domain: string; // 后端直接返回
|
||||
intent: string; // intent_id
|
||||
confidence: number; // classifier stage score
|
||||
slots: Record<string, string | number | boolean>; // filled_slots
|
||||
routeHint: RouteHint;
|
||||
decisionGrade: "high" | "medium" | "low";
|
||||
rawDecision: "execute" | "clarify" | "route_to_cloud" | "reject";
|
||||
};
|
||||
|
||||
export function mapDecisionToRouteHint(
|
||||
decision: string,
|
||||
domain: string
|
||||
): RouteHint {
|
||||
if (decision === "execute") {
|
||||
if (domain === "machine_control") return "tool_call";
|
||||
if (domain === "equipment_knowledge") return "knowledge_query";
|
||||
return "tool_call";
|
||||
}
|
||||
if (decision === "route_to_cloud") return "knowledge_query";
|
||||
if (decision === "reject") return "smalltalk";
|
||||
return "fallback"; // clarify 等待补槽,暂用 fallback
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 confidence 读取位置
|
||||
|
||||
`routing_debug.stages` 里找 `stage === "classifier"` 的记录,取其 `score` 字段。
|
||||
这是 BERT 分类器 softmax 后的原始概率,等价于 Canvas 描述里的 `confidence`。
|
||||
|
||||
---
|
||||
|
||||
## 第三部分:语音处理前置拦截链路(已定稿)
|
||||
|
||||
### 3.1 设计原则
|
||||
|
||||
- **所有当前可见 UI 组件的按钮文本都参与语音匹配**,命中则直接触发点击事件,不调 BERT
|
||||
- 调机流程(GuidedProcedure)当前**不在实现范围**,相关 1c 拦截逻辑暂不涉及
|
||||
- BERT 报错**直接抛出**,不降级,不用 Mock 兜底
|
||||
|
||||
### 3.2 四阶段处理链路
|
||||
|
||||
```
|
||||
ASR 文本输入
|
||||
│
|
||||
▼
|
||||
[阶段 0] 停止词检测 ← 静态词表,构建时嵌入
|
||||
├── 命中 cancel_words → 生成 stop_action,链路终止
|
||||
└── 未命中 ↓
|
||||
│
|
||||
▼
|
||||
[阶段 1] UI 可见元素语音点击匹配 ← 纯前端规则,<1ms
|
||||
├── 1a. session.status=waiting_confirmation 时的 affirm/deny(最高优先级)
|
||||
└── 1b. 当前可见 Artifact 按钮 text 匹配
|
||||
│ 命中任意 → 生成 ActionEvent,走 Canvas 状态机,链路终止
|
||||
│
|
||||
│ 全部未命中
|
||||
▼
|
||||
[阶段 1.5] waiting_slot + inform 检测
|
||||
├── session.status=waiting_slot && 输入为数字/数值类
|
||||
└── 命中 → 调 fill_slots 接口,链路终止
|
||||
│
|
||||
▼
|
||||
[阶段 2] BERT NLU(intelligent_cabin /api/v1/agent/chat)
|
||||
├── 报错 → 直接抛出,不降级
|
||||
├── decision=execute → 工具调用层(DBus)→ Artifact
|
||||
├── decision=clarify → 渲染补槽卡,等待 waiting_slot
|
||||
├── decision=route_to_cloud → LLM + 知识库 → KnowledgeLessonArtifact
|
||||
└── decision=reject → LLM 直接作答,不写 ArtifactStore
|
||||
```
|
||||
|
||||
### 3.3 阶段 1 内部优先级说明
|
||||
|
||||
```typescript
|
||||
// 优先级从高到低(1c 调机 textAliases 暂不实现)
|
||||
const PRIORITY_ORDER = [
|
||||
"waiting_confirmation_affirm_deny", // 1a
|
||||
"visible_artifact_button", // 1b
|
||||
];
|
||||
```
|
||||
|
||||
**为什么 1a 最高**:当高风险操作(如"开始加工")弹出确认卡时,
|
||||
操作员说"确认"应当触发确认动作,而不是响应画布上同时存在的其他按钮。
|
||||
状态(`session.status`)决定优先级,而非文本本身。
|
||||
|
||||
### 3.4 阶段 1 匹配实现(pipeline.ts 骨架)
|
||||
|
||||
```typescript
|
||||
// src/lib/nlu/pipeline.ts
|
||||
|
||||
import { AFFIRM_WORDS, CANCEL_WORDS } from "./voice-aliases.gen"; // 构建时生成
|
||||
|
||||
type ActionEvent = {
|
||||
type: "voice_click_event" | "slot_fill_event" | "stop_action";
|
||||
actionId?: string;
|
||||
artifactId?: string;
|
||||
sourceText: string;
|
||||
};
|
||||
|
||||
export async function processVoiceInput(
|
||||
asrText: string,
|
||||
session: CanvasSession
|
||||
): Promise<NluResult | ActionEvent> {
|
||||
|
||||
// 阶段 0:停止词
|
||||
const norm = normalizeVoice(asrText);
|
||||
if (CANCEL_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "stop_action", sourceText: asrText };
|
||||
}
|
||||
|
||||
// 阶段 1a:waiting_confirmation 状态的 affirm/deny
|
||||
if (session.status === "waiting_confirmation") {
|
||||
if (AFFIRM_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "voice_click_event", actionId: "confirm", sourceText: asrText };
|
||||
}
|
||||
if (CANCEL_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "voice_click_event", actionId: "cancel", sourceText: asrText };
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 1b:当前 Artifact 按钮匹配
|
||||
const voiceClick = matchVoiceToAction(asrText, session.visibleActions);
|
||||
if (voiceClick) {
|
||||
return {
|
||||
type: "voice_click_event",
|
||||
actionId: voiceClick.actionId,
|
||||
artifactId: voiceClick.artifactId,
|
||||
sourceText: asrText,
|
||||
};
|
||||
}
|
||||
|
||||
// 阶段 1.5:waiting_slot + 数值输入
|
||||
if (session.status === "waiting_slot" && isNumericInput(asrText)) {
|
||||
return { type: "slot_fill_event", sourceText: asrText };
|
||||
}
|
||||
|
||||
// 阶段 2:BERT NLU(报错直接抛出)
|
||||
const response = await callNluService(asrText, session.sessionId);
|
||||
return adaptNluResponse(response);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 voice_aliases 配置(已确认:静态构建)
|
||||
|
||||
**词表位置**:`intelligent_cabin/config/voice_aliases.yml`(和 `dialog_acts.yml` 放在一起)
|
||||
|
||||
```yaml
|
||||
# voice_aliases.yml
|
||||
affirm_words: ["确认", "好的", "执行", "是", "对", "继续", "好", "ok"]
|
||||
cancel_words: ["取消", "算了", "不要", "不用", "停止", "停"]
|
||||
|
||||
# 工控设备别名(按 intent_id 分组,用于阶段 1b 的 Artifact voiceActions)
|
||||
intent_aliases:
|
||||
wirecut_start_run: ["开始", "启动", "加工", "跑起来"]
|
||||
wirecut_stop_run: ["停", "停机", "急停", "停止"]
|
||||
wirecut_home_all: ["回零", "归零", "回原点"]
|
||||
wirecut_pause_run: ["暂停", "变频暂停"]
|
||||
```
|
||||
|
||||
**构建时生成**:构建脚本读取 yml → 生成 `src/lib/nlu/voice-aliases.gen.ts`,
|
||||
TypeScript 侧直接 import,不需要运行时 HTTP 请求。
|
||||
|
||||
---
|
||||
|
||||
## 第四部分:下一步实现计划
|
||||
|
||||
| 步骤 | 位置 | 内容 |
|
||||
|---|---|---|
|
||||
| 1 | `intelligent_cabin` Python 侧 | `ChatResponse` schema 加 `domain` 字段,`agent_service.py` 填充 |
|
||||
| 2 | `intelligent_cabin/config/` | 创建 `voice_aliases.yml`,补充工控别名 |
|
||||
| 3 | `src/lib/nlu/` | 新建 `types.ts`,废弃 `mock.ts` 中旧类型 |
|
||||
| 4 | `src/lib/nlu/` | 新建 `pipeline.ts`,实现四阶段处理链路 |
|
||||
| 5 | `src/lib/artifacts/types.ts` | 各 Artifact 类型上加 `voiceActions` 字段 |
|
||||
| 6 | 构建配置 | 添加 yml → ts 生成脚本(`voice-aliases.gen.ts`) |
|
||||
469
docs/归档(包含调机流程).html
Normal file
469
docs/归档(包含调机流程).html
Normal file
@@ -0,0 +1,469 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>工业 AI 交互画布:流程总览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f0f2f5;
|
||||
--card: #fff;
|
||||
--ink: #111827;
|
||||
--muted: #667085;
|
||||
--line: #d8dde6;
|
||||
--soft: #f1f4f8;
|
||||
--blue: #2563eb;
|
||||
--green: #059669;
|
||||
--amber: #d97706;
|
||||
--violet: #7c3aed;
|
||||
--dark: #111827;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif; font-size: 14px; }
|
||||
button { font: inherit; cursor: pointer; border: none; }
|
||||
|
||||
.page { max-width: 1500px; margin: 0 auto; padding: 24px 24px 60px; }
|
||||
|
||||
/* Hero */
|
||||
.hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
||||
h1 { font-size: 22px; font-weight: 800; }
|
||||
.subtitle { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.6; }
|
||||
|
||||
/* Layout */
|
||||
.main-grid { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||
|
||||
/* Left panel */
|
||||
.l-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 18px; }
|
||||
.panel-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; color: var(--ink); }
|
||||
.stack { display: grid; gap: 8px; }
|
||||
.layer { display: grid; grid-template-columns: 30px 1fr; gap: 10px; align-items: center; border: 1px solid var(--line); border-left: 4px solid var(--dark); border-radius: 7px; padding: 9px 12px; }
|
||||
.layer[data-c="blue"] { border-left-color: #2563eb; }
|
||||
.layer[data-c="cyan"] { border-left-color: #0891b2; }
|
||||
.layer[data-c="violet"] { border-left-color: #7c3aed; }
|
||||
.layer[data-c="green"] { border-left-color: #059669; }
|
||||
.layer[data-c="amber"] { border-left-color: #d97706; }
|
||||
.layer[data-c="dark"] { border-left-color: #111827; }
|
||||
.ln { display: flex; height: 24px; width: 24px; align-items: center; justify-content: center; border-radius: 50%; background: var(--soft); font-weight: 800; font-size: 12px; color: #334155; }
|
||||
.lname { font-size: 12px; font-weight: 700; }
|
||||
.ldesc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
||||
|
||||
/* Right: flow panel */
|
||||
.r-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Controls bar */
|
||||
.controls { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fafbfc; flex-wrap: wrap; }
|
||||
.tg { display: inline-flex; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: 9px; background: var(--card); }
|
||||
.tbtn { background: transparent; color: #475467; padding: 8px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; transition: all .15s; white-space: nowrap; }
|
||||
.tbtn.active-flow { background: var(--dark); color: #fff; }
|
||||
.tbtn.active-ux { background: #4f46e5; color: #fff; }
|
||||
.tbtn.active-tech { background: #0d9488; color: #fff; }
|
||||
|
||||
/* View indicator bar */
|
||||
.view-bar { height: 4px; width: 100%; }
|
||||
.view-bar.ux { background: linear-gradient(90deg, #818cf8, #a78bfa); }
|
||||
.view-bar.tech { background: linear-gradient(90deg, #0d9488, #059669); }
|
||||
|
||||
/* Flow body */
|
||||
.flow-body { padding: 22px 26px 30px; overflow-x: auto; }
|
||||
.flow-meta { margin-bottom: 18px; }
|
||||
.flow-meta h2 { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.flow-meta p { font-size: 13px; color: var(--muted); margin-top: 6px; line-height: 1.6; }
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; }
|
||||
.badge-a { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-b { background: #d1fae5; color: #065f46; }
|
||||
.badge-ux { background: #ede9fe; color: #4c1d95; }
|
||||
.badge-tech { background: #ccfbf1; color: #134e4a; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; gap: 10px; flex-wrap: wrap; padding: 9px 14px; background: var(--soft); border-radius: 8px; margin-bottom: 18px; }
|
||||
.lg { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #475467; }
|
||||
.lg-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
|
||||
/* ═══ FLOWCHART BASE ═══ */
|
||||
.fc { display: flex; flex-direction: column; align-items: center; min-width: 580px; }
|
||||
|
||||
/* Nodes */
|
||||
.nd { border-radius: 8px; padding: 9px 16px; text-align: center; border: 1.5px solid var(--line); background: #fff; }
|
||||
.nd .nt { font-size: 13px; font-weight: 700; line-height: 1.4; }
|
||||
.nd .ns { font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.35; }
|
||||
|
||||
/* Node type variants */
|
||||
.nd-start { background: var(--dark); border: none; border-radius: 22px; min-width: 180px; }
|
||||
.nd-start .nt { color: #fff; font-size: 14px; }
|
||||
.nd-start .ns { color: rgba(255,255,255,0.6); }
|
||||
|
||||
/* UX node types */
|
||||
.nd-show { background: #f5f3ff; border-color: #a78bfa; } /* 画面显示 */
|
||||
.nd-show .nt { color: #4c1d95; }
|
||||
.nd-do { background: #ecfdf5; border-color: #34d399; } /* 用户操作 */
|
||||
.nd-do .nt { color: #065f46; }
|
||||
.nd-end { background: #f0fdf4; border-color: #86efac; } /* 结果/结束 */
|
||||
.nd-end .nt { color: #14532d; }
|
||||
.nd-trig { background: #fff7ed; border-color: #fdba74; } /* 触发条件/场景 */
|
||||
.nd-trig .nt { color: #9a3412; }
|
||||
.nd-tab { background: #f0f9ff; border-color: #7dd3fc; border-style: dashed; }
|
||||
.nd-tab .nt { color: #0c4a6e; }
|
||||
|
||||
/* Tech node types */
|
||||
.nd-route { background: #fffbeb; border-color: #fbbf24; }
|
||||
.nd-route .nt { color: #78350f; }
|
||||
.nd-bert { background: #f5f3ff; border-color: #c4b5fd; }
|
||||
.nd-bert .nt { color: #5b21b6; }
|
||||
.nd-llm { background: #fff7ed; border-color: #fdba74; }
|
||||
.nd-llm .nt { color: #9a3412; }
|
||||
.nd-tool { background: #dbeafe; border-color: #60a5fa; }
|
||||
.nd-tool .nt { color: #1e3a8a; }
|
||||
.nd-kb { background: #ecfdf5; border-color: #6ee7b7; }
|
||||
.nd-kb .nt { color: #064e3b; }
|
||||
.nd-art { background: #faf5ff; border-color: #c4b5fd; border-style: dashed; }
|
||||
.nd-art .nt { color: #5b21b6; }
|
||||
.nd-snap { background: #f0fdf4; border-color: #86efac; border-style: dashed; }
|
||||
.nd-snap .nt { color: #14532d; }
|
||||
.nd-minor { background: var(--soft); border-color: var(--line); }
|
||||
.nd-minor .nt { color: #475467; }
|
||||
|
||||
/* Tags inside nodes */
|
||||
.tag { display: inline-block; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; margin: 3px 2px 0; }
|
||||
.t-bert { background: #ede9fe; color: #5b21b6; }
|
||||
.t-llm { background: #fff7ed; color: #9a3412; }
|
||||
.t-rule { background: #dcfce7; color: #166534; }
|
||||
.t-tool { background: #dbeafe; color: #1e40af; }
|
||||
.t-art { background: #faf5ff; color: #5b21b6; }
|
||||
|
||||
/* Connectors */
|
||||
.dn { width: 2px; background: #c9d0db; height: 20px; flex-shrink: 0; }
|
||||
.dn.tall { height: 30px; }
|
||||
.arr { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #b0b9c8; flex-shrink: 0; }
|
||||
|
||||
/* Branch spread link */
|
||||
.slink { position: relative; width: 100%; height: 24px; flex-shrink: 0; }
|
||||
|
||||
/* Branch columns */
|
||||
.brow { display: flex; width: 100%; align-items: flex-start; }
|
||||
.bcol { display: flex; flex-direction: column; align-items: center; flex: 1; padding: 0 7px; min-width: 0; }
|
||||
|
||||
/* Branch labels */
|
||||
.blbl { padding: 4px 11px; border-radius: 20px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||
.bl-b { background: #dbeafe; color: #1d4ed8; border: 1px solid #93c5fd; }
|
||||
.bl-g { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-a { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.bl-v { background: #ede9fe; color: #4c1d95; border: 1px solid #a78bfa; }
|
||||
.bl-gr { background: var(--soft); color: #475467; border: 1px solid var(--line); }
|
||||
.bl-hit { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-miss { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
|
||||
/* Notes */
|
||||
.fnote { font-size: 11px; color: #64748b; margin-top: 4px; text-align: center; line-height: 1.4; }
|
||||
.fopt { font-size: 11px; color: #6d28d9; margin-top: 4px; text-align: center; font-style: italic; }
|
||||
.fwarn { font-size: 11px; color: #b91c1c; margin-top: 4px; text-align: center; }
|
||||
|
||||
/* Divider */
|
||||
.fc-sep { width: 100%; border: none; border-top: 1px dashed var(--line); margin: 4px 0; }
|
||||
|
||||
@media (max-width: 1080px) { .main-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<h1>工业 AI 交互画布 · 操作流程与技术链路</h1>
|
||||
<p class="subtitle">选择视角,分别查看"用户界面交互路径"或"背后技术判断逻辑"。语音输入经过四阶段前置拦截后再进入 BERT NLU。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="main-grid">
|
||||
<!-- Left: Architecture Layers -->
|
||||
<aside class="l-panel">
|
||||
<div class="panel-title">主链路层级</div>
|
||||
<div class="stack">
|
||||
<div class="layer" data-c="blue">
|
||||
<div class="ln">1</div>
|
||||
<div><div class="lname">输入层</div><div class="ldesc">文字 / 语音(ASR) / 点击</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="cyan">
|
||||
<div class="ln">2</div>
|
||||
<div><div class="lname">前置拦截层</div><div class="ldesc">停止词 → UI语音点击 → Slot填写 → BERT</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="violet">
|
||||
<div class="ln">3</div>
|
||||
<div><div class="lname">路由层</div><div class="ldesc">decision: execute / clarify / route_to_cloud</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="green">
|
||||
<div class="ln">4</div>
|
||||
<div><div class="lname">编排层</div><div class="ldesc">工具选择 / 知识检索 / Artifact 生成</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="amber">
|
||||
<div class="ln">5</div>
|
||||
<div><div class="lname">执行层</div><div class="ldesc">PLC / HMI / 知识库调用</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="dark">
|
||||
<div class="ln">6</div>
|
||||
<div><div class="lname">画布层</div><div class="ldesc">渲染 Artifact + 状态快照</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right: Flow Panel -->
|
||||
<section class="r-panel">
|
||||
<div class="controls">
|
||||
<div class="tg" id="viewTabs">
|
||||
<button class="tbtn active-ux" data-view="ux">👁 交互流程</button>
|
||||
<button class="tbtn" data-view="tech">⚙️ 技术流程</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-bar ux" id="viewBar"></div>
|
||||
<div class="flow-body" id="flowBody"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ─── State ────────────────────────────────────────────────────────────────
|
||||
const S = { view: 'ux' };
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
const dn = (h = 20) => `<div class="dn" style="height:${h}px"></div>`;
|
||||
const arr = () => `<div class="arr"></div>`;
|
||||
const da = (h = 20) => dn(h) + arr();
|
||||
|
||||
function nd(cls, title, sub = '', tags = []) {
|
||||
const ts = tags.map(t => `<span class="tag t-${t}">${t.toUpperCase()}</span>`).join('');
|
||||
return `<div class="nd ${cls}"><div class="nt">${title}</div>${sub ? `<div class="ns">${sub}</div>` : ''}${ts ? `<div>${ts}</div>` : ''}</div>`;
|
||||
}
|
||||
|
||||
function blbl(text, cls) {
|
||||
return `<div class="blbl ${cls}">${text}</div>`;
|
||||
}
|
||||
|
||||
// Horizontal branch spread link: n = 2 or 3
|
||||
function slink(n) {
|
||||
if (n === 3) {
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:16.67%;right:16.67%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(50% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:25%;right:25%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─── Legends ─────────────────────────────────────────────────────────────
|
||||
const legendUX = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #a78bfa;"></div> 界面展示内容</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #34d399;"></div> 用户可做的操作</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0fdf4;border:1px solid #86efac;"></div> 最终结果/结束态</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0f9ff;border:1px solid #7dd3fc;border-style:dashed;"></div> Tab 分页展示(不覆盖主卡)</div>
|
||||
</div>`;
|
||||
|
||||
const legendTech = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#fffbeb;border:1px solid #fbbf24;"></div> 路由判断节点</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #c4b5fd;"></div> BERT / NLU</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#fff7ed;border:1px solid #fdba74;"></div> LLM 推断</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#dbeafe;border:1px solid #60a5fa;"></div> 工具调用</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #6ee7b7;"></div> 知识库检索</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#faf5ff;border:1px solid #c4b5fd;border-style:dashed;"></div> Artifact 生成</div>
|
||||
</div>`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · UX VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">无调机流程</span> <span class="badge badge-ux">👁 交互流程</span></h2>
|
||||
<p>操作员从零开始发起一次输入。界面根据输入内容呈现三种不同的画布组件,操作员按提示进行确认或查阅。</p>
|
||||
</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '👤 操作员输入文字或语音', '当前画布无激活流程')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '系统识别输入类型,分三种场景响应', '用户感知到的是画布出现了不同内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 1 · 操控设备', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现操控确认卡', '参数变更卡 或 设备动画卡')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员选择:确认或取消', '点击按钮 或 输入"确认/取消"')}
|
||||
${da()}
|
||||
${nd('nd-end', '画布显示执行结果', '成功 / 失败 / 重试')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 2 · 询问设备问题', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现知识教学卡', 'SOP / 报警处理 / 说明书内容')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅内容', '可展开详情、查看步骤')}
|
||||
${da()}
|
||||
${nd('nd-end', '内容展示完毕')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 3 · 通用 / 无关', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '对话框返回文字回答', '打招呼、天气、闲聊等内容')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕', '不影响工业主流程')}
|
||||
<div class="fwarn">⚠ 非核心场景</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · TECH VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">工控对话</span> <span class="badge badge-tech">⚙️ 技术流程</span></h2>
|
||||
<p>语音输入经过四阶段前置拦截(停止词→UI语音点击→Slot填写→BERT),只有前三阶段全部未命中的输入才进入 BERT NLU,再由 decision 字段驱动工具调用、知识检索或 LLM 作答。</p>
|
||||
</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '语音 / 文字输入', '来自 ASR 转录文本 / 直接文字')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 0 · 停止词检测', '命中 cancel 词表 → 直接生成 stop_action,流程终止', ['rule'])}
|
||||
<div class="fnote">词表来自 voice_aliases.yml · cancel_words(静态构建)</div>
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 1 · UI 可见元素语音点击匹配', '优先级:waiting_confirmation affirm/deny > 当前Artifact按钮 > 全局固定操作', ['rule'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 语音点击', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', '生成 ActionEvent', 'actionId / artifactId / sourceText', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布状态机直接响应', '不调用 BERT,不产生新 Artifact')}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 继续', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-route', '阶段 1.5 · waiting_slot + inform 检测', 'session.status=waiting_slot AND 输入为数字/数值', ['rule'])}
|
||||
${slink(2)}
|
||||
<div class="brow" style="width:100%;">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 填槽', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', 'fill_slots 接口', '直接补全当前 slot,不走 BERT', ['rule'])}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入 BERT', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', '阶段 2 · BERT NLU(intelligent_cabin)', 'POST /api/v1/agent/chat\n返回 intent_id + decision + slots', ['bert'])}
|
||||
<div class="fnote">inference 报错直接抛出,不降级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${da(30)}
|
||||
${nd('nd-route', 'decision 字段路由', 'execute / clarify / route_to_cloud / reject', ['rule'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('execute · 设备控制域', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = machine_control\nconfidence_grade = high\nintent_id = wirecut_*')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工业工具调用', 'DBus 写参 / 设备控制指令', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 Artifact', 'ParameterChangeArtifact\nDeviceActionArtifact', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染 + 等待 ActionEvent')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('route_to_cloud · 知识域', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = equipment_knowledge\n或 confidence 偏低')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 语义兜底分析', '提取检索关键词', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索', '说明书 / SOP / 报警手册', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 组织检索结果', '生成教学结构', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 KnowledgeLessonArtifact', '', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('reject · smalltalk / fallback', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = smalltalk\n或无法匹配工业 domain')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '文字回复,不生成 Artifact')}
|
||||
<div class="fnote">不写入 ArtifactStore</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// ─── Render Dispatch ──────────────────────────────────────────────────────
|
||||
const renderers = {
|
||||
ux: renderA_UX,
|
||||
tech: renderA_Tech,
|
||||
};
|
||||
|
||||
function render() {
|
||||
const { view } = S;
|
||||
|
||||
// View tabs
|
||||
document.querySelectorAll('#viewTabs .tbtn').forEach(b => {
|
||||
b.classList.remove('active-ux', 'active-tech');
|
||||
if (b.dataset.view === view) {
|
||||
b.classList.add(view === 'ux' ? 'active-ux' : 'active-tech');
|
||||
}
|
||||
});
|
||||
|
||||
// Color bar
|
||||
const bar = document.getElementById('viewBar');
|
||||
bar.className = `view-bar ${view}`;
|
||||
|
||||
// Content
|
||||
document.getElementById('flowBody').innerHTML = renderers[view]();
|
||||
}
|
||||
|
||||
// ─── Events ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('viewTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.view) { S.view = btn.dataset.view; render(); }
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
44
intelligent_cabin/.vscode/launch.json
vendored
Normal file
44
intelligent_cabin/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug FastAPI",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"python": "${workspaceFolder}/.venv/bin/python",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"args": [
|
||||
"app.main:app",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"8000",
|
||||
"--reload"
|
||||
],
|
||||
"jinja": true,
|
||||
"justMyCode": false,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug FastAPI Without Reload",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"python": "${workspaceFolder}/.venv/bin/python",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"args": [
|
||||
"app.main:app",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"8000"
|
||||
],
|
||||
"jinja": true,
|
||||
"justMyCode": false,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
161
intelligent_cabin/README.md
Normal file
161
intelligent_cabin/README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Intelligent Cabin Agent
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Create and activate the Python 3.11 virtual environment:
|
||||
|
||||
```bash
|
||||
uv venv .venv --python 3.11
|
||||
source .venv/bin/activate
|
||||
```
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Start the service:
|
||||
|
||||
```bash
|
||||
.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
4. Open the API docs:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/docs
|
||||
```
|
||||
|
||||
Demo UI:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/demo
|
||||
```
|
||||
|
||||
Architecture and flow review:
|
||||
|
||||
```text
|
||||
solution_review.md
|
||||
```
|
||||
|
||||
The demo console supports:
|
||||
|
||||
- local browser session history restore
|
||||
- runtime matcher/classifier/session backend switching
|
||||
- matcher routing debug panel with Top-K candidates
|
||||
- local `rewrite -> keyword/classifier/retrieval -> fusion` decision trace
|
||||
- direct display of classifier backend / raw label / fallback reason / raw candidate payload
|
||||
- workflow JSON visualization
|
||||
|
||||
## Core APIs
|
||||
|
||||
- `POST /api/v1/agent/chat`
|
||||
- `POST /api/v1/agent/fill-slots`
|
||||
- `GET /health`
|
||||
|
||||
## Current Scope
|
||||
|
||||
- Configurable session backend: memory / Redis
|
||||
- Config-driven intent registry
|
||||
- Router layer with pluggable matcher / extractor
|
||||
- Rule-based fast-path intent routing
|
||||
- Basic slot extraction
|
||||
- Plugin registry with mock handlers
|
||||
- Workflow response payloads
|
||||
|
||||
## Runtime Config
|
||||
|
||||
- `AGENT_SESSION_BACKEND=memory|redis`
|
||||
- `AGENT_REDIS_URL=redis://127.0.0.1:6379/0`
|
||||
- `AGENT_REDIS_KEY_PREFIX=agent:session`
|
||||
- `AGENT_SESSION_TTL_SECONDS=86400`
|
||||
- `AGENT_MATCHER_PIPELINE=keyword`
|
||||
- `AGENT_SLOT_EXTRACTOR_BACKEND=heuristic`
|
||||
- `AGENT_CLASSIFIER_BACKEND=mock`
|
||||
- `AGENT_CLASSIFIER_THRESHOLD=1.2`
|
||||
- `AGENT_CLASSIFIER_BERT_THRESHOLD=0.0`
|
||||
- `AGENT_CLASSIFIER_MODEL_PATH=/path/to/model`
|
||||
- `AGENT_CLASSIFIER_LABEL_MAP_PATH=/path/to/label_map.json`
|
||||
- `AGENT_CLASSIFIER_REMOTE_URL=http://127.0.0.1:9000/classify`
|
||||
- `AGENT_CLASSIFIER_REMOTE_TIMEOUT_SECONDS=3.0`
|
||||
- `AGENT_LOCAL_EXECUTE_THRESHOLD=1.65`
|
||||
- `AGENT_LOCAL_ROUTE_TO_CLOUD_THRESHOLD=0.75`
|
||||
- `AGENT_LOCAL_CLARIFY_MARGIN_THRESHOLD=0.12`
|
||||
- `AGENT_PLANNER_BACKEND=heuristic|dashscope`
|
||||
- `AGENT_PLANNER_BASE_URL=https://your-base-url/v1`
|
||||
- `AGENT_PLANNER_API_KEY=your-api-key`
|
||||
- `AGENT_PLANNER_MODEL_NAME=qwen3.5-plus`
|
||||
- `AGENT_PLANNER_TIMEOUT_SECONDS=6.0`
|
||||
|
||||
Matcher pipeline examples:
|
||||
|
||||
- `AGENT_MATCHER_PIPELINE=keyword`
|
||||
- `AGENT_MATCHER_PIPELINE=keyword,classifier,retrieval`
|
||||
- `AGENT_MATCHER_PIPELINE=keyword,retrieval`
|
||||
- `AGENT_MATCHER_PIPELINE=classifier`
|
||||
- `AGENT_MATCHER_PIPELINE=retrieval`
|
||||
|
||||
Classifier backend examples:
|
||||
|
||||
- `AGENT_CLASSIFIER_BACKEND=mock`
|
||||
- `AGENT_CLASSIFIER_BACKEND=bert`
|
||||
- `AGENT_CLASSIFIER_BACKEND=remote`
|
||||
- `AGENT_CLASSIFIER_TOP_K=3`
|
||||
|
||||
For local BERT models:
|
||||
|
||||
- install optional runtime deps such as `transformers` and a backend like `torch`
|
||||
- point `AGENT_CLASSIFIER_MODEL_PATH` to the local model directory
|
||||
- if the model outputs labels like `LABEL_0`, provide `AGENT_CLASSIFIER_LABEL_MAP_PATH`
|
||||
- use `AGENT_CLASSIFIER_BERT_THRESHOLD` instead of the mock threshold
|
||||
|
||||
Example label map:
|
||||
|
||||
```json
|
||||
{
|
||||
"LABEL_0": "cs_query_order",
|
||||
"LABEL_1": "cs_cancel_order",
|
||||
"LABEL_2": "cabin_play_music"
|
||||
}
|
||||
```
|
||||
|
||||
Remote classifier expected request payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "我的订单现在什么情况 A808001",
|
||||
"top_k": 3,
|
||||
"labels": ["cs_query_order", "cs_cancel_order", "cabin_play_music"]
|
||||
}
|
||||
```
|
||||
|
||||
Remote classifier response payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"intent_id": "cs_query_order",
|
||||
"label": "LABEL_0",
|
||||
"score": 0.982,
|
||||
"model_name": "bert-remote-v1",
|
||||
"candidates": [
|
||||
{"label": "LABEL_0", "intent_id": "cs_query_order", "score": 0.982},
|
||||
{"label": "LABEL_1", "intent_id": "cs_cancel_order", "score": 0.011},
|
||||
{"label": "LABEL_7", "intent_id": "cs_query_logistics", "score": 0.007}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When `bert` or `remote` is unavailable or below threshold, the classifier falls back to `mock`, and the demo debug panel shows both the attempted backend and the fallback reason.
|
||||
|
||||
Planner notes:
|
||||
|
||||
- keep the planner key in environment variables, not in source code or front-end code
|
||||
- the planner uses `POST {base_url}/chat/completions`
|
||||
- for DashScope OpenAI-compatible endpoints, use the compatible `v1` base URL and set `AGENT_PLANNER_BACKEND=dashscope`
|
||||
- when cloud planning is unavailable, the service falls back to a local heuristic planner for multi-command splitting
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Replace rule router with classifier + retrieval + LLM
|
||||
- Connect real business plugins
|
||||
- Add automated tests
|
||||
1
intelligent_cabin/app/__init__.py
Normal file
1
intelligent_cabin/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Application package for the intelligent cabin agent service."""
|
||||
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/__pycache__/main.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/__pycache__/main.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
1
intelligent_cabin/app/core/__init__.py
Normal file
1
intelligent_cabin/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core configuration package."""
|
||||
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/bootstrap.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/core/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
323
intelligent_cabin/app/core/bootstrap.py
Normal file
323
intelligent_cabin/app/core/bootstrap.py
Normal file
@@ -0,0 +1,323 @@
|
||||
from app.core.config import settings
|
||||
from app.plugins.base import PluginRegistry
|
||||
from app.plugins.mock import MockPluginExecutor
|
||||
from app.services.agent_service import AgentService
|
||||
from app.services.classifier import (
|
||||
BertIntentClassifier,
|
||||
IntentClassifier,
|
||||
JointBertIntentClassifier,
|
||||
MockIntentClassifier,
|
||||
RemoteIntentClassifier,
|
||||
)
|
||||
from app.services.config_loader import ConfigLoader
|
||||
from app.services.intent_registry import IntentRegistry
|
||||
from app.services.joint_nlu import JointBertNLU
|
||||
from app.services.knowledge_llm import DashScopeKnowledgeLLM
|
||||
from app.services.knowledge_store import KnowledgeStore
|
||||
from app.services.multi_intent_detector import (
|
||||
BertMultiIntentDetector,
|
||||
JointBertMultiIntentDetector,
|
||||
MultiIntentDetector,
|
||||
)
|
||||
from app.services.planner import (
|
||||
CompositeWorkflowPlanner,
|
||||
DashScopeWorkflowPlanner,
|
||||
HeuristicWorkflowPlanner,
|
||||
TemplateWorkflowPlanner,
|
||||
WorkflowPlanner,
|
||||
)
|
||||
from app.services.response_policy import ResponsePolicy
|
||||
from app.services.rewrite_engine import ContextRewriteEngine
|
||||
from app.services.router import (
|
||||
HeuristicSlotExtractor,
|
||||
JointBertSlotExtractor,
|
||||
IntentRouter,
|
||||
Router,
|
||||
build_matcher_pipeline,
|
||||
)
|
||||
from app.services.session_store import InMemorySessionStore, RedisSessionStore, SessionStore
|
||||
from app.services.social import DashScopeSocialResponder, SocialResponder, SocialRouter
|
||||
|
||||
|
||||
def build_session_store(session_backend: str | None = None) -> SessionStore:
|
||||
backend = session_backend or settings.session_backend
|
||||
if backend == "memory":
|
||||
return InMemorySessionStore()
|
||||
if backend == "redis":
|
||||
return RedisSessionStore(
|
||||
redis_url=settings.redis_url,
|
||||
key_prefix=settings.redis_key_prefix,
|
||||
ttl_seconds=settings.session_ttl_seconds,
|
||||
)
|
||||
raise ValueError(f"Unsupported session backend: {backend}")
|
||||
|
||||
|
||||
def build_router(
|
||||
intent_registry: IntentRegistry,
|
||||
matcher_pipeline: str | None = None,
|
||||
classifier_backend: str | None = None,
|
||||
classifier: IntentClassifier | None = None,
|
||||
joint_nlu: JointBertNLU | None = None,
|
||||
) -> Router:
|
||||
active_pipeline = matcher_pipeline or settings.matcher_pipeline
|
||||
matcher_stages = [stage.strip() for stage in active_pipeline.split(",") if stage.strip()]
|
||||
if not matcher_stages:
|
||||
matcher_stages = ["classifier"]
|
||||
if matcher_stages != ["classifier"]:
|
||||
raise ValueError("Only classifier matcher pipeline is supported in bert-first mode")
|
||||
if settings.slot_extractor_backend not in {"heuristic", "joint_bert"}:
|
||||
raise ValueError(f"Unsupported slot extractor backend: {settings.slot_extractor_backend}")
|
||||
classifier = classifier or build_classifier(
|
||||
matcher_pipeline=active_pipeline,
|
||||
classifier_backend=classifier_backend,
|
||||
joint_nlu=joint_nlu,
|
||||
)
|
||||
if settings.slot_extractor_backend == "heuristic":
|
||||
slot_extractor = HeuristicSlotExtractor()
|
||||
else:
|
||||
if joint_nlu is None:
|
||||
raise ValueError("slot_extractor_backend=joint_bert requires a Joint NLU runtime")
|
||||
slot_extractor = JointBertSlotExtractor(joint_nlu)
|
||||
return IntentRouter(
|
||||
matcher=build_matcher_pipeline(
|
||||
intent_registry,
|
||||
matcher_stages,
|
||||
classifier=classifier,
|
||||
route_to_cloud_threshold=settings.local_route_to_cloud_threshold,
|
||||
clarify_margin_threshold=settings.local_clarify_margin_threshold,
|
||||
classifier_execute_score_threshold=settings.local_classifier_execute_score_threshold,
|
||||
classifier_execute_margin_threshold=settings.local_classifier_execute_margin_threshold,
|
||||
),
|
||||
slot_extractor=slot_extractor,
|
||||
)
|
||||
|
||||
|
||||
def build_classifier(
|
||||
matcher_pipeline: str | None = None,
|
||||
classifier_backend: str | None = None,
|
||||
joint_nlu: JointBertNLU | None = None,
|
||||
) -> IntentClassifier | None:
|
||||
active_pipeline = matcher_pipeline or settings.matcher_pipeline
|
||||
active_backend = classifier_backend or settings.classifier_backend
|
||||
if "classifier" not in active_pipeline:
|
||||
return None
|
||||
fallback = MockIntentClassifier(
|
||||
threshold=settings.classifier_threshold,
|
||||
top_k=settings.classifier_top_k,
|
||||
)
|
||||
if active_backend == "mock":
|
||||
return fallback
|
||||
if active_backend == "bert":
|
||||
classifier = BertIntentClassifier(
|
||||
model_path=settings.classifier_model_path,
|
||||
threshold=settings.classifier_bert_threshold,
|
||||
label_map_path=settings.classifier_label_map_path or None,
|
||||
fallback=fallback,
|
||||
top_k=settings.classifier_top_k,
|
||||
)
|
||||
if settings.classifier_warmup_enabled:
|
||||
classifier.warmup(settings.classifier_warmup_text)
|
||||
return classifier
|
||||
if active_backend == "joint_bert":
|
||||
runtime = joint_nlu or build_joint_nlu()
|
||||
classifier = JointBertIntentClassifier(
|
||||
nlu=runtime,
|
||||
threshold=settings.joint_nlu_intent_threshold if settings.joint_nlu_intent_threshold > 0 else 0.0,
|
||||
top_k=settings.joint_nlu_top_k,
|
||||
)
|
||||
if settings.classifier_warmup_enabled:
|
||||
classifier.warmup(settings.classifier_warmup_text)
|
||||
return classifier
|
||||
if active_backend == "remote":
|
||||
return RemoteIntentClassifier(
|
||||
endpoint=settings.classifier_remote_url,
|
||||
timeout_seconds=settings.classifier_remote_timeout_seconds,
|
||||
threshold=settings.classifier_threshold,
|
||||
fallback=fallback,
|
||||
label_map_path=settings.classifier_label_map_path or None,
|
||||
top_k=settings.classifier_top_k,
|
||||
)
|
||||
raise ValueError(f"Unsupported classifier backend: {active_backend}")
|
||||
|
||||
|
||||
def build_agent_service() -> AgentService:
|
||||
return build_agent_service_with_runtime()
|
||||
|
||||
|
||||
def build_agent_service_with_runtime(
|
||||
matcher_pipeline: str | None = None,
|
||||
classifier_backend: str | None = None,
|
||||
session_backend: str | None = None,
|
||||
) -> AgentService:
|
||||
runtime_bundle = load_runtime_bundle()
|
||||
intent_registry = runtime_bundle.intent_registry
|
||||
active_classifier_backend = classifier_backend or settings.classifier_backend
|
||||
needs_joint_nlu = active_classifier_backend == "joint_bert" or settings.slot_extractor_backend == "joint_bert"
|
||||
joint_nlu = build_joint_nlu() if needs_joint_nlu else None
|
||||
classifier = build_classifier(
|
||||
matcher_pipeline=matcher_pipeline or settings.matcher_pipeline,
|
||||
classifier_backend=active_classifier_backend,
|
||||
joint_nlu=joint_nlu,
|
||||
)
|
||||
planner_clause_classifier = (
|
||||
classifier
|
||||
if settings.planner_clause_classifier_enabled and active_classifier_backend in {"bert", "remote", "joint_bert"}
|
||||
else None
|
||||
)
|
||||
multi_intent_detector = build_multi_intent_detector(
|
||||
classifier_backend=classifier_backend,
|
||||
joint_nlu=joint_nlu,
|
||||
)
|
||||
plugin_registry = PluginRegistry()
|
||||
MockPluginExecutor().register(plugin_registry)
|
||||
return AgentService(
|
||||
intent_registry=intent_registry,
|
||||
router=build_router(
|
||||
intent_registry,
|
||||
matcher_pipeline=matcher_pipeline,
|
||||
classifier_backend=active_classifier_backend,
|
||||
classifier=classifier,
|
||||
joint_nlu=joint_nlu,
|
||||
),
|
||||
plugins=plugin_registry,
|
||||
session_store=build_session_store(session_backend=session_backend),
|
||||
rewrite_engine=runtime_bundle.rewrite_engine,
|
||||
response_policy=ResponsePolicy(
|
||||
templates=runtime_bundle.response_templates,
|
||||
intent_hints=runtime_bundle.intent_hints,
|
||||
),
|
||||
dialog_rules=runtime_bundle.dialog_rules,
|
||||
dialog_act_engine=runtime_bundle.dialog_act_engine,
|
||||
planner=build_planner(
|
||||
runtime_bundle.workflow_templates,
|
||||
clause_classifier=planner_clause_classifier,
|
||||
multi_intent_detector=multi_intent_detector,
|
||||
joint_nlu=joint_nlu,
|
||||
),
|
||||
social_router=SocialRouter(),
|
||||
social_responder=build_social_responder(),
|
||||
knowledge_llm=build_knowledge_llm(),
|
||||
)
|
||||
|
||||
|
||||
def build_intent_registry() -> IntentRegistry:
|
||||
return load_runtime_bundle().intent_registry
|
||||
|
||||
|
||||
def load_runtime_bundle():
|
||||
return ConfigLoader(
|
||||
domain_path=settings.domain_config_path,
|
||||
action_path=settings.action_config_path,
|
||||
response_path=settings.response_config_path,
|
||||
form_path=settings.form_config_path,
|
||||
rule_path=settings.rule_config_path,
|
||||
dialog_act_path=settings.dialog_act_config_path,
|
||||
workflow_path=settings.workflow_config_path,
|
||||
legacy_intent_path=settings.intent_config_path,
|
||||
context_rewrite_path=settings.context_rewrite_config_path,
|
||||
).load()
|
||||
|
||||
|
||||
def build_joint_nlu() -> JointBertNLU:
|
||||
runtime = JointBertNLU(
|
||||
model_path=settings.joint_nlu_model_path,
|
||||
intent_threshold=settings.joint_nlu_intent_threshold if settings.joint_nlu_intent_threshold > 0 else None,
|
||||
top_k=settings.joint_nlu_top_k,
|
||||
)
|
||||
if settings.classifier_warmup_enabled:
|
||||
runtime.warmup(settings.classifier_warmup_text)
|
||||
return runtime
|
||||
|
||||
|
||||
def build_multi_intent_detector(
|
||||
classifier_backend: str | None = None,
|
||||
joint_nlu: JointBertNLU | None = None,
|
||||
) -> MultiIntentDetector | None:
|
||||
active_backend = classifier_backend or settings.classifier_backend
|
||||
if not settings.planner_multi_intent_detector_enabled:
|
||||
return None
|
||||
if active_backend not in {"bert", "joint_bert"}:
|
||||
return None
|
||||
if active_backend == "joint_bert":
|
||||
runtime = joint_nlu or build_joint_nlu()
|
||||
detector = JointBertMultiIntentDetector(
|
||||
nlu=runtime,
|
||||
threshold=settings.planner_multi_intent_detector_threshold if settings.planner_multi_intent_detector_threshold > 0 else None,
|
||||
top_k=settings.planner_multi_intent_detector_top_k,
|
||||
max_labels=settings.planner_multi_intent_detector_max_labels,
|
||||
)
|
||||
if settings.classifier_warmup_enabled:
|
||||
detector.warmup(settings.classifier_warmup_text)
|
||||
return detector
|
||||
detector_model_path = settings.planner_multi_intent_detector_model_path or settings.classifier_model_path
|
||||
detector = BertMultiIntentDetector(
|
||||
model_path=detector_model_path,
|
||||
threshold=settings.planner_multi_intent_detector_threshold,
|
||||
top_k=settings.planner_multi_intent_detector_top_k,
|
||||
max_labels=settings.planner_multi_intent_detector_max_labels,
|
||||
)
|
||||
if settings.classifier_warmup_enabled:
|
||||
detector.warmup(settings.classifier_warmup_text)
|
||||
return detector
|
||||
|
||||
|
||||
def build_planner(
|
||||
workflow_templates=None,
|
||||
clause_classifier: IntentClassifier | None = None,
|
||||
multi_intent_detector: MultiIntentDetector | None = None,
|
||||
joint_nlu: JointBertNLU | None = None,
|
||||
) -> WorkflowPlanner:
|
||||
template_planner = TemplateWorkflowPlanner(
|
||||
workflow_templates,
|
||||
clause_classifier=clause_classifier,
|
||||
multi_intent_detector=multi_intent_detector,
|
||||
joint_nlu=joint_nlu,
|
||||
classifier_weight=settings.planner_clause_classifier_weight,
|
||||
model_only_threshold=settings.planner_clause_model_only_threshold,
|
||||
)
|
||||
local_first = CompositeWorkflowPlanner(
|
||||
[
|
||||
template_planner,
|
||||
HeuristicWorkflowPlanner(
|
||||
clause_classifier=clause_classifier,
|
||||
multi_intent_detector=multi_intent_detector,
|
||||
joint_nlu=joint_nlu,
|
||||
classifier_weight=settings.planner_clause_classifier_weight,
|
||||
model_only_threshold=settings.planner_clause_model_only_threshold,
|
||||
),
|
||||
]
|
||||
)
|
||||
if settings.planner_backend == "heuristic":
|
||||
return local_first
|
||||
if settings.planner_backend == "dashscope":
|
||||
cloud_planner = DashScopeWorkflowPlanner(
|
||||
base_url=settings.planner_base_url,
|
||||
api_key=settings.planner_api_key,
|
||||
model_name=settings.planner_model_name,
|
||||
timeout_seconds=settings.planner_timeout_seconds,
|
||||
fallback=local_first,
|
||||
joint_nlu=joint_nlu,
|
||||
)
|
||||
return CompositeWorkflowPlanner([local_first, cloud_planner])
|
||||
raise ValueError(f"Unsupported planner backend: {settings.planner_backend}")
|
||||
|
||||
|
||||
def build_social_responder() -> SocialResponder:
|
||||
return DashScopeSocialResponder(
|
||||
base_url=settings.planner_base_url,
|
||||
api_key=settings.planner_api_key,
|
||||
model_name=settings.planner_model_name,
|
||||
timeout_seconds=settings.planner_timeout_seconds,
|
||||
)
|
||||
|
||||
|
||||
def build_knowledge_llm() -> DashScopeKnowledgeLLM:
|
||||
"""构建知识库 LLM 问答器(与 planner 共用 DashScope 配置)。"""
|
||||
store = KnowledgeStore(settings.knowledge_dir)
|
||||
return DashScopeKnowledgeLLM(
|
||||
base_url=settings.planner_base_url,
|
||||
api_key=settings.planner_api_key,
|
||||
model_name=settings.planner_model_name,
|
||||
knowledge_store=store,
|
||||
timeout_seconds=12.0,
|
||||
)
|
||||
61
intelligent_cabin/app/core/config.py
Normal file
61
intelligent_cabin/app/core/config.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "Intelligent Cabin Agent"
|
||||
app_env: str = "dev"
|
||||
app_host: str = "0.0.0.0"
|
||||
app_port: int = 8000
|
||||
intent_config_path: str = "app/data/intents.json"
|
||||
domain_config_path: str = "config/domain.yml"
|
||||
action_config_path: str = "config/actions.yml"
|
||||
response_config_path: str = "config/responses.yml"
|
||||
form_config_path: str = "config/forms.yml"
|
||||
rule_config_path: str = "config/rules.yml"
|
||||
dialog_act_config_path: str = "config/dialog_acts.yml"
|
||||
workflow_config_path: str = "config/workflows.yml"
|
||||
# 本地上下文改写引擎配置(不同设备可切换不同 yml 文件)
|
||||
context_rewrite_config_path: str = "config/context_rewrite.yml"
|
||||
session_backend: str = "memory"
|
||||
redis_url: str = "redis://127.0.0.1:6379/0"
|
||||
redis_key_prefix: str = "agent:session"
|
||||
session_ttl_seconds: int = 86400
|
||||
matcher_pipeline: str = "classifier"
|
||||
slot_extractor_backend: str = "joint_bert"
|
||||
classifier_backend: str = "joint_bert"
|
||||
classifier_threshold: float = 1.2
|
||||
classifier_bert_threshold: float = 0.0
|
||||
classifier_top_k: int = 3
|
||||
classifier_model_path: str = ""
|
||||
classifier_label_map_path: str = ""
|
||||
classifier_warmup_enabled: bool = True
|
||||
classifier_warmup_text: str = "打开车窗"
|
||||
classifier_remote_url: str = ""
|
||||
classifier_remote_timeout_seconds: float = 3.0
|
||||
joint_nlu_model_path: str = "models/local_joint_bert_nlu"
|
||||
joint_nlu_intent_threshold: float = 0.0
|
||||
joint_nlu_top_k: int = 3
|
||||
local_route_to_cloud_threshold: float = 0.75
|
||||
local_clarify_margin_threshold: float = 0.12
|
||||
local_classifier_execute_score_threshold: float = 0.55
|
||||
local_classifier_execute_margin_threshold: float = 0.18
|
||||
planner_backend: str = "heuristic"
|
||||
planner_base_url: str = ""
|
||||
planner_api_key: str = ""
|
||||
planner_model_name: str = ""
|
||||
planner_timeout_seconds: float = 6.0
|
||||
planner_clause_classifier_enabled: bool = True
|
||||
planner_clause_classifier_weight: float = 1.6
|
||||
planner_clause_model_only_threshold: float = 0.62
|
||||
planner_multi_intent_detector_enabled: bool = True
|
||||
planner_multi_intent_detector_model_path: str = ""
|
||||
planner_multi_intent_detector_threshold: float = 0.0
|
||||
planner_multi_intent_detector_top_k: int = 8
|
||||
planner_multi_intent_detector_max_labels: int = 4
|
||||
# 本地知识库目录(存放 .md 格式知识文档)
|
||||
knowledge_dir: str = "config/knowledge"
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_prefix="AGENT_")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,42 @@
|
||||
{"text":"空调先别吹了,关掉吧","expected_label":"cabin_ac_off","category":"business"}
|
||||
{"text":"车里有点闷,把冷气开起来","expected_label":"cabin_ac_on","category":"business"}
|
||||
{"text":"前挡有雾,赶紧除一下","expected_label":"cabin_defog_front_on","category":"business"}
|
||||
{"text":"后玻璃起雾了,开后挡除雾","expected_label":"cabin_defog_rear_on","category":"business"}
|
||||
{"text":"风太猛了,给我调小一档","expected_label":"cabin_fan_down","category":"business"}
|
||||
{"text":"出风再大一点","expected_label":"cabin_fan_up","category":"business"}
|
||||
{"text":"把灯熄了吧","expected_label":"cabin_lights_off","category":"business"}
|
||||
{"text":"天快黑了,把大灯打开","expected_label":"cabin_lights_on","category":"business"}
|
||||
{"text":"锁上所有车门","expected_label":"cabin_lock_doors","category":"business"}
|
||||
{"text":"两边后视镜收起来","expected_label":"cabin_mirror_fold","category":"business"}
|
||||
{"text":"把后视镜展开准备出发","expected_label":"cabin_mirror_unfold","category":"business"}
|
||||
{"text":"路线不用导了,结束导航","expected_label":"cabin_nav_cancel","category":"business"}
|
||||
{"text":"直接带我去龙阳路地铁站","expected_label":"cabin_nav_to","category":"business"}
|
||||
{"text":"这首不想听了,切到下一首","expected_label":"cabin_next_track","category":"business"}
|
||||
{"text":"音乐先停一下","expected_label":"cabin_pause_music","category":"business"}
|
||||
{"text":"放点适合夜里开车听的歌","expected_label":"cabin_play_music","category":"business"}
|
||||
{"text":"切回上一首","expected_label":"cabin_previous_track","category":"business"}
|
||||
{"text":"座椅加热可以关了","expected_label":"cabin_seat_heat_off","category":"business"}
|
||||
{"text":"主驾座椅加热打开","expected_label":"cabin_seat_heat_on","category":"business"}
|
||||
{"text":"把车内温度定在二十二度","expected_label":"cabin_set_ac","category":"business"}
|
||||
{"text":"天窗给我合上","expected_label":"cabin_sunroof_close","category":"business"}
|
||||
{"text":"把天窗翘起来透透气","expected_label":"cabin_sunroof_open","category":"business"}
|
||||
{"text":"车门解锁一下","expected_label":"cabin_unlock_doors","category":"business"}
|
||||
{"text":"声音太响了,压低点","expected_label":"cabin_volume_down","category":"business"}
|
||||
{"text":"音响直接静音","expected_label":"cabin_volume_mute","category":"business"}
|
||||
{"text":"把媒体音量往上加","expected_label":"cabin_volume_up","category":"business"}
|
||||
{"text":"把四个窗都关严","expected_label":"cabin_window_close","category":"business"}
|
||||
{"text":"左前窗打开一点","expected_label":"cabin_window_open","category":"business"}
|
||||
{"text":"雨停了,把雨刮停掉","expected_label":"cabin_wiper_off","category":"business"}
|
||||
{"text":"开始刮雨刷","expected_label":"cabin_wiper_on","category":"business"}
|
||||
{"text":"订单A551201别发了,撤单","expected_label":"cs_cancel_order","category":"business"}
|
||||
{"text":"A661202这个包裹送到哪了","expected_label":"cs_query_logistics","category":"business"}
|
||||
{"text":"帮我看看A771203这单处理进度","expected_label":"cs_query_order","category":"business"}
|
||||
{"text":"这个事情我要真人来跟进","expected_label":"cs_transfer_human","category":"business"}
|
||||
{"text":"你好呀","expected_label":"__social__","category":"social"}
|
||||
{"text":"早上好,今天心情不错","expected_label":"__social__","category":"social"}
|
||||
{"text":"你叫什么名字来着","expected_label":"__social__","category":"social"}
|
||||
{"text":"今天天气挺舒服的","expected_label":"__social__","category":"social"}
|
||||
{"text":"帮我点份炸鸡外卖","expected_label":"__out_of_scope__","category":"out_of_scope"}
|
||||
{"text":"给我订明晚的酒店","expected_label":"__out_of_scope__","category":"out_of_scope"}
|
||||
{"text":"人活着的意义是什么","expected_label":"__out_of_scope__","category":"out_of_scope"}
|
||||
{"text":"推荐一部悬疑电影","expected_label":"__out_of_scope__","category":"out_of_scope"}
|
||||
@@ -0,0 +1,37 @@
|
||||
{"text":"车里闷,给我透个气,再放点轻松的歌","expected_intent_ids":["cabin_window_open","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"先把空调开起来,顺手把窗户关好","expected_intent_ids":["cabin_ac_on","cabin_window_close"],"category":"cabin_parallel"}
|
||||
{"text":"带我去公司,路上播点民谣","expected_intent_ids":["cabin_nav_to","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"有点热,把温度打到二十一度,再来点音乐","expected_intent_ids":["cabin_set_ac","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"导航去虹桥站,然后把空调打开","expected_intent_ids":["cabin_nav_to","cabin_ac_on"],"category":"cabin_sequence"}
|
||||
{"text":"前挡看不清了,开除雾,风也加大一点","expected_intent_ids":["cabin_defog_front_on","cabin_fan_up"],"category":"cabin_parallel"}
|
||||
{"text":"后面玻璃有雾,先除雾,再把窗关上","expected_intent_ids":["cabin_defog_rear_on","cabin_window_close"],"category":"cabin_sequence"}
|
||||
{"text":"把空调开了,风别太小,再来首歌","expected_intent_ids":["cabin_ac_on","cabin_fan_up","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"去浦东机场,车里凉一点,顺便放点歌","expected_intent_ids":["cabin_nav_to","cabin_set_ac","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"先开一点窗,别那么闷,再把温度调低","expected_intent_ids":["cabin_window_open","cabin_set_ac"],"category":"cabin_parallel"}
|
||||
{"text":"把四个窗都关了,然后播点轻音乐","expected_intent_ids":["cabin_window_close","cabin_play_music"],"category":"cabin_sequence"}
|
||||
{"text":"把天窗打开透口气,再开空调","expected_intent_ids":["cabin_sunroof_open","cabin_ac_on"],"category":"cabin_parallel"}
|
||||
{"text":"开导航去徐家汇,顺便把风量调大","expected_intent_ids":["cabin_nav_to","cabin_fan_up"],"category":"cabin_parallel"}
|
||||
{"text":"音乐停一下,然后导航到公司","expected_intent_ids":["cabin_pause_music","cabin_nav_to"],"category":"cabin_sequence"}
|
||||
{"text":"锁车门,再把后视镜收起来","expected_intent_ids":["cabin_lock_doors","cabin_mirror_fold"],"category":"cabin_sequence"}
|
||||
{"text":"把车门解锁,再把镜子展开","expected_intent_ids":["cabin_unlock_doors","cabin_mirror_unfold"],"category":"cabin_sequence"}
|
||||
{"text":"路线别导了,音乐也停一下","expected_intent_ids":["cabin_nav_cancel","cabin_pause_music"],"category":"cabin_parallel"}
|
||||
{"text":"温度调到二十三度,风稍微小一点","expected_intent_ids":["cabin_set_ac","cabin_fan_down"],"category":"cabin_parallel"}
|
||||
{"text":"查下订单A812301,如果还没发货就取消掉","expected_intent_ids":["cs_query_order","cs_cancel_order"],"category":"cs_conditional"}
|
||||
{"text":"帮我看A812302物流,要是太慢就转人工","expected_intent_ids":["cs_query_logistics","cs_transfer_human"],"category":"cs_conditional"}
|
||||
{"text":"先查一下A812303这单进度,再帮我转人工客服","expected_intent_ids":["cs_query_order","cs_transfer_human"],"category":"cs_sequence"}
|
||||
{"text":"订单A812304先查下状态,再看看物流到了哪","expected_intent_ids":["cs_query_order","cs_query_logistics"],"category":"cs_sequence"}
|
||||
{"text":"我想先看看A812305有没有发货,没发的话直接撤单","expected_intent_ids":["cs_query_order","cs_cancel_order"],"category":"cs_conditional"}
|
||||
{"text":"把空调关掉","expected_intent_ids":["cabin_ac_off"],"category":"single_guard"}
|
||||
{"text":"帮我开一下前挡除雾","expected_intent_ids":["cabin_defog_front_on"],"category":"single_guard"}
|
||||
{"text":"风太大了,往小调一点","expected_intent_ids":["cabin_fan_down"],"category":"single_guard"}
|
||||
{"text":"给我导航到龙阳路","expected_intent_ids":["cabin_nav_to"],"category":"single_guard"}
|
||||
{"text":"来点轻音乐","expected_intent_ids":["cabin_play_music"],"category":"single_guard"}
|
||||
{"text":"把左前窗降一点","expected_intent_ids":["cabin_window_open"],"category":"single_guard"}
|
||||
{"text":"订单A812306不要了,直接取消","expected_intent_ids":["cs_cancel_order"],"category":"single_guard"}
|
||||
{"text":"A812307这个快递到哪了","expected_intent_ids":["cs_query_logistics"],"category":"single_guard"}
|
||||
{"text":"导航去公司,再把空调开开,歌也放起来","expected_intent_ids":["cabin_nav_to","cabin_ac_on","cabin_play_music"],"category":"cabin_parallel"}
|
||||
{"text":"把雨刮打开,顺便关下车窗","expected_intent_ids":["cabin_wiper_on","cabin_window_close"],"category":"cabin_parallel"}
|
||||
{"text":"雨停了,雨刮关掉,再把窗开一点","expected_intent_ids":["cabin_wiper_off","cabin_window_open"],"category":"cabin_sequence"}
|
||||
{"text":"把天窗合上,然后把音乐暂停","expected_intent_ids":["cabin_sunroof_close","cabin_pause_music"],"category":"cabin_sequence"}
|
||||
{"text":"先把音量调大,再切下一首","expected_intent_ids":["cabin_volume_up","cabin_next_track"],"category":"cabin_parallel"}
|
||||
{"text":"静音之后切回上一首","expected_intent_ids":["cabin_volume_mute","cabin_previous_track"],"category":"cabin_sequence"}
|
||||
@@ -0,0 +1,72 @@
|
||||
{"text": "打开车窗并播放音乐", "intent_ids": ["cabin_window_open", "cabin_play_music"]}
|
||||
{"text": "把空调打开然后开下车窗", "intent_ids": ["cabin_ac_on", "cabin_window_open"]}
|
||||
{"text": "导航去公司再来点轻音乐", "intent_ids": ["cabin_nav_to", "cabin_play_music"]}
|
||||
{"text": "把空调调到22度并播放周杰伦", "intent_ids": ["cabin_set_ac", "cabin_play_music"]}
|
||||
{"text": "打开空调顺便把车窗关上", "intent_ids": ["cabin_ac_on", "cabin_window_close"]}
|
||||
{"text": "导航去虹桥机场然后把空调调到21度", "intent_ids": ["cabin_nav_to", "cabin_set_ac"]}
|
||||
{"text": "开下窗,再来首歌", "intent_ids": ["cabin_window_open", "cabin_play_music"]}
|
||||
{"text": "空调开起来,风再大一点", "intent_ids": ["cabin_ac_on", "cabin_fan_up"]}
|
||||
{"text": "把空调调低一点,再把风量开大", "intent_ids": ["cabin_set_ac", "cabin_fan_up"]}
|
||||
{"text": "外面太吵了,关窗,然后放点轻音乐", "intent_ids": ["cabin_window_close", "cabin_play_music"]}
|
||||
{"text": "前挡起雾了,开除雾,再把风量调大", "intent_ids": ["cabin_defog_front_on", "cabin_fan_up"]}
|
||||
{"text": "后挡有雾,除一下,再把窗户关好", "intent_ids": ["cabin_defog_rear_on", "cabin_window_close"]}
|
||||
{"text": "把空调设到24度,顺便透透气", "intent_ids": ["cabin_set_ac", "cabin_window_open"]}
|
||||
{"text": "导航到公司并打开空调", "intent_ids": ["cabin_nav_to", "cabin_ac_on"]}
|
||||
{"text": "去徐家汇,车里太热了顺便降温", "intent_ids": ["cabin_nav_to", "cabin_set_ac"]}
|
||||
{"text": "来点歌,再把车窗打开一点", "intent_ids": ["cabin_play_music", "cabin_window_open"]}
|
||||
{"text": "把风量调小一点,然后放点音乐", "intent_ids": ["cabin_fan_down", "cabin_play_music"]}
|
||||
{"text": "开空调,关窗,播放轻音乐", "intent_ids": ["cabin_ac_on", "cabin_window_close", "cabin_play_music"]}
|
||||
{"text": "导航去最近的充电站,再开一点窗透气", "intent_ids": ["cabin_nav_to", "cabin_window_open"]}
|
||||
{"text": "把温度调到20度,关上车窗,再来一首夜曲", "intent_ids": ["cabin_set_ac", "cabin_window_close", "cabin_play_music"]}
|
||||
{"text": "查一下订单A700001,如果还没发货就取消", "intent_ids": ["cs_query_order", "cs_cancel_order"]}
|
||||
{"text": "帮我看下A700002这单物流,没到的话转人工", "intent_ids": ["cs_query_logistics", "cs_transfer_human"]}
|
||||
{"text": "查下订单A700003现在啥情况,然后帮我转人工", "intent_ids": ["cs_query_order", "cs_transfer_human"]}
|
||||
{"text": "先查订单A700004,再查物流进度", "intent_ids": ["cs_query_order", "cs_query_logistics"]}
|
||||
{"text": "打开空调并导航去公司再放点歌", "intent_ids": ["cabin_ac_on", "cabin_nav_to", "cabin_play_music"]}
|
||||
{"text": "帮我透透气,然后把温度调到21度", "intent_ids": ["cabin_window_open", "cabin_set_ac"]}
|
||||
{"text": "开前挡除雾,再把风开大一点", "intent_ids": ["cabin_defog_front_on", "cabin_fan_up"]}
|
||||
{"text": "后窗除雾后把车窗关上", "intent_ids": ["cabin_defog_rear_on", "cabin_window_close"]}
|
||||
{"text": "导航到浦东机场,空调开一下,来点民谣", "intent_ids": ["cabin_nav_to", "cabin_ac_on", "cabin_play_music"]}
|
||||
{"text": "把车里弄凉快点,顺便放点轻音乐", "intent_ids": ["cabin_set_ac", "cabin_play_music"]}
|
||||
{"text": "打开车窗和空调", "intent_ids": ["cabin_window_open", "cabin_ac_on"]}
|
||||
{"text": "开窗和开空调", "intent_ids": ["cabin_window_open", "cabin_ac_on"]}
|
||||
{"text": "把车窗打开,空调也打开", "intent_ids": ["cabin_window_open", "cabin_ac_on"]}
|
||||
{"text": "车里闷,给我透个气,再放点轻松的歌", "intent_ids": ["cabin_window_open", "cabin_play_music"]}
|
||||
{"text": "透透气,再来一首黄昏", "intent_ids": ["cabin_window_open", "cabin_play_music"]}
|
||||
{"text": "先把空调开起来,顺手把窗户关好", "intent_ids": ["cabin_ac_on", "cabin_window_close"]}
|
||||
{"text": "带我去公司,路上播点民谣", "intent_ids": ["cabin_nav_to", "cabin_play_music"]}
|
||||
{"text": "有点热,把温度打到二十一度,再来点音乐", "intent_ids": ["cabin_set_ac", "cabin_play_music"]}
|
||||
{"text": "导航去虹桥站,然后把空调打开", "intent_ids": ["cabin_nav_to", "cabin_ac_on"]}
|
||||
{"text": "前挡看不清了,开除雾,风也加大一点", "intent_ids": ["cabin_defog_front_on", "cabin_fan_up"]}
|
||||
{"text": "后面玻璃有雾,先除雾,再把窗关上", "intent_ids": ["cabin_defog_rear_on", "cabin_window_close"]}
|
||||
{"text": "把空调开了,风别太小,再来首歌", "intent_ids": ["cabin_ac_on", "cabin_fan_up", "cabin_play_music"]}
|
||||
{"text": "去浦东机场,车里凉一点,顺便放点歌", "intent_ids": ["cabin_nav_to", "cabin_set_ac", "cabin_play_music"]}
|
||||
{"text": "先开一点窗,别那么闷,再把温度调低", "intent_ids": ["cabin_window_open", "cabin_set_ac"]}
|
||||
{"text": "把四个窗都关了,然后播点轻音乐", "intent_ids": ["cabin_window_close", "cabin_play_music"]}
|
||||
{"text": "把天窗打开透口气,再开空调", "intent_ids": ["cabin_sunroof_open", "cabin_ac_on"]}
|
||||
{"text": "开导航去徐家汇,顺便把风量调大", "intent_ids": ["cabin_nav_to", "cabin_fan_up"]}
|
||||
{"text": "音乐停一下,然后导航到公司", "intent_ids": ["cabin_pause_music", "cabin_nav_to"]}
|
||||
{"text": "锁车门,再把后视镜收起来", "intent_ids": ["cabin_lock_doors", "cabin_mirror_fold"]}
|
||||
{"text": "把车门解锁,再把镜子展开", "intent_ids": ["cabin_unlock_doors", "cabin_mirror_unfold"]}
|
||||
{"text": "路线别导了,音乐也停一下", "intent_ids": ["cabin_nav_cancel", "cabin_pause_music"]}
|
||||
{"text": "温度调到二十三度,风稍微小一点", "intent_ids": ["cabin_set_ac", "cabin_fan_down"]}
|
||||
{"text": "查下订单A812301,如果还没发货就取消掉", "intent_ids": ["cs_query_order", "cs_cancel_order"]}
|
||||
{"text": "帮我看A812302物流,要是太慢就转人工", "intent_ids": ["cs_query_logistics", "cs_transfer_human"]}
|
||||
{"text": "先查一下A812303这单进度,再帮我转人工客服", "intent_ids": ["cs_query_order", "cs_transfer_human"]}
|
||||
{"text": "订单A812304先查下状态,再看看物流到了哪", "intent_ids": ["cs_query_order", "cs_query_logistics"]}
|
||||
{"text": "我想先看看A812305有没有发货,没发的话直接撤单", "intent_ids": ["cs_query_order", "cs_cancel_order"]}
|
||||
{"text": "导航去公司,再把空调开开,歌也放起来", "intent_ids": ["cabin_nav_to", "cabin_ac_on", "cabin_play_music"]}
|
||||
{"text": "把雨刮打开,顺便关下车窗", "intent_ids": ["cabin_wiper_on", "cabin_window_close"]}
|
||||
{"text": "雨停了,雨刮关掉,再把窗开一点", "intent_ids": ["cabin_wiper_off", "cabin_window_open"]}
|
||||
{"text": "把天窗合上,然后把音乐暂停", "intent_ids": ["cabin_sunroof_close", "cabin_pause_music"]}
|
||||
{"text": "先把音量调大,再切下一首", "intent_ids": ["cabin_volume_up", "cabin_next_track"]}
|
||||
{"text": "静音之后切回上一首", "intent_ids": ["cabin_volume_mute", "cabin_previous_track"]}
|
||||
{"text": "来点music", "intent_ids": ["cabin_play_music"]}
|
||||
{"text": "来点音乐,想听黄昏", "intent_ids": ["cabin_play_music"]}
|
||||
{"text": "放周杰伦的黄昏", "intent_ids": ["cabin_play_music"]}
|
||||
{"text": "来一首黄昏", "intent_ids": ["cabin_play_music"]}
|
||||
{"text": "给我放黄昏", "intent_ids": ["cabin_play_music"]}
|
||||
{"text": "开窗顺便放点歌", "intent_ids": ["cabin_window_open", "cabin_play_music"]}
|
||||
{"text": "导航到公司同时放点轻音乐", "intent_ids": ["cabin_nav_to", "cabin_play_music"]}
|
||||
{"text": "空调打开再把风量调大", "intent_ids": ["cabin_ac_on", "cabin_fan_up"]}
|
||||
{"text": "关窗再暂停音乐", "intent_ids": ["cabin_window_close", "cabin_pause_music"]}
|
||||
35
intelligent_cabin/app/data/bert_intent_test.jsonl
Normal file
35
intelligent_cabin/app/data/bert_intent_test.jsonl
Normal file
@@ -0,0 +1,35 @@
|
||||
{"text":"查一下订单A700001现在什么状态","intent_id":"cs_query_order"}
|
||||
{"text":"我的订单A700002到哪一步了","intent_id":"cs_query_order"}
|
||||
{"text":"帮我看看A700003这个订单","intent_id":"cs_query_order"}
|
||||
{"text":"订单A700004现在处理到哪里","intent_id":"cs_query_order"}
|
||||
{"text":"确认下A700005订单状态","intent_id":"cs_query_order"}
|
||||
{"text":"帮我查A800001物流进度","intent_id":"cs_query_logistics"}
|
||||
{"text":"快递A800002到哪儿了","intent_id":"cs_query_logistics"}
|
||||
{"text":"看看A800003配送状态","intent_id":"cs_query_logistics"}
|
||||
{"text":"订单A800004物流更新了吗","intent_id":"cs_query_logistics"}
|
||||
{"text":"查询A800005的快递信息","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我取消A900001这个订单","intent_id":"cs_cancel_order"}
|
||||
{"text":"A900002别要了给我撤销","intent_id":"cs_cancel_order"}
|
||||
{"text":"把订单A900003取消掉","intent_id":"cs_cancel_order"}
|
||||
{"text":"我不要A900004了","intent_id":"cs_cancel_order"}
|
||||
{"text":"撤销一下A900005订单","intent_id":"cs_cancel_order"}
|
||||
{"text":"我要找人工客服处理","intent_id":"cs_transfer_human"}
|
||||
{"text":"现在转人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"麻烦给我接人工服务","intent_id":"cs_transfer_human"}
|
||||
{"text":"帮我呼叫真人客服","intent_id":"cs_transfer_human"}
|
||||
{"text":"别机器人了我要人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"导航到公司停车场","intent_id":"cabin_nav_to"}
|
||||
{"text":"带我去浦东机场T2","intent_id":"cabin_nav_to"}
|
||||
{"text":"去最近的服务区","intent_id":"cabin_nav_to"}
|
||||
{"text":"我要去徐家汇","intent_id":"cabin_nav_to"}
|
||||
{"text":"开导航去虹桥机场","intent_id":"cabin_nav_to"}
|
||||
{"text":"把空调设到22度","intent_id":"cabin_set_ac"}
|
||||
{"text":"车里温度调成24度","intent_id":"cabin_set_ac"}
|
||||
{"text":"冷气开到20度","intent_id":"cabin_set_ac"}
|
||||
{"text":"空调给我调低一点到21度","intent_id":"cabin_set_ac"}
|
||||
{"text":"温度改成23度","intent_id":"cabin_set_ac"}
|
||||
{"text":"播放一首轻音乐","intent_id":"cabin_play_music"}
|
||||
{"text":"来点周杰伦的歌","intent_id":"cabin_play_music"}
|
||||
{"text":"放一首夜曲","intent_id":"cabin_play_music"}
|
||||
{"text":"我想听摇滚","intent_id":"cabin_play_music"}
|
||||
{"text":"给我播点古典音乐","intent_id":"cabin_play_music"}
|
||||
118
intelligent_cabin/app/data/bert_intent_train.jsonl
Normal file
118
intelligent_cabin/app/data/bert_intent_train.jsonl
Normal file
@@ -0,0 +1,118 @@
|
||||
{"text":"帮我查一下订单A123456","intent_id":"cs_query_order"}
|
||||
{"text":"查询订单A765432现在到哪一步了","intent_id":"cs_query_order"}
|
||||
{"text":"我的订单A998877是什么状态","intent_id":"cs_query_order"}
|
||||
{"text":"帮我看看订单A556677","intent_id":"cs_query_order"}
|
||||
{"text":"查下订单A112233","intent_id":"cs_query_order"}
|
||||
{"text":"订单A456789现在怎么样","intent_id":"cs_query_order"}
|
||||
{"text":"我想查订单A333444","intent_id":"cs_query_order"}
|
||||
{"text":"看看订单A909090进度","intent_id":"cs_query_order"}
|
||||
{"text":"订单A202501状态","intent_id":"cs_query_order"}
|
||||
{"text":"帮我确认一下订单A808001","intent_id":"cs_query_order"}
|
||||
{"text":"查物流A123456","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我看订单A765432物流","intent_id":"cs_query_logistics"}
|
||||
{"text":"快递A998877到哪了","intent_id":"cs_query_logistics"}
|
||||
{"text":"物流A556677现在什么情况","intent_id":"cs_query_logistics"}
|
||||
{"text":"查一下快递单A112233","intent_id":"cs_query_logistics"}
|
||||
{"text":"看看A456789物流信息","intent_id":"cs_query_logistics"}
|
||||
{"text":"订单A333444的快递到了吗","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我查查A909090配送进度","intent_id":"cs_query_logistics"}
|
||||
{"text":"物流单号A202501现在在哪里","intent_id":"cs_query_logistics"}
|
||||
{"text":"我的订单A808001物流到了哪","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我取消订单A123456","intent_id":"cs_cancel_order"}
|
||||
{"text":"取消一下A765432这个订单","intent_id":"cs_cancel_order"}
|
||||
{"text":"撤销订单A998877","intent_id":"cs_cancel_order"}
|
||||
{"text":"订单A556677我不要了","intent_id":"cs_cancel_order"}
|
||||
{"text":"把A112233这个订单取消掉","intent_id":"cs_cancel_order"}
|
||||
{"text":"我想退掉并取消A456789","intent_id":"cs_cancel_order"}
|
||||
{"text":"帮我撤回订单A333444","intent_id":"cs_cancel_order"}
|
||||
{"text":"A909090别发了直接取消","intent_id":"cs_cancel_order"}
|
||||
{"text":"取消订单号A202501","intent_id":"cs_cancel_order"}
|
||||
{"text":"把A808001撤销了","intent_id":"cs_cancel_order"}
|
||||
{"text":"帮我转人工客服","intent_id":"cs_transfer_human"}
|
||||
{"text":"我要人工服务","intent_id":"cs_transfer_human"}
|
||||
{"text":"接人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"帮我联系人工客服","intent_id":"cs_transfer_human"}
|
||||
{"text":"转接人工处理","intent_id":"cs_transfer_human"}
|
||||
{"text":"这个问题我要找人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"给我人工坐席","intent_id":"cs_transfer_human"}
|
||||
{"text":"不要机器人了转人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"请给我人工客服","intent_id":"cs_transfer_human"}
|
||||
{"text":"帮我找真人客服","intent_id":"cs_transfer_human"}
|
||||
{"text":"导航去公司","intent_id":"cabin_nav_to"}
|
||||
{"text":"带我去机场","intent_id":"cabin_nav_to"}
|
||||
{"text":"去虹桥火车站","intent_id":"cabin_nav_to"}
|
||||
{"text":"导航到世纪大道","intent_id":"cabin_nav_to"}
|
||||
{"text":"帮我开车去陆家嘴","intent_id":"cabin_nav_to"}
|
||||
{"text":"去最近的充电站","intent_id":"cabin_nav_to"}
|
||||
{"text":"带我到南京东路","intent_id":"cabin_nav_to"}
|
||||
{"text":"导航去浦东机场","intent_id":"cabin_nav_to"}
|
||||
{"text":"去静安寺","intent_id":"cabin_nav_to"}
|
||||
{"text":"我要去公司园区","intent_id":"cabin_nav_to"}
|
||||
{"text":"把空调调到22度","intent_id":"cabin_set_ac"}
|
||||
{"text":"空调设成24度","intent_id":"cabin_set_ac"}
|
||||
{"text":"温度调低到20度","intent_id":"cabin_set_ac"}
|
||||
{"text":"车里空调开到23度","intent_id":"cabin_set_ac"}
|
||||
{"text":"帮我把车内温度设置为21度","intent_id":"cabin_set_ac"}
|
||||
{"text":"空调打到25度","intent_id":"cabin_set_ac"}
|
||||
{"text":"把冷气调成19度","intent_id":"cabin_set_ac"}
|
||||
{"text":"把空调温度改为26度","intent_id":"cabin_set_ac"}
|
||||
{"text":"车内设到18度","intent_id":"cabin_set_ac"}
|
||||
{"text":"温度给我设成22度","intent_id":"cabin_set_ac"}
|
||||
{"text":"播放轻音乐","intent_id":"cabin_play_music"}
|
||||
{"text":"来点歌","intent_id":"cabin_play_music"}
|
||||
{"text":"帮我放首歌","intent_id":"cabin_play_music"}
|
||||
{"text":"我想听周杰伦","intent_id":"cabin_play_music"}
|
||||
{"text":"来一首夜曲","intent_id":"cabin_play_music"}
|
||||
{"text":"放点摇滚","intent_id":"cabin_play_music"}
|
||||
{"text":"听一下古典音乐","intent_id":"cabin_play_music"}
|
||||
{"text":"给我播轻音乐","intent_id":"cabin_play_music"}
|
||||
{"text":"来点民谣","intent_id":"cabin_play_music"}
|
||||
{"text":"播放默认歌单","intent_id":"cabin_play_music"}
|
||||
{"text":"订单A310001现在受理了吗","intent_id":"cs_query_order"}
|
||||
{"text":"帮我看下A310002这单进展","intent_id":"cs_query_order"}
|
||||
{"text":"A310003这个订单处理到哪了","intent_id":"cs_query_order"}
|
||||
{"text":"查询一下订单A310004当前状态","intent_id":"cs_query_order"}
|
||||
{"text":"订单A310005现在啥情况","intent_id":"cs_query_order"}
|
||||
{"text":"帮我确认A310006订单有没有在处理","intent_id":"cs_query_order"}
|
||||
{"text":"A310007这笔订单最新进度是什么","intent_id":"cs_query_order"}
|
||||
{"text":"看看订单A310008有没有结果","intent_id":"cs_query_order"}
|
||||
{"text":"订单A320001物流到哪了","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我查A320002这单配送进度","intent_id":"cs_query_logistics"}
|
||||
{"text":"A320003快递现在运到哪里了","intent_id":"cs_query_logistics"}
|
||||
{"text":"看一下订单A320004有没有派件","intent_id":"cs_query_logistics"}
|
||||
{"text":"A320005物流轨迹更新了吗","intent_id":"cs_query_logistics"}
|
||||
{"text":"帮我追踪一下A320006运输状态","intent_id":"cs_query_logistics"}
|
||||
{"text":"订单A320007快件到哪一步了","intent_id":"cs_query_logistics"}
|
||||
{"text":"A320008这单现在送到哪儿了","intent_id":"cs_query_logistics"}
|
||||
{"text":"查一下A320009的派送信息","intent_id":"cs_query_logistics"}
|
||||
{"text":"A320010物流有没有新动态","intent_id":"cs_query_logistics"}
|
||||
{"text":"订单A330001不要了,帮我撤单","intent_id":"cs_cancel_order"}
|
||||
{"text":"A330002这单别发了直接取消","intent_id":"cs_cancel_order"}
|
||||
{"text":"把订单A330003停掉吧","intent_id":"cs_cancel_order"}
|
||||
{"text":"A330004这个订单我不想要了","intent_id":"cs_cancel_order"}
|
||||
{"text":"订单A330005给我撤回","intent_id":"cs_cancel_order"}
|
||||
{"text":"把A330006这笔订单关掉","intent_id":"cs_cancel_order"}
|
||||
{"text":"A330007先别发货了,取消掉","intent_id":"cs_cancel_order"}
|
||||
{"text":"帮我把订单A330008作废","intent_id":"cs_cancel_order"}
|
||||
{"text":"A330009这单不要了","intent_id":"cs_cancel_order"}
|
||||
{"text":"订单A330010撤单处理","intent_id":"cs_cancel_order"}
|
||||
{"text":"这个问题给我人工跟进","intent_id":"cs_transfer_human"}
|
||||
{"text":"安排真人客服接手","intent_id":"cs_transfer_human"}
|
||||
{"text":"机器人处理不了,转人工","intent_id":"cs_transfer_human"}
|
||||
{"text":"帮我叫个客服专员","intent_id":"cs_transfer_human"}
|
||||
{"text":"我要人工来处理这事","intent_id":"cs_transfer_human"}
|
||||
{"text":"规划路线去虹桥机场T2","intent_id":"cabin_nav_to"}
|
||||
{"text":"直接开去公司停车场","intent_id":"cabin_nav_to"}
|
||||
{"text":"给我导到最近的充电站","intent_id":"cabin_nav_to"}
|
||||
{"text":"徐家汇怎么走,导航一下","intent_id":"cabin_nav_to"}
|
||||
{"text":"出发去外滩","intent_id":"cabin_nav_to"}
|
||||
{"text":"把车内温度设为20度","intent_id":"cabin_set_ac"}
|
||||
{"text":"空调温度改到21度","intent_id":"cabin_set_ac"}
|
||||
{"text":"冷气帮我调到22度","intent_id":"cabin_set_ac"}
|
||||
{"text":"舱内调成23度","intent_id":"cabin_set_ac"}
|
||||
{"text":"给我把温度定在24度","intent_id":"cabin_set_ac"}
|
||||
{"text":"随机放点轻音乐","intent_id":"cabin_play_music"}
|
||||
{"text":"帮我播首晴天","intent_id":"cabin_play_music"}
|
||||
{"text":"来点适合开车听的民谣","intent_id":"cabin_play_music"}
|
||||
{"text":"打开音乐,放夜曲","intent_id":"cabin_play_music"}
|
||||
{"text":"给我放一些爵士","intent_id":"cabin_play_music"}
|
||||
352
intelligent_cabin/app/data/intents.json
Normal file
352
intelligent_cabin/app/data/intents.json
Normal file
@@ -0,0 +1,352 @@
|
||||
[
|
||||
{
|
||||
"intent_id": "cs_query_order",
|
||||
"plugin_id": "plugin.order.query",
|
||||
"domain": "customer_service",
|
||||
"risk_level": "low",
|
||||
"required_slots": ["order_id"],
|
||||
"ask_templates": {
|
||||
"order_id": "请提供订单号。"
|
||||
},
|
||||
"keywords": ["查询订单", "查订单", "订单状态"],
|
||||
"examples": ["帮我查一下订单", "我的订单到哪一步了", "查下订单状态"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cs_query_logistics",
|
||||
"plugin_id": "plugin.logistics.query",
|
||||
"domain": "customer_service",
|
||||
"risk_level": "low",
|
||||
"required_slots": ["order_id"],
|
||||
"ask_templates": {
|
||||
"order_id": "请提供订单号。"
|
||||
},
|
||||
"keywords": ["查物流", "物流", "快递"],
|
||||
"examples": ["帮我查一下快递", "看看物流到哪了", "快递什么时候到"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cs_cancel_order",
|
||||
"plugin_id": "plugin.order.cancel",
|
||||
"domain": "customer_service",
|
||||
"risk_level": "medium",
|
||||
"required_slots": ["order_id"],
|
||||
"ask_templates": {
|
||||
"order_id": "请提供要取消的订单号。"
|
||||
},
|
||||
"keywords": ["取消订单", "撤销订单"],
|
||||
"examples": ["帮我取消这个订单", "我不想要了取消吧", "撤销刚才的订单"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cs_transfer_human",
|
||||
"plugin_id": "plugin.service.transfer_human",
|
||||
"domain": "customer_service",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["转人工", "人工客服", "联系客服"],
|
||||
"examples": ["我要人工客服", "帮我转人工", "联系客服"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_nav_cancel",
|
||||
"plugin_id": "plugin.cabin.navigation.cancel",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["取消导航", "结束导航", "停止导航"],
|
||||
"examples": ["把导航关掉", "退出导航", "别导航了"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_nav_to",
|
||||
"plugin_id": "plugin.cabin.navigation",
|
||||
"domain": "cabin",
|
||||
"risk_level": "medium",
|
||||
"required_slots": ["destination"],
|
||||
"ask_templates": {
|
||||
"destination": "请告诉我要导航去哪里。"
|
||||
},
|
||||
"keywords": ["导航去", "导航到", "带我去"],
|
||||
"examples": ["导航去公司", "带我去机场", "导航到虹桥火车站"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_ac_on",
|
||||
"plugin_id": "plugin.cabin.ac.on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开空调", "开启空调", "空调打开"],
|
||||
"examples": ["把空调打开", "开空调", "启动空调"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_ac_off",
|
||||
"plugin_id": "plugin.cabin.ac.off",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭空调", "关掉空调", "空调关闭"],
|
||||
"examples": ["把空调关掉", "别吹空调了", "空调先关了"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_set_ac",
|
||||
"plugin_id": "plugin.cabin.ac_control",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": ["temperature"],
|
||||
"ask_templates": {
|
||||
"temperature": "请告诉我要设置多少度。"
|
||||
},
|
||||
"keywords": ["空调调到", "温度设成", "设成多少度"],
|
||||
"examples": ["把空调调到22度", "温度设成24度", "空调调到20度"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_fan_up",
|
||||
"plugin_id": "plugin.cabin.fan.up",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["调大风量", "风量大一点", "风量调高"],
|
||||
"examples": ["把风量调大一点", "空调风再大一点", "风量开大些"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_fan_down",
|
||||
"plugin_id": "plugin.cabin.fan.down",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["调小风量", "风量小一点", "风量调低"],
|
||||
"examples": ["把风量调小一点", "空调风太大了", "风量关小些"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_defog_front_on",
|
||||
"plugin_id": "plugin.cabin.defog.front_on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开前挡除雾", "前挡风除雾", "前窗除雾"],
|
||||
"examples": ["帮我打开前挡除雾", "前挡风玻璃起雾了", "开一下前挡除雾"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_defog_rear_on",
|
||||
"plugin_id": "plugin.cabin.defog.rear_on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开后挡除雾", "后挡风除雾", "后窗除雾"],
|
||||
"examples": ["帮我打开后挡除雾", "后挡风玻璃起雾了", "开一下后挡除雾"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_window_open",
|
||||
"plugin_id": "plugin.cabin.window.open",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开车窗", "开车窗", "车窗打开"],
|
||||
"examples": ["把车窗打开", "帮我开一下车窗", "打开一点车窗"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_window_close",
|
||||
"plugin_id": "plugin.cabin.window.close",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭车窗", "关车窗", "车窗关上"],
|
||||
"examples": ["把车窗关上", "帮我关一下车窗", "车窗全部关闭"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_sunroof_open",
|
||||
"plugin_id": "plugin.cabin.sunroof.open",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开天窗", "开天窗", "天窗打开"],
|
||||
"examples": ["把天窗打开", "帮我开一下天窗", "天窗打开一点"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_sunroof_close",
|
||||
"plugin_id": "plugin.cabin.sunroof.close",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭天窗", "关天窗", "天窗关上"],
|
||||
"examples": ["把天窗关上", "帮我关一下天窗", "关闭全景天窗"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_lock_doors",
|
||||
"plugin_id": "plugin.cabin.doors.lock",
|
||||
"domain": "cabin",
|
||||
"risk_level": "medium",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["锁车门", "锁门", "车门锁上"],
|
||||
"examples": ["帮我锁车", "把车门锁上", "全部车门上锁"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_unlock_doors",
|
||||
"plugin_id": "plugin.cabin.doors.unlock",
|
||||
"domain": "cabin",
|
||||
"risk_level": "medium",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["解锁车门", "开锁", "车门解锁"],
|
||||
"examples": ["帮我解锁车门", "把车门打开锁", "全部车门解锁"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_play_music",
|
||||
"plugin_id": "plugin.cabin.music_play",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["播放音乐", "来点音乐", "放首歌"],
|
||||
"examples": ["播放轻音乐", "来点歌", "帮我放首歌"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_pause_music",
|
||||
"plugin_id": "plugin.cabin.music.pause",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["暂停音乐", "暂停播放", "音乐暂停"],
|
||||
"examples": ["把音乐暂停", "先别放了", "暂停当前歌曲"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_next_track",
|
||||
"plugin_id": "plugin.cabin.music.next",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["下一首", "切下一首", "换首歌"],
|
||||
"examples": ["帮我切到下一首", "下一首歌", "换一首歌"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_previous_track",
|
||||
"plugin_id": "plugin.cabin.music.previous",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["上一首", "切上一首", "返回上一首"],
|
||||
"examples": ["帮我切到上一首", "上一首歌", "返回刚才那首歌"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_volume_up",
|
||||
"plugin_id": "plugin.cabin.volume.up",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["调大音量", "音量大一点", "音量调高"],
|
||||
"examples": ["把音量调大一点", "声音太小了", "音量开大些"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_volume_down",
|
||||
"plugin_id": "plugin.cabin.volume.down",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["调小音量", "音量小一点", "音量调低"],
|
||||
"examples": ["把音量调小一点", "声音太大了", "音量关小些"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_volume_mute",
|
||||
"plugin_id": "plugin.cabin.volume.mute",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["静音", "关闭声音", "音量静音"],
|
||||
"examples": ["把声音关掉", "先静音", "音响静音"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_lights_on",
|
||||
"plugin_id": "plugin.cabin.lights.on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开车灯", "开灯", "车灯打开"],
|
||||
"examples": ["把车灯打开", "帮我开一下灯", "打开大灯"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_lights_off",
|
||||
"plugin_id": "plugin.cabin.lights.off",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭车灯", "关灯", "车灯关闭"],
|
||||
"examples": ["把车灯关掉", "帮我关一下灯", "关闭大灯"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_seat_heat_on",
|
||||
"plugin_id": "plugin.cabin.seat_heat.on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开座椅加热", "开座椅加热", "座椅加热打开"],
|
||||
"examples": ["把座椅加热打开", "帮我开一下座椅加热", "打开主驾座椅加热"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_seat_heat_off",
|
||||
"plugin_id": "plugin.cabin.seat_heat.off",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭座椅加热", "关座椅加热", "座椅加热关闭"],
|
||||
"examples": ["把座椅加热关掉", "帮我关一下座椅加热", "关闭主驾座椅加热"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_mirror_fold",
|
||||
"plugin_id": "plugin.cabin.mirror.fold",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["折叠后视镜", "收起后视镜", "后视镜折叠"],
|
||||
"examples": ["把后视镜折叠起来", "帮我收起后视镜", "折叠两侧后视镜"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_mirror_unfold",
|
||||
"plugin_id": "plugin.cabin.mirror.unfold",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["展开后视镜", "打开后视镜", "后视镜展开"],
|
||||
"examples": ["把后视镜展开", "帮我打开后视镜", "展开两侧后视镜"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_wiper_on",
|
||||
"plugin_id": "plugin.cabin.wiper.on",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["打开雨刷", "开雨刷", "雨刷启动"],
|
||||
"examples": ["把雨刷打开", "帮我开一下雨刷", "启动雨刮器"]
|
||||
},
|
||||
{
|
||||
"intent_id": "cabin_wiper_off",
|
||||
"plugin_id": "plugin.cabin.wiper.off",
|
||||
"domain": "cabin",
|
||||
"risk_level": "low",
|
||||
"required_slots": [],
|
||||
"ask_templates": {},
|
||||
"keywords": ["关闭雨刷", "关雨刷", "雨刷停止"],
|
||||
"examples": ["把雨刷关掉", "帮我关一下雨刷", "停止雨刮器"]
|
||||
}
|
||||
]
|
||||
10
intelligent_cabin/app/data/joint_nlu_eval.jsonl
Normal file
10
intelligent_cabin/app/data/joint_nlu_eval.jsonl
Normal file
@@ -0,0 +1,10 @@
|
||||
{"text":"把空调设到21度","intent_id":"cabin_set_ac","slots":[{"slot_name":"temperature","value":"21度","start":6,"end":9}]}
|
||||
{"text":"导航到虹桥机场","intent_id":"cabin_nav_to","slots":[{"slot_name":"destination","value":"虹桥机场","start":3,"end":7}]}
|
||||
{"text":"来首稻香","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"稻香","start":2,"end":4}]}
|
||||
{"text":"放点摇滚","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"摇滚","start":2,"end":4}]}
|
||||
{"text":"查一下订单A700001现在什么状态","intent_id":"cs_query_order","slots":[{"slot_name":"order_id","value":"A700001","start":5,"end":12}]}
|
||||
{"text":"帮我查A900005物流进度","intent_id":"cs_query_logistics","slots":[{"slot_name":"order_id","value":"A900005","start":4,"end":11}]}
|
||||
{"text":"撤销订单A202501","intent_id":"cs_cancel_order","slots":[{"slot_name":"order_id","value":"A202501","start":4,"end":11}]}
|
||||
{"text":"把车窗打开","intent_id":"cabin_window_open","slots":[]}
|
||||
{"text":"把音乐暂停","intent_id":"cabin_pause_music","slots":[]}
|
||||
{"text":"帮我锁车","intent_id":"cabin_lock_doors","slots":[]}
|
||||
43
intelligent_cabin/app/data/joint_nlu_eval_independent.jsonl
Normal file
43
intelligent_cabin/app/data/joint_nlu_eval_independent.jsonl
Normal file
@@ -0,0 +1,43 @@
|
||||
{"text":"把空调调到22度","expected_intent_id":"cabin_set_ac","expected_slots":{"temperature":22},"category":"slot_temperature"}
|
||||
{"text":"空调给我调到20度","expected_intent_id":"cabin_set_ac","expected_slots":{"temperature":20},"category":"slot_temperature"}
|
||||
{"text":"车里温度设成24度","expected_intent_id":"cabin_set_ac","expected_slots":{"temperature":24},"category":"slot_temperature"}
|
||||
{"text":"把温度打到21度","expected_intent_id":"cabin_set_ac","expected_slots":{"temperature":21},"category":"slot_temperature"}
|
||||
{"text":"导航去公司停车场","expected_intent_id":"cabin_nav_to","expected_slots":{"destination":"公司停车场"},"category":"slot_destination"}
|
||||
{"text":"带我去浦东机场","expected_intent_id":"cabin_nav_to","expected_slots":{"destination":"浦东机场"},"category":"slot_destination"}
|
||||
{"text":"导航到南京东路","expected_intent_id":"cabin_nav_to","expected_slots":{"destination":"南京东路"},"category":"slot_destination"}
|
||||
{"text":"去虹桥机场","expected_intent_id":"cabin_nav_to","expected_slots":{"destination":"虹桥机场"},"category":"slot_destination"}
|
||||
{"text":"查一下订单A123456","expected_intent_id":"cs_query_order","expected_slots":{"order_id":"A123456"},"category":"slot_order"}
|
||||
{"text":"帮我看看订单A808001","expected_intent_id":"cs_query_order","expected_slots":{"order_id":"A808001"},"category":"slot_order"}
|
||||
{"text":"快递A998877到哪了","expected_intent_id":"cs_query_logistics","expected_slots":{"order_id":"A998877"},"category":"slot_order"}
|
||||
{"text":"取消订单A556677","expected_intent_id":"cs_cancel_order","expected_slots":{"order_id":"A556677"},"category":"slot_order"}
|
||||
{"text":"来一首青花瓷","expected_intent_id":"cabin_play_music","expected_slots":{"song":"青花瓷"},"category":"slot_music"}
|
||||
{"text":"播放夜的第七章","expected_intent_id":"cabin_play_music","expected_slots":{"song":"夜的第七章"},"category":"slot_music"}
|
||||
{"text":"来点爵士","expected_intent_id":"cabin_play_music","expected_slots":{"genre":"爵士"},"category":"slot_music"}
|
||||
{"text":"放点摇滚","expected_intent_id":"cabin_play_music","expected_slots":{"genre":"摇滚"},"category":"slot_music"}
|
||||
{"text":"给我播点民谣","expected_intent_id":"cabin_play_music","expected_slots":{"genre":"民谣"},"category":"slot_music"}
|
||||
{"text":"把车窗打开","expected_intent_id":"cabin_window_open","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把车窗关上","expected_intent_id":"cabin_window_close","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把天窗打开","expected_intent_id":"cabin_sunroof_open","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把天窗合上","expected_intent_id":"cabin_sunroof_close","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把空调打开","expected_intent_id":"cabin_ac_on","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把空调关掉","expected_intent_id":"cabin_ac_off","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把风量调大一点","expected_intent_id":"cabin_fan_up","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"风太大了,往小调一点","expected_intent_id":"cabin_fan_down","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把音乐暂停","expected_intent_id":"cabin_pause_music","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"帮我切到下一首","expected_intent_id":"cabin_next_track","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"切回上一首","expected_intent_id":"cabin_previous_track","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"先静音","expected_intent_id":"cabin_volume_mute","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把音量调大一点","expected_intent_id":"cabin_volume_up","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把音量调小一点","expected_intent_id":"cabin_volume_down","expected_slots":{},"category":"no_slot_control"}
|
||||
{"text":"把后视镜收起来","expected_intent_id":"cabin_mirror_fold","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"把镜子展开","expected_intent_id":"cabin_mirror_unfold","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"锁车门","expected_intent_id":"cabin_lock_doors","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"把车门解锁","expected_intent_id":"cabin_unlock_doors","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"路线别导了","expected_intent_id":"cabin_nav_cancel","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"音乐停一下","expected_intent_id":"cabin_pause_music","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"雨刮关掉","expected_intent_id":"cabin_wiper_off","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"把雨刮打开","expected_intent_id":"cabin_wiper_on","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"把左前窗降一点","expected_intent_id":"cabin_window_open","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"给我透个气","expected_intent_id":"cabin_window_open","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"风别太小","expected_intent_id":"cabin_fan_up","expected_slots":{},"category":"failure_replay"}
|
||||
{"text":"要是太慢就转人工","expected_intent_id":"cs_transfer_human","expected_slots":{},"category":"failure_replay"}
|
||||
12
intelligent_cabin/app/data/joint_nlu_multilabel_eval.jsonl
Normal file
12
intelligent_cabin/app/data/joint_nlu_multilabel_eval.jsonl
Normal file
@@ -0,0 +1,12 @@
|
||||
{"text":"打开车窗和空调","intent_ids":["cabin_window_open","cabin_ac_on"],"slots":[]}
|
||||
{"text":"导航去公司再放点轻音乐","intent_ids":["cabin_nav_to","cabin_play_music"],"slots":[{"slot_name":"destination","value":"公司","start":3,"end":5},{"slot_name":"genre","value":"轻音乐","start":8,"end":11}]}
|
||||
{"text":"把空调调到22度并播放夜曲","intent_ids":["cabin_set_ac","cabin_play_music"],"slots":[{"slot_name":"temperature","value":"22度","start":6,"end":9},{"slot_name":"song","value":"夜曲","start":12,"end":14}]}
|
||||
{"text":"开空调,关窗,再来点民谣","intent_ids":["cabin_ac_on","cabin_window_close","cabin_play_music"],"slots":[{"slot_name":"genre","value":"民谣","start":11,"end":13}]}
|
||||
{"text":"查一下订单A700001,如果还没发货就取消","intent_ids":["cs_query_order","cs_cancel_order"],"slots":[{"slot_name":"order_id","value":"A700001","start":5,"end":12}]}
|
||||
{"text":"查下A808001物流,没到就转人工","intent_ids":["cs_query_logistics","cs_transfer_human"],"slots":[{"slot_name":"order_id","value":"A808001","start":2,"end":9}]}
|
||||
{"text":"把风量调大一点,再把窗户打开","intent_ids":["cabin_fan_up","cabin_window_open"],"slots":[]}
|
||||
{"text":"透透气,再来一首黄昏","intent_ids":["cabin_window_open","cabin_play_music"],"slots":[{"slot_name":"song","value":"黄昏","start":9,"end":11}]}
|
||||
{"text":"导航到虹桥机场,空调也打开","intent_ids":["cabin_nav_to","cabin_ac_on"],"slots":[{"slot_name":"destination","value":"虹桥机场","start":3,"end":7}]}
|
||||
{"text":"后视镜收起来,再锁车门","intent_ids":["cabin_mirror_fold","cabin_lock_doors"],"slots":[]}
|
||||
{"text":"雨刮关掉并打开车窗","intent_ids":["cabin_wiper_off","cabin_window_open"],"slots":[]}
|
||||
{"text":"来点音乐,再把温度设成21度","intent_ids":["cabin_play_music","cabin_set_ac"],"slots":[{"slot_name":"temperature","value":"21度","start":12,"end":15}]}
|
||||
27
intelligent_cabin/app/data/joint_nlu_seed.jsonl
Normal file
27
intelligent_cabin/app/data/joint_nlu_seed.jsonl
Normal file
@@ -0,0 +1,27 @@
|
||||
{"text":"把空调调到22度","intent_id":"cabin_set_ac","slots":[{"slot_name":"temperature","value":"22度","start":6,"end":9}]}
|
||||
{"text":"空调给我调到20度","intent_id":"cabin_set_ac","slots":[{"slot_name":"temperature","value":"20度","start":7,"end":10}]}
|
||||
{"text":"车里温度设成24度","intent_id":"cabin_set_ac","slots":[{"slot_name":"temperature","value":"24度","start":7,"end":10}]}
|
||||
{"text":"导航去公司停车场","intent_id":"cabin_nav_to","slots":[{"slot_name":"destination","value":"公司停车场","start":3,"end":8}]}
|
||||
{"text":"带我去浦东机场","intent_id":"cabin_nav_to","slots":[{"slot_name":"destination","value":"浦东机场","start":3,"end":7}]}
|
||||
{"text":"导航到南京东路","intent_id":"cabin_nav_to","slots":[{"slot_name":"destination","value":"南京东路","start":3,"end":7}]}
|
||||
{"text":"播放夜曲","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"夜曲","start":2,"end":4}]}
|
||||
{"text":"来一首青花瓷","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"青花瓷","start":3,"end":6}]}
|
||||
{"text":"放一首晴天","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"晴天","start":3,"end":5}]}
|
||||
{"text":"来一首告白气球","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"告白气球","start":3,"end":7}]}
|
||||
{"text":"播放稻香","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"稻香","start":2,"end":4}]}
|
||||
{"text":"帮我播首夜的第七章","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"夜的第七章","start":4,"end":9}]}
|
||||
{"text":"给我来一首晴天","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"晴天","start":5,"end":7}]}
|
||||
{"text":"车里放首稻香","intent_id":"cabin_play_music","slots":[{"slot_name":"song","value":"稻香","start":4,"end":6}]}
|
||||
{"text":"播放轻音乐","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"轻音乐","start":2,"end":5}]}
|
||||
{"text":"来点爵士","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"爵士","start":2,"end":4}]}
|
||||
{"text":"给我播点民谣","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"民谣","start":4,"end":6}]}
|
||||
{"text":"随机放点流行","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"流行","start":4,"end":6}]}
|
||||
{"text":"来点古典音乐","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"古典","start":2,"end":4}]}
|
||||
{"text":"车里放点摇滚","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"摇滚","start":4,"end":6}]}
|
||||
{"text":"给我来点儿歌","intent_id":"cabin_play_music","slots":[{"slot_name":"genre","value":"儿歌","start":4,"end":6}]}
|
||||
{"text":"查一下订单A123456","intent_id":"cs_query_order","slots":[{"slot_name":"order_id","value":"A123456","start":5,"end":12}]}
|
||||
{"text":"帮我看看订单A808001","intent_id":"cs_query_order","slots":[{"slot_name":"order_id","value":"A808001","start":6,"end":13}]}
|
||||
{"text":"快递A998877到哪了","intent_id":"cs_query_logistics","slots":[{"slot_name":"order_id","value":"A998877","start":2,"end":9}]}
|
||||
{"text":"帮我查A202501物流","intent_id":"cs_query_logistics","slots":[{"slot_name":"order_id","value":"A202501","start":4,"end":11}]}
|
||||
{"text":"取消订单A556677","intent_id":"cs_cancel_order","slots":[{"slot_name":"order_id","value":"A556677","start":4,"end":11}]}
|
||||
{"text":"把A112233这个订单取消掉","intent_id":"cs_cancel_order","slots":[{"slot_name":"order_id","value":"A112233","start":1,"end":8}]}
|
||||
151
intelligent_cabin/app/main.py
Normal file
151
intelligent_cabin/app/main.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.bootstrap import build_agent_service_with_runtime, build_intent_registry
|
||||
from app.schemas.chat import ChatRequest, ChatResponse, FillSlotsRequest
|
||||
from app.schemas.demo import DemoRuntimeConfig, DemoRuntimeUpdateRequest
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
|
||||
# CORS:允许 Canvas 前端跨域调用
|
||||
# 生产环境请将 allow_origins 替换为实际前端域名
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
demo_html_path = Path(__file__).parent / "static" / "demo.html"
|
||||
chat_stream_executor = ThreadPoolExecutor(max_workers=8)
|
||||
|
||||
runtime_config = DemoRuntimeConfig(
|
||||
matcher_pipeline=settings.matcher_pipeline,
|
||||
classifier_backend=settings.classifier_backend,
|
||||
session_backend=settings.session_backend,
|
||||
slot_extractor_backend=settings.slot_extractor_backend,
|
||||
planner_backend=settings.planner_backend,
|
||||
planner_model_name=settings.planner_model_name,
|
||||
)
|
||||
agent_service = build_agent_service_with_runtime(
|
||||
matcher_pipeline=runtime_config.matcher_pipeline,
|
||||
classifier_backend=runtime_config.classifier_backend,
|
||||
session_backend=runtime_config.session_backend,
|
||||
)
|
||||
intent_registry = build_intent_registry()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok", "env": settings.app_env}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@app.get("/demo")
|
||||
def demo() -> FileResponse:
|
||||
return FileResponse(demo_html_path)
|
||||
|
||||
|
||||
@app.post("/api/v1/agent/chat", response_model=ChatResponse)
|
||||
def chat(request: ChatRequest) -> ChatResponse:
|
||||
return agent_service.handle_chat(request)
|
||||
|
||||
|
||||
@app.post("/api/v1/agent/chat-stream")
|
||||
def chat_stream(request: ChatRequest) -> StreamingResponse:
|
||||
def stream():
|
||||
future = chat_stream_executor.submit(agent_service.handle_chat, request)
|
||||
try:
|
||||
response = future.result(timeout=1.0)
|
||||
except TimeoutError:
|
||||
ack = {
|
||||
"type": "ack",
|
||||
"reply_text": "好的,正在处理中,请稍等一下。",
|
||||
"status": "processing",
|
||||
"trace_id": uuid4().hex,
|
||||
}
|
||||
yield json.dumps(ack, ensure_ascii=False) + "\n"
|
||||
try:
|
||||
response = future.result()
|
||||
except Exception as exc: # pragma: no cover - stream error fallback
|
||||
payload = {
|
||||
"type": "error",
|
||||
"message": str(exc),
|
||||
}
|
||||
yield json.dumps(payload, ensure_ascii=False) + "\n"
|
||||
return
|
||||
except Exception as exc: # pragma: no cover - stream error fallback
|
||||
payload = {
|
||||
"type": "error",
|
||||
"message": str(exc),
|
||||
}
|
||||
yield json.dumps(payload, ensure_ascii=False) + "\n"
|
||||
return
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"type": "final",
|
||||
"data": response.model_dump(mode="json"),
|
||||
}
|
||||
yield json.dumps(payload, ensure_ascii=False) + "\n"
|
||||
except Exception as exc: # pragma: no cover - stream error fallback
|
||||
payload = {
|
||||
"type": "error",
|
||||
"message": str(exc),
|
||||
}
|
||||
yield json.dumps(payload, ensure_ascii=False) + "\n"
|
||||
|
||||
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
||||
|
||||
|
||||
@app.post("/api/v1/agent/fill-slots", response_model=ChatResponse)
|
||||
def fill_slots(request: FillSlotsRequest) -> ChatResponse:
|
||||
return agent_service.handle_fill_slots(request)
|
||||
|
||||
|
||||
@app.get("/api/v1/intents")
|
||||
def list_intents() -> list[dict[str, object]]:
|
||||
return [intent.model_dump() for intent in intent_registry.list()]
|
||||
|
||||
|
||||
@app.get("/api/v1/demo/runtime", response_model=DemoRuntimeConfig)
|
||||
def get_demo_runtime() -> DemoRuntimeConfig:
|
||||
return runtime_config
|
||||
|
||||
|
||||
@app.post("/api/v1/demo/runtime", response_model=DemoRuntimeConfig)
|
||||
def update_demo_runtime(request: DemoRuntimeUpdateRequest) -> DemoRuntimeConfig:
|
||||
global agent_service, runtime_config
|
||||
|
||||
matcher_stages = [stage.strip() for stage in request.matcher_pipeline.split(",") if stage.strip()]
|
||||
if matcher_stages != ["classifier"]:
|
||||
raise HTTPException(status_code=400, detail="Only classifier matcher pipeline is supported in bert-first mode")
|
||||
|
||||
try:
|
||||
next_service = build_agent_service_with_runtime(
|
||||
matcher_pipeline=request.matcher_pipeline,
|
||||
classifier_backend=request.classifier_backend,
|
||||
session_backend=request.session_backend,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
agent_service = next_service
|
||||
runtime_config = DemoRuntimeConfig(
|
||||
matcher_pipeline=request.matcher_pipeline,
|
||||
classifier_backend=request.classifier_backend,
|
||||
session_backend=request.session_backend,
|
||||
slot_extractor_backend=settings.slot_extractor_backend,
|
||||
planner_backend=settings.planner_backend,
|
||||
planner_model_name=settings.planner_model_name,
|
||||
)
|
||||
return runtime_config
|
||||
1
intelligent_cabin/app/plugins/__init__.py
Normal file
1
intelligent_cabin/app/plugins/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Plugin adapters for the agent service."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/plugins/__pycache__/mock.cpython-313.pyc
Normal file
Binary file not shown.
27
intelligent_cabin/app/plugins/base.py
Normal file
27
intelligent_cabin/app/plugins/base.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
PluginHandler = Callable[[dict[str, Any]], dict[str, Any]]
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._handlers: dict[str, PluginHandler] = {}
|
||||
|
||||
def register(self, plugin_id: str, handler: PluginHandler) -> None:
|
||||
self._handlers[plugin_id] = handler
|
||||
|
||||
def execute(self, plugin_id: str, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
handler = self._handlers.get(plugin_id)
|
||||
if handler is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"插件 {plugin_id} 未注册。",
|
||||
"data": {"plugin_id": plugin_id, "slots": slots},
|
||||
}
|
||||
return handler(slots)
|
||||
|
||||
def registered_plugins(self) -> list[str]:
|
||||
return sorted(self._handlers.keys())
|
||||
216
intelligent_cabin/app/plugins/mock.py
Normal file
216
intelligent_cabin/app/plugins/mock.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.plugins.base import PluginRegistry
|
||||
|
||||
|
||||
class MockPluginExecutor:
|
||||
def register(self, registry: PluginRegistry) -> PluginRegistry:
|
||||
registry.register("plugin.order.query", self._query_order)
|
||||
registry.register("plugin.logistics.query", self._query_logistics)
|
||||
registry.register("plugin.order.cancel", self._cancel_order)
|
||||
registry.register("plugin.service.transfer_human", self._transfer_human)
|
||||
registry.register("plugin.cabin.navigation.cancel", self._navigate_cancel)
|
||||
registry.register("plugin.cabin.navigation", self._navigate)
|
||||
registry.register("plugin.cabin.ac.on", self._ac_on)
|
||||
registry.register("plugin.cabin.ac.off", self._ac_off)
|
||||
registry.register("plugin.cabin.ac_control", self._set_ac)
|
||||
registry.register("plugin.cabin.fan.up", self._fan_up)
|
||||
registry.register("plugin.cabin.fan.down", self._fan_down)
|
||||
registry.register("plugin.cabin.defog.front_on", self._defog_front_on)
|
||||
registry.register("plugin.cabin.defog.rear_on", self._defog_rear_on)
|
||||
registry.register("plugin.cabin.window.open", self._window_open)
|
||||
registry.register("plugin.cabin.window.close", self._window_close)
|
||||
registry.register("plugin.cabin.sunroof.open", self._sunroof_open)
|
||||
registry.register("plugin.cabin.sunroof.close", self._sunroof_close)
|
||||
registry.register("plugin.cabin.doors.lock", self._lock_doors)
|
||||
registry.register("plugin.cabin.doors.unlock", self._unlock_doors)
|
||||
registry.register("plugin.cabin.music_play", self._play_music)
|
||||
registry.register("plugin.cabin.music.pause", self._pause_music)
|
||||
registry.register("plugin.cabin.music.next", self._next_track)
|
||||
registry.register("plugin.cabin.music.previous", self._previous_track)
|
||||
registry.register("plugin.cabin.volume.up", self._volume_up)
|
||||
registry.register("plugin.cabin.volume.down", self._volume_down)
|
||||
registry.register("plugin.cabin.volume.mute", self._volume_mute)
|
||||
registry.register("plugin.cabin.lights.on", self._lights_on)
|
||||
registry.register("plugin.cabin.lights.off", self._lights_off)
|
||||
registry.register("plugin.cabin.seat_heat.on", self._seat_heat_on)
|
||||
registry.register("plugin.cabin.seat_heat.off", self._seat_heat_off)
|
||||
registry.register("plugin.cabin.mirror.fold", self._mirror_fold)
|
||||
registry.register("plugin.cabin.mirror.unfold", self._mirror_unfold)
|
||||
registry.register("plugin.cabin.wiper.on", self._wiper_on)
|
||||
registry.register("plugin.cabin.wiper.off", self._wiper_off)
|
||||
return registry
|
||||
|
||||
def _query_order(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"订单 {slots['order_id']} 当前待发货。",
|
||||
"data": {"order_status": "pending_shipment"},
|
||||
}
|
||||
|
||||
def _query_logistics(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"订单 {slots['order_id']} 最新物流状态为运输中。",
|
||||
"data": {"logistics_status": "shipping"},
|
||||
}
|
||||
|
||||
def _cancel_order(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"订单 {slots['order_id']} 已取消。",
|
||||
"data": {"cancel_status": "cancelled"},
|
||||
}
|
||||
|
||||
def _transfer_human(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "已为你转接人工客服,请稍候。",
|
||||
"data": {"queue_no": "A12", "reason": slots.get("reason", "用户请求")},
|
||||
}
|
||||
|
||||
def _navigate(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已开始导航到 {slots['destination']}。",
|
||||
"data": {"route_id": "route_001", "destination": slots["destination"]},
|
||||
}
|
||||
|
||||
def _navigate_cancel(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已结束当前导航。", {"navigation": "stopped"})
|
||||
|
||||
def _ac_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开空调。", {"power": "on"})
|
||||
|
||||
def _ac_off(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭空调。", {"power": "off"})
|
||||
|
||||
def _set_ac(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已将空调调到 {slots['temperature']} 度。",
|
||||
"data": {"temperature": slots["temperature"]},
|
||||
}
|
||||
|
||||
def _fan_up(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已调大风量。", {"fan": "up"})
|
||||
|
||||
def _fan_down(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已调小风量。", {"fan": "down"})
|
||||
|
||||
def _defog_front_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开前挡除雾。", {"defog": "front_on"})
|
||||
|
||||
def _defog_rear_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开后挡除雾。", {"defog": "rear_on"})
|
||||
|
||||
def _window_open(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开车窗。", {"window": "open"})
|
||||
|
||||
def _window_close(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭车窗。", {"window": "close"})
|
||||
|
||||
def _sunroof_open(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开天窗。", {"sunroof": "open"})
|
||||
|
||||
def _sunroof_close(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭天窗。", {"sunroof": "close"})
|
||||
|
||||
def _lock_doors(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已锁定车门。", {"doors": "locked"})
|
||||
|
||||
def _unlock_doors(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已解锁车门。", {"doors": "unlocked"})
|
||||
|
||||
def _play_music(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
target = slots.get("song") or slots.get("genre") or "默认歌单"
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"正在播放 {target}。",
|
||||
"data": {"play_target": target},
|
||||
}
|
||||
|
||||
def _pause_music(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已暂停播放。", {"music": "paused"})
|
||||
|
||||
def _next_track(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已切到下一首。", {"music": "next"})
|
||||
|
||||
def _previous_track(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已切到上一首。", {"music": "previous"})
|
||||
|
||||
def _volume_up(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已调大音量。", {"volume": "up"})
|
||||
|
||||
def _volume_down(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已调小音量。", {"volume": "down"})
|
||||
|
||||
def _volume_mute(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已静音。", {"volume": "mute"})
|
||||
|
||||
def _lights_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开车灯。", {"lights": "on"})
|
||||
|
||||
def _lights_off(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭车灯。", {"lights": "off"})
|
||||
|
||||
def _seat_heat_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开座椅加热。", {"seat_heat": "on"})
|
||||
|
||||
def _seat_heat_off(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭座椅加热。", {"seat_heat": "off"})
|
||||
|
||||
def _mirror_fold(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已折叠后视镜。", {"mirror": "folded"})
|
||||
|
||||
def _mirror_unfold(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已展开后视镜。", {"mirror": "unfolded"})
|
||||
|
||||
def _wiper_on(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已打开雨刷。", {"wiper": "on"})
|
||||
|
||||
def _wiper_off(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = slots
|
||||
return self._simple_action("好的,已关闭雨刷。", {"wiper": "off"})
|
||||
|
||||
def _fallback(self, slots: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "已接收请求,当前使用 mock 插件返回成功结果。",
|
||||
"data": slots,
|
||||
}
|
||||
|
||||
def _simple_action(self, message: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"data": data,
|
||||
}
|
||||
1
intelligent_cabin/app/schemas/__init__.py
Normal file
1
intelligent_cabin/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Pydantic schemas for the agent service."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/chat.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/debug.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/demo.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-311.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-311.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-312.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-312.pyc
Normal file
Binary file not shown.
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-313.pyc
Normal file
BIN
intelligent_cabin/app/schemas/__pycache__/intent.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
46
intelligent_cabin/app/schemas/chat.py
Normal file
46
intelligent_cabin/app/schemas/chat.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.debug import RoutingDebug
|
||||
from app.schemas.workflow import Workflow
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
session_id: str
|
||||
user_id: str
|
||||
channel: str = "app"
|
||||
input_text: str
|
||||
input_type: Literal["text", "voice"] = "text"
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FillSlotsRequest(BaseModel):
|
||||
session_id: str
|
||||
user_id: str
|
||||
input_text: str
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
session_id: str
|
||||
reply_type: Literal["text", "ask_slot", "ask_confirmation", "workflow_result", "fallback", "clarify", "reject"] = "text"
|
||||
reply_text: str
|
||||
intent: str | None = None
|
||||
domain: str | None = None
|
||||
decision: str | None = None
|
||||
decision_reason: str | None = None
|
||||
status: str
|
||||
pending_slots: list[str] = Field(default_factory=list)
|
||||
filled_slots: dict[str, Any] = Field(default_factory=dict)
|
||||
workflow: Workflow | None = None
|
||||
routing_debug: RoutingDebug | None = None
|
||||
first_response_latency_ms: float | None = None
|
||||
total_latency_ms: float | None = None
|
||||
processing_breakdown: dict[str, float] = Field(default_factory=dict)
|
||||
trace_id: str
|
||||
# ── 知识库查询结果(LLM function call 命中时填充)──────────────────────────
|
||||
knowledge_doc_id: str | None = None # 知识文档 ID(MD 文件名)
|
||||
knowledge_doc_title: str | None = None # 知识文档标题
|
||||
knowledge_content: str | None = None # 完整 MD 正文,供前端渲染知识卡片
|
||||
135
intelligent_cabin/app/schemas/configuration.py
Normal file
135
intelligent_cabin/app/schemas/configuration.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.intent import IntentDefinition
|
||||
|
||||
|
||||
class ActionDefinition(BaseModel):
|
||||
action_id: str
|
||||
plugin_id: str
|
||||
risk_level: Literal["low", "medium", "high"] = "low"
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DomainIntentDefinition(BaseModel):
|
||||
intent_id: str
|
||||
domain: str
|
||||
action_id: str | None = None
|
||||
plugin_id: str | None = None
|
||||
risk_level: Literal["low", "medium", "high"] | None = None
|
||||
label: str | None = None
|
||||
required_slots: list[str] = Field(default_factory=list)
|
||||
ask_templates: dict[str, str] = Field(default_factory=dict)
|
||||
keywords: list[str] = Field(default_factory=list)
|
||||
examples: list[str] = Field(default_factory=list)
|
||||
|
||||
def to_intent_definition(self, actions: dict[str, ActionDefinition]) -> IntentDefinition:
|
||||
plugin_id = self.plugin_id
|
||||
risk_level = self.risk_level
|
||||
if self.action_id:
|
||||
action = actions[self.action_id]
|
||||
plugin_id = action.plugin_id
|
||||
if risk_level is None:
|
||||
risk_level = action.risk_level
|
||||
if not plugin_id:
|
||||
raise ValueError(f"intent {self.intent_id} is missing plugin_id/action_id mapping")
|
||||
return IntentDefinition(
|
||||
intent_id=self.intent_id,
|
||||
plugin_id=plugin_id,
|
||||
domain=self.domain,
|
||||
risk_level=risk_level or "low",
|
||||
required_slots=self.required_slots,
|
||||
ask_templates=self.ask_templates,
|
||||
keywords=self.keywords,
|
||||
examples=self.examples,
|
||||
)
|
||||
|
||||
|
||||
class DomainConfig(BaseModel):
|
||||
intents: list[DomainIntentDefinition] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ActionsConfig(BaseModel):
|
||||
actions: list[ActionDefinition] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ResponsesConfig(BaseModel):
|
||||
templates: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FormDefinition(BaseModel):
|
||||
intent_id: str
|
||||
required_slots: list[str] = Field(default_factory=list)
|
||||
ask_templates: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FormsConfig(BaseModel):
|
||||
forms: list[FormDefinition] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ConfirmationRuleConfig(BaseModel):
|
||||
positive_tokens: list[str] = Field(default_factory=list)
|
||||
negative_tokens: list[str] = Field(default_factory=list)
|
||||
required_intents: list[str] = Field(default_factory=list)
|
||||
required_risk_levels: list[Literal["low", "medium", "high"]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class StopRuleConfig(BaseModel):
|
||||
phrases: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DialogRulesConfig(BaseModel):
|
||||
stop: StopRuleConfig = Field(default_factory=StopRuleConfig)
|
||||
confirmation: ConfirmationRuleConfig = Field(default_factory=ConfirmationRuleConfig)
|
||||
|
||||
|
||||
class DialogActPatternDefinition(BaseModel):
|
||||
act_id: str
|
||||
phrases: list[str] = Field(default_factory=list)
|
||||
# 正则列表:文本匹配任意一个则命中该 act(用于数字类 inform)
|
||||
numeric_patterns: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DialogActsConfig(BaseModel):
|
||||
acts: list[DialogActPatternDefinition] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ── Context Rewrite Config ────────────────────────────────────────────────────
|
||||
|
||||
class ParamContextDefinition(BaseModel):
|
||||
"""一类可相对调节参数的上下文改写规则。"""
|
||||
intent_ids: list[str]
|
||||
slot_name: str
|
||||
unit: str = ""
|
||||
step: int | float = 1
|
||||
min_value: int | float = 0
|
||||
max_value: int | float = 9999
|
||||
default_value: int | float = 0
|
||||
up_phrases: list[str] = Field(default_factory=list)
|
||||
down_phrases: list[str] = Field(default_factory=list)
|
||||
rewrite_template: str = "{value}"
|
||||
|
||||
|
||||
class ContextRewriteConfig(BaseModel):
|
||||
param_contexts: list[ParamContextDefinition] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WorkflowTemplateStepOverride(BaseModel):
|
||||
depends_on: list[int] = Field(default_factory=list)
|
||||
condition: dict[str, Any] = Field(default_factory=dict)
|
||||
requires_confirmation: bool = False
|
||||
|
||||
|
||||
class WorkflowTemplateDefinition(BaseModel):
|
||||
template_id: str
|
||||
workflow_type: Literal["sequence", "conditional", "parallel"] = "sequence"
|
||||
intent_sequence: list[str] = Field(default_factory=list)
|
||||
step_overrides: list[WorkflowTemplateStepOverride] = Field(default_factory=list)
|
||||
trigger_keywords: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WorkflowTemplatesConfig(BaseModel):
|
||||
templates: list[WorkflowTemplateDefinition] = Field(default_factory=list)
|
||||
42
intelligent_cabin/app/schemas/debug.py
Normal file
42
intelligent_cabin/app/schemas/debug.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IntentCandidate(BaseModel):
|
||||
intent_id: str
|
||||
score: float = 0.0
|
||||
reason: str | None = None
|
||||
model_name: str | None = None
|
||||
raw_label: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class MatcherStageDebug(BaseModel):
|
||||
stage: str
|
||||
accepted: bool = False
|
||||
selected_intent: str | None = None
|
||||
score: float = 0.0
|
||||
elapsed_ms: float | None = None
|
||||
reason: str | None = None
|
||||
model_name: str | None = None
|
||||
backend: str | None = None
|
||||
fallback_used: bool = False
|
||||
raw_label: str | None = None
|
||||
error_message: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
candidates: list[IntentCandidate] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RoutingDebug(BaseModel):
|
||||
selected_intent: str | None = None
|
||||
matched_stage: str | None = None
|
||||
decision: str = "reject"
|
||||
decision_reason: str | None = None
|
||||
confidence_grade: str | None = None
|
||||
total_match_latency_ms: float | None = None
|
||||
unknown_detected: bool = False
|
||||
extracted_slots: dict[str, Any] = Field(default_factory=dict)
|
||||
stages: list[MatcherStageDebug] = Field(default_factory=list)
|
||||
20
intelligent_cabin/app/schemas/demo.py
Normal file
20
intelligent_cabin/app/schemas/demo.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DemoRuntimeConfig(BaseModel):
|
||||
matcher_pipeline: Literal["classifier"]
|
||||
classifier_backend: Literal["mock", "bert", "remote", "joint_bert"]
|
||||
session_backend: Literal["memory", "redis"]
|
||||
slot_extractor_backend: str
|
||||
planner_backend: str
|
||||
planner_model_name: str
|
||||
|
||||
|
||||
class DemoRuntimeUpdateRequest(BaseModel):
|
||||
matcher_pipeline: Literal["classifier"] = Field(default="classifier")
|
||||
classifier_backend: Literal["mock", "bert", "remote", "joint_bert"]
|
||||
session_backend: Literal["memory", "redis"]
|
||||
16
intelligent_cabin/app/schemas/intent.py
Normal file
16
intelligent_cabin/app/schemas/intent.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IntentDefinition(BaseModel):
|
||||
intent_id: str
|
||||
plugin_id: str
|
||||
domain: str
|
||||
risk_level: Literal["low", "medium", "high"] = "low"
|
||||
required_slots: list[str] = Field(default_factory=list)
|
||||
ask_templates: dict[str, str] = Field(default_factory=dict)
|
||||
keywords: list[str] = Field(default_factory=list)
|
||||
examples: list[str] = Field(default_factory=list)
|
||||
38
intelligent_cabin/app/schemas/workflow.py
Normal file
38
intelligent_cabin/app/schemas/workflow.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class WorkflowStep(BaseModel):
|
||||
step: int
|
||||
step_id: str
|
||||
intent_id: str
|
||||
plugin_id: str
|
||||
action: str
|
||||
status: Literal["pending", "running", "completed", "failed", "skipped", "waiting_confirmation"] = "pending"
|
||||
depends_on: list[str] = Field(default_factory=list)
|
||||
slots: dict[str, Any] = Field(default_factory=dict)
|
||||
condition: dict[str, Any] = Field(default_factory=dict)
|
||||
requires_confirmation: bool = False
|
||||
timeout_ms: int = 1500
|
||||
|
||||
|
||||
class MissingSlot(BaseModel):
|
||||
slot_name: str
|
||||
ask_template: str
|
||||
priority: int = 1
|
||||
|
||||
|
||||
class Workflow(BaseModel):
|
||||
workflow_id: str
|
||||
workflow_type: Literal["single", "sequence", "conditional", "parallel"] = "single"
|
||||
domain: str
|
||||
intent_id: str
|
||||
status: Literal["ready", "waiting_slot", "waiting_confirmation", "running", "completed", "failed"] = "ready"
|
||||
risk_level: Literal["low", "medium", "high"] = "low"
|
||||
slots: dict[str, Any] = Field(default_factory=dict)
|
||||
missing_slots: list[MissingSlot] = Field(default_factory=list)
|
||||
steps: list[WorkflowStep] = Field(default_factory=list)
|
||||
meta: dict[str, Any] = Field(default_factory=dict)
|
||||
1
intelligent_cabin/app/services/__init__.py
Normal file
1
intelligent_cabin/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Application services for orchestration and session management."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user