Files
wecom_it_smart_desk/docs/01-产品文档/04-坐席工作台/坐席端AI辅助消息框-PRD.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

621 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 坐席端 AI 辅助消息框 — 产品需求文档 (PRD)
> **版本**: v1.0
> **日期**: 2026-07-11
> **作者**: 宋献
> **状态**: 需求已确认,待开发
---
## 1. 背景与目标
### 1.1 现状分析
坐席端已具备较完善的 AI 辅助基础设施:
| 已有功能 | 模式 | 状态 |
|----------|------|------|
| AI 草稿生成 | 点击右栏按钮 → 生成完整回复 → 采纳 | 已对接 Dify API |
| AI 草稿气泡 | 消息流内联气泡 → 采纳/编辑/忽略 | 已对接后端 |
| 快速回复模板 | 三层渐进导航 + 变量替换 | 本地静态数据 |
| AI 推荐回复(内联) | 3 张卡片 + Ctrl+1/2/3 | Mock 数据,未对接 API |
| 会话摘要 + 智能标注 | 结单时自动生成 | 已对接 Dify API |
**核心差距**:现有功能均为"生成完整草稿 → 坐席采纳"模式,缺失"输入过程中实时辅助 + 发送前精修"模式。
### 1.2 行业标杆
- **Freshdesk Freddy AI**:语气增强(更正式/更随意/专业/友好/随性),选中文字后一键改写
- **智齿科技**:AI 扩写润色 + 风格设置(友好口吻/专业口吻)
- **Yellow.ai**Tone Changer + Sentiment Detection + AI Copilot
### 1.3 目标
在现有 ReplyBox 输入框中新增 4 项 AI 辅助功能,补齐"输入过程中实时辅助 + 发送前精修"能力:
1. **实时自动补齐** — 输入停顿时显示灰色幽灵文字,Tab 键接受
2. **语气调整** — 选中文字后选择专业/友好/简洁风格,一键改写
3. **文字润色** — 扩写/压缩/纠错,弹出精修面板确认后替换
4. **智能改写** — 基于对话上下文 + 知识库,生成 3 个备选版本
### 1.4 设计原则
- **共存不替代**:新增功能与现有草稿生成互补,不替代
- **渐进增强**:坐席可选择使用,不强制改变现有工作流
- **低延迟感知**:补齐建议 < 1.5s,语气/润色/改写 < 3s
- **可降级**:AI 服务不可用时不影响正常输入和发送
---
## 2. 功能详细需求
### 2.1 实时自动补齐
#### 2.1.1 交互流程
```
坐席输入文字 → 停顿 > 0.8s → 调用 AI 补齐 API → 光标位置显示灰色幽灵文字
├─ Tab 键 → 接受补齐,文字变为正常颜色
├─ 继续输入 → 忽略补齐,重新触发
└─ Esc 键 → 清除补齐建议
```
#### 2.1.2 功能规格
| 项目 | 规格 |
|------|------|
| 触发条件 | 输入停顿 > 800ms,且输入框内容 > 5 字符 |
| 请求防抖 | debounce 800ms,输入中取消上一个请求 |
| 显示方式 | 光标后方灰色斜体文字(ghost text) |
| 补齐长度 | 1-2 个短句(不超过 80 字符) |
| 接受方式 | Tab 键接受全部;Shift+Tab 接受一个词 |
| 忽略方式 | 继续输入自动忽略;Esc 清除 |
| 上下文 | 携带最近 5 条对话消息 + 当前输入内容 |
| 并发控制 | 同一时刻只保留最新的补齐请求 |
| 加载状态 | 幽灵文字位置显示 3 个点动画(typing indicator |
| 超时处理 | 1.5s 未返回则取消,不显示 |
#### 2.1.3 前端实现要点
-`<textarea>` 上方覆盖一层 `<div>` 渲染幽灵文字,通过计算光标位置定位
- 或使用 `contenteditable` div 替换 textarea(更灵活但改动较大)
- **推荐方案**:保持 `<textarea>` 不变,在下方独立区域显示补齐建议条(简化实现,降低风险)
> **决策点**:纯内联幽灵文字需要精确计算光标坐标,实现复杂度高。若需降低首版复杂度,可采用"输入框下方建议条"作为过渡方案。当前需求确认为内联幽灵文字。
#### 2.1.4 后端 API
```
POST /api/conversations/{conversation_id}/wingman/autocomplete
```
**请求体**
```json
{
"current_text": "张工您好,您反馈的VPN连接问题我已经",
"cursor_position": 22,
"max_length": 80
}
```
**响应体**
```json
{
"completion": "正在查看相关工单记录,请稍候。",
"confidence": 0.85
}
```
**Dify System Prompt**
```
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
要求:
1. 补齐内容自然衔接当前文字,不要重复已有内容
2. 长度控制在1-2个短句,不超过80字
3. 语气专业、简洁,符合IT服务规范
4. 只返回补齐的文字,不要加引号或其他标记
```
---
### 2.2 语气调整
#### 2.2.1 交互流程
```
坐席选中已输入文字 → 点击工具栏"语气"按钮 → 弹出语气选择菜单
├─ 专业 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
├─ 友好 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
└─ 简洁 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
```
#### 2.2.2 功能规格
| 项目 | 规格 |
|------|------|
| 触发条件 | 选中输入框中的文字(≥ 5 字符),点击工具栏"语气"按钮 |
| 语气选项 | 专业 / 友好 / 简洁(3 种) |
| 显示方式 | 弹出浮层菜单,3 个选项纵向排列 |
| 改写显示 | 浮层中显示"原文 → 改写文"对比 |
| 确认方式 | 点击"替换"按钮,用改写文替换选中文字 |
| 取消方式 | 点击外部区域或 Esc 关闭浮层 |
| 上下文 | 携带对话上下文 + 选中文字 + 当前完整输入内容 |
| 超时处理 | 3s 未返回显示重试按钮 |
#### 2.2.3 语气定义
| 语气 | 定义 | 示例 |
|------|------|------|
| **专业** | 使用准确的技术术语,结构化表达,去除口语化内容 | "经排查,您的VPN连接异常是由本地网络DNS解析超时引起,建议执行以下操作:" |
| **友好** | 适当增加问候和关心语,语气更亲和 | "张工您好~VPN连不上确实挺急的,我帮您看了下,可能是DNS解析有点慢,咱们试试这样操作:" |
| **简洁** | 去除冗余,直奔主题,控制字数 | "VPN异常原因:DNS超时。操作:1. 刷新DNS 2. 重连VPN" |
#### 2.2.4 后端 API
```
POST /api/conversations/{conversation_id}/wingman/tone-adjust
```
**请求体**
```json
{
"selected_text": "VPN连接问题我已经在看了,你别急啊,我查一下",
"full_text": "张工您好,VPN连接问题我已经在看了,你别急啊,我查一下",
"tone": "professional",
"conversation_context": true
}
```
**响应体**
```json
{
"rewritten_text": "经排查,您的VPN连接异常由本地网络DNS解析超时引起,建议执行以下操作:",
"tone": "professional",
"changes_summary": "去除口语化表达,增加技术术语,结构化排版"
}
```
**Dify System Prompt(按语气动态选择)**
```
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
要求:
1. 保持原意不变,只调整语气和表达方式
2. 改写后的文字长度与原文相近(±30%)
3. 符合IT服务坐席的专业规范
4. 只返回改写后的文字,不要加引号或解释
```
---
### 2.3 文字润色
#### 2.3.1 交互流程
```
坐席点击工具栏"润色"按钮 → 弹出精修面板
├─ 扩写 → AI 扩展内容,增加细节和步骤
├─ 压缩 → AI 精简内容,去除冗余
└─ 纠错 → AI 检查并修正语法/错别字
→ 显示结果 → 确认替换 / 取消
```
#### 2.3.2 功能规格
| 项目 | 规格 |
|------|------|
| 触发条件 | 点击工具栏"润色"按钮(无需选中文字,对全部内容操作) |
| 操作选项 | 扩写 / 压缩 / 纠错(3 种) |
| 显示方式 | 弹出精修面板,左右对比(原文 | 结果) |
| 确认方式 | 点击"替换全部"用结果替换输入框全部内容 |
| 部分采纳 | 支持在结果区域手动编辑后再替换 |
| 超时处理 | 3s 未返回显示重试按钮 |
#### 2.3.3 操作定义
| 操作 | 定义 |
|------|------|
| **扩写** | 在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整 |
| **压缩** | 精简表达,去除重复和冗余,保留核心信息,控制字数 |
| **纠错** | 检查并修正错别字、语法错误、标点符号、格式问题 |
#### 2.3.4 后端 API
```
POST /api/conversations/{conversation_id}/wingman/polish
```
**请求体**
```json
{
"text": "你先试试重启一下vpn客户端 然后看看能不能连上来",
"action": "expand",
"conversation_context": true
}
```
**响应体**
```json
{
"polished_text": "建议您按以下步骤操作:\n1. 完全退出VPN客户端(不只是断开,需要关闭进程)\n2. 重新启动VPN客户端\n3. 输入账号密码重新连接\n4. 如仍无法连接,请截图错误信息发给我",
"action": "expand",
"changes_summary": "增加了操作步骤细化、注意事项和兜底方案"
}
```
---
### 2.4 智能改写
#### 2.4.1 交互流程
```
坐席点击工具栏"改写"按钮 → AI 基于对话上下文+知识库生成3个备选版本
→ 弹出选择面板,展示3个版本
→ 坐席选择一个版本 → 替换输入框内容 / 追加到输入框
```
#### 2.4.2 功能规格
| 项目 | 规格 |
|------|------|
| 触发条件 | 点击工具栏"改写"按钮 |
| 生成数量 | 3 个备选版本 |
| 版本差异 | 版本1=简洁直接 / 版本2=详细带步骤 / 版本3=带知识库引用 |
| 显示方式 | 弹出面板,3 个版本纵向排列,可滚动预览 |
| 选择方式 | 点击版本卡片 → "替换"或"追加" |
| 上下文 | 携带最近 10 条对话消息 + 知识库检索结果 |
| 超时处理 | 5s 未返回显示重试按钮 |
| 与草稿生成关系 | 草稿生成是右栏入口;改写是输入框内入口,更轻量 |
#### 2.4.3 后端 API
```
POST /api/conversations/{conversation_id}/wingman/rewrite
```
**请求体**
```json
{
"current_text": "",
"generate_count": 3,
"include_knowledge": true
}
```
**响应体**
```json
{
"versions": [
{
"text": "VPN连接异常,建议重启客户端后重试。",
"style": "简洁直接",
"source": "对话上下文"
},
{
"text": "VPN连接问题处理步骤:\n1. 退出VPN客户端\n2. 重启客户端\n3. 重新连接\n如仍有问题请截图反馈",
"style": "详细带步骤",
"source": "对话上下文"
},
{
"text": "根据知识库文档《VPN故障排查指南》,DNS解析超时是常见原因。建议:\n1. 执行 ipconfig /flushdns\n2. 重启VPN客户端\n3. 重连\n参考文档:[KB-VPN-001]",
"style": "带知识库引用",
"source": "RAGFlow知识库"
}
]
}
```
---
## 3. UI 设计规格
### 3.1 工具栏布局
在 ReplyBox.vue 现有工具栏中新增 4 个 AI 辅助按钮:
```
现有工具栏: [表情] [文件] [截图] [拍照] [语音] [闪电快捷回复] [邀请] [发送]
新增区域: [补齐] [语气] [润色] [改写] [发送]
```
- AI 辅助按钮使用 Element Plus 的 `el-tooltip` 悬浮提示
- 按钮图标使用简洁线性图标,AI 相关按钮使用统一的紫色系(#534AB7)以区分
- 按钮间距 8px,与现有按钮风格一致
### 3.2 幽灵文字样式
```css
.ghost-text {
color: #B4B2A9; /* 灰色 */
font-style: italic; /* 斜体 */
pointer-events: none; /* 不可点击 */
user-select: none; /* 不可选中 */
}
```
### 3.3 语气选择浮层
```
+-------------------------------+
| 选择语气 |
+-------------------------------+
| [专业] 使用准确术语,结构化 |
| [友好] 增加问候,语气亲和 |
| [简洁] 去除冗余,直奔主题 |
+-------------------------------+
| 原文: VPN连不上别急我看看... |
| 改写: 经排查,VPN连接异常... |
+-------------------------------+
| [替换] [取消] |
+-------------------------------+
```
### 3.4 润色精修面板
```
+------------------------------------------+
| 文字润色 [扩写] [压缩] [纠错] |
+------------------------------------------+
| 原文 | 结果(可编辑) |
| ---------- | ---------- |
| 你先试试重启 | 建议您按以下步骤: |
| 一下vpn客户端 | 1. 退出VPN客户端 |
| 然后看看能不能 | 2. 重新启动 |
| 连上来 | 3. 重新连接 |
| | 4. 如仍不行请截图 |
+------------------------------------------+
| [替换全部] [取消] |
+------------------------------------------+
```
### 3.5 改写版本选择面板
```
+------------------------------------------+
| 智能改写 — 3 个备选版本 |
+------------------------------------------+
| 版本1: 简洁直接 |
| VPN连接异常,建议重启客户端后重试。 |
| [替换] [追加] |
+------------------------------------------+
| 版本2: 详细带步骤 |
| VPN连接问题处理步骤: |
| 1. 退出VPN客户端 |
| 2. 重启客户端 |
| ... |
| [替换] [追加] |
+------------------------------------------+
| 版本3: 带知识库引用 |
| 根据知识库文档《VPN故障排查》... |
| [替换] [追加] |
+------------------------------------------+
```
---
## 4. 技术实现方案
### 4.1 后端
#### 4.1.1 新增 API 端点
`backend/app/api/wingman.py` 中新增 4 个端点:
| 端点 | 方法 | 功能 |
|------|------|------|
| `/api/conversations/{id}/wingman/autocomplete` | POST | 实时自动补齐 |
| `/api/conversations/{id}/wingman/tone-adjust` | POST | 语气调整 |
| `/api/conversations/{id}/wingman/polish` | POST | 文字润色 |
| `/api/conversations/{id}/wingman/rewrite` | POST | 智能改写 |
#### 4.1.2 WingmanService 扩展
`backend/app/services/wingman_service.py` 中新增 4 个方法:
- `generate_completion()` — 补齐,使用低 temperature (0.2)
- `adjust_tone()` — 语气调整,使用中 temperature (0.3)
- `polish_text()` — 润色,使用中 temperature (0.3)
- `rewrite_versions()` — 改写,使用高 temperature (0.6) 生成多样化版本
每个方法使用独立的 system prompt,复用现有 Dify API 调用管道。
#### 4.1.3 性能优化
- 补齐 API 设置独立超时:`timeout=3s`(比其他 API 更短)
- 补齐 API 请求添加 `X-Request-Id` 头,前端可取消过时请求
- 考虑对补齐结果做 Redis 缓存(key = hash(当前文字+对话ID)TTL=30s
### 4.2 前端
#### 4.2.1 组件结构
```
frontend-agent/src/components/chat/
├── ReplyBox.vue (修改: 新增 AI 工具栏按钮)
├── ai-assist/
│ ├── GhostText.vue (新建: 幽灵文字渲染层)
│ ├── ToneAdjustPopover.vue (新建: 语气选择浮层)
│ ├── PolishPanel.vue (新建: 润色精修面板)
│ ├── RewritePanel.vue (新建: 改写版本选择面板)
│ └── AiAssistToolbar.vue (新建: AI 辅助工具栏组件)
```
#### 4.2.2 API 封装
`frontend-agent/src/api/wingman.ts` 中新增 4 个方法:
```typescript
export function autocomplete(convId: number, text: string, cursorPos: number): Promise<AutocompleteResponse>
export function adjustTone(convId: number, selectedText: string, fullText: string, tone: ToneType): Promise<ToneAdjustResponse>
export function polishText(convId: number, text: string, action: PolishAction): Promise<PolishResponse>
export function rewriteVersions(convId: number, currentText: string): Promise<RewriteResponse>
```
#### 4.2.3 关键实现细节
**幽灵文字定位**
- 创建一个隐藏的 `<div>` 镜像 textarea 的样式和内容
- 通过镜像 div 计算光标在文本中的像素位置
- 在该位置覆盖一层透明 `<textarea>` 或使用 `contenteditable` 渲染幽灵文字
**请求取消**
- 使用 `AbortController` 管理补齐请求
- 新请求发起时 abort 上一个未完成的请求
**防抖**
- 补齐:debounce 800ms
- 语气/润色/改写:无防抖(用户主动触发)
---
## 5. 与现有功能的关系
| 现有功能 | 新功能 | 关系 |
|----------|--------|------|
| AI 草稿生成(右栏 Wingman) | 智能改写(输入框内) | **互补**:草稿生成用于从零开始;改写用于对已有输入的多版本化 |
| AI 草稿气泡 | 实时自动补齐 | **互补**:草稿气泡是系统主动推荐;补齐是输入过程中的延续 |
| 快速回复模板 | 语气调整 / 润色 | **互补**:快速回复是模板填充;语气/润色是对自由文本的加工 |
| AI 推荐回复(内联 Mock) | 智能改写 | **替代路径**:改写功能可使用真实 API,逐步替代 Mock 推荐回复 |
---
## 6. 非功能需求
| 项目 | 要求 |
|------|------|
| 补齐延迟 | P95 < 1.5s |
| 语气/润色延迟 | P95 < 3s |
| 改写延迟 | P95 < 5s |
| 可用性 | AI 服务不可用时不影响正常输入和发送 |
| 并发 | 单个坐席同一时刻最多 1 个补齐请求 + 1 个其他 AI 请求 |
| 数据安全 | 补齐请求不记录到数据库;语气/润色/改写可记录用于后续优化 |
| 浏览器兼容 | Chrome 90+ / Edge 90+ / 企微内置浏览器 |
---
## 7. 开发计划
### 阶段一:后端 API + 前端骨架(预计 2 天)
- [ ] WingmanService 新增 4 个方法 + Dify system prompt
- [ ] wingman.py 新增 4 个 API 端点
- [ ] 前端 wingman.ts 新增 API 封装
- [ ] ReplyBox.vue 新增 AI 工具栏按钮
### 阶段二:语气调整 + 文字润色(预计 2 天)
- [ ] ToneAdjustPopover.vue 组件
- [ ] PolishPanel.vue 组件
- [ ] 联调测试
### 阶段三:智能改写(预计 1 天)
- [ ] RewritePanel.vue 组件
- [ ] 知识库检索集成
- [ ] 联调测试
### 阶段四:实时自动补齐(预计 2 天)
- [ ] GhostText.vue 幽灵文字渲染
- [ ] 光标定位计算
- [ ] 请求防抖 + 取消机制
- [ ] 联调测试
### 阶段五:集成测试 + 优化(预计 1 天)
- [ ] 端到端测试
- [ ] 性能优化
- [ ] 边界情况处理
---
## 8. 验收标准
### 8.1 功能验收
- [ ] 坐席输入停顿 > 0.8s 后,光标位置出现灰色幽灵文字
- [ ] Tab 键接受补齐,文字变为正常颜色
- [ ] 选中文字后点击"语气"按钮,弹出 3 种语气选项
- [ ] 选择语气后显示原文/改写文对比,确认后替换
- [ ] 点击"润色"按钮,弹出精修面板,支持扩写/压缩/纠错
- [ ] 点击"改写"按钮,生成 3 个备选版本,可选择替换或追加
- [ ] AI 服务不可用时不影响正常输入和发送
### 8.2 性能验收
- [ ] 补齐 P95 延迟 < 1.5s
- [ ] 语气/润色 P95 延迟 < 3s
- [ ] 改写 P95 延迟 < 5s
- [ ] 输入过程中无明显卡顿
### 8.3 兼容性验收
- [ ] Chrome 90+ 正常工作
- [ ] 企微内置浏览器正常工作
- [ ] 移动端 H5 不受影响(本功能仅坐席端)
---
## 9. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| 幽灵文字定位不准确 | 中 | 中 | 首版可采用建议条过渡方案 |
| Dify 补齐延迟过高 | 中 | 高 | 设置 1.5s 超时,降级为不显示 |
| 坐席过度依赖 AI | 低 | 中 | 补齐建议有置信度阈值,低置信度不显示 |
| 语气改写偏离原意 | 中 | 中 | 显示原文/改写文对比,坐席确认后才替换 |
| 并发请求冲突 | 低 | 低 | AbortController 管理请求生命周期 |
---
## 附录 ADify System Prompt 汇总
### A.1 自动补齐
```
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
要求:
1. 补齐内容自然衔接当前文字,不要重复已有内容
2. 长度控制在1-2个短句,不超过80字
3. 语气专业、简洁,符合IT服务规范
4. 只返回补齐的文字,不要加引号或其他标记
```
### A.2 语气调整
```
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
语气定义:
- 专业:使用准确的技术术语,结构化表达,去除口语化内容
- 友好:适当增加问候和关心语,语气更亲和
- 简洁:去除冗余,直奔主题,控制字数
要求:
1. 保持原意不变,只调整语气和表达方式
2. 改写后的文字长度与原文相近(±30%)
3. 符合IT服务坐席的专业规范
4. 只返回改写后的文字,不要加引号或解释
```
### A.3 文字润色
```
你是一个IT服务文字精修助手。对坐席输入的文字进行{action}处理。
操作定义:
- 扩写:在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整
- 压缩:精简表达,去除重复和冗余,保留核心信息,控制字数
- 纠错:检查并修正错别字、语法错误、标点符号、格式问题
要求:
1. 保持原意不变
2. 只返回处理后的文字,不要加引号或解释
```
### A.4 智能改写
```
你是一个IT服务智能回复助手。基于对话上下文和知识库,为坐席生成3个不同风格的备选回复。
版本要求:
- 版本1:简洁直接,一句话说明问题和解决方案
- 版本2:详细带步骤,包含操作步骤和注意事项
- 版本3:带知识库引用,引用相关文档并给出权威解决方案
要求:
1. 3个版本内容不重复,各有侧重
2. 每个版本独立成段,用---分隔
3. 只返回回复内容,不要加额外解释
```