Compare commits
3 Commits
21c9eaa44d
...
1d89bc89fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d89bc89fe | |||
| ab9b099e9b | |||
| 1e004f1a83 |
40
AGENT.md
40
AGENT.md
@@ -11,8 +11,8 @@
|
||||
- 文件上传与附件管理
|
||||
- 前后端统一的管理控制台
|
||||
|
||||
当前阶段已经完成平台骨架、公共接口规范、知识库/知识文档管理、文档上传、文档解析、解析快照与手动切片入口。
|
||||
后续重点从"文档可切片"推进到"向量可检索"、"模型可路由"和"Agent 可运行"。
|
||||
当前阶段已经完成平台骨架、公共接口规范、知识库/知识文档管理、文档上传、文档解析、解析快照、手动切片入口、模型平台基础配置与 Agent 定义管理/调试入口。
|
||||
后续重点从"文档可切片"推进到"向量可检索"、"模型可路由"和"Agent 运行时可编排"。
|
||||
|
||||
## 2. 总体设计思路
|
||||
|
||||
@@ -74,18 +74,24 @@
|
||||
|
||||
### 3.3 Agent 运行模块
|
||||
|
||||
后续平台重点能力,建议逐步补齐:
|
||||
当前已落地最小可用能力:
|
||||
|
||||
- Agent 定义
|
||||
- Prompt 模板
|
||||
- 工具注册与调用
|
||||
- 会话上下文与记忆
|
||||
- 执行日志与任务状态
|
||||
- `agent_definition`:Agent 定义管理(CRUD、编码唯一校验、知识库绑定校验)
|
||||
- Agent 管理接口:`/api/agents/list`、`/api/agents/query`、`/api/agents/detail`、`/api/agents/save`、`/api/agents/delete`
|
||||
- Agent 调试接口:`POST /api/agents/{agentId}/chat`,支持普通对话与 RAG 对话两种模式
|
||||
- Agent 调试链路:用户问题向量化 -> `rag_chunk_embedding` 相似度召回 -> 组装上下文 -> Chat 模型回答 -> 返回引用切片
|
||||
- 统一模型调用日志:通过 `ChatModelGateway` 与 `model_call_log` 记录请求 ID、模型、耗时与 token 信息
|
||||
|
||||
后续平台重点能力:
|
||||
|
||||
- Prompt 模板管理
|
||||
- 会话上下文持久化与记忆
|
||||
- 工具注册与调用协议
|
||||
- 执行任务状态与日志
|
||||
- 多步骤编排
|
||||
|
||||
建议未来增加的核心对象:
|
||||
建议后续补齐的核心对象:
|
||||
|
||||
- `agent_definition`
|
||||
- `agent_session`
|
||||
- `agent_message`
|
||||
- `agent_task`
|
||||
@@ -104,6 +110,8 @@
|
||||
- 知识库管理页(完整 CRUD + 概览卡片 + 双栏详情 + 批量上传入口)
|
||||
- 知识文档页(条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除)
|
||||
- 切片任务页(解析成功/失败文档概览与切片入口)
|
||||
- Agent 管理页(Agent 定义管理与知识库绑定)
|
||||
- Agent 调试页(普通对话 / RAG 对话切换、引用切片回显)
|
||||
|
||||
前端技术要点:
|
||||
|
||||
@@ -125,7 +133,7 @@
|
||||
- 附件管理页面前端联调
|
||||
- RAG 检索配置、向量索引任务和最近任务页面联调
|
||||
- 模型服务商、模型配置、路由规则和调用日志管理
|
||||
- Agent 调试页
|
||||
- Agent 会话历史与运行日志页
|
||||
- 执行日志查看
|
||||
|
||||
## 4. 当前接口设计原则
|
||||
@@ -219,7 +227,7 @@
|
||||
5. ~~接入切片生成与切片持久化~~(已完成定长/分隔符切片与手动切片入口)
|
||||
6. 建设模型服务商配置与模型路由层
|
||||
7. 接入 Embedding / Chat 模型并完成向量写入
|
||||
8. 建立 Agent 运行时骨架
|
||||
8. 完善 Agent 运行时骨架(会话、工具、任务)
|
||||
9. ~~补前端控制台基础骨架~~(已完成,部分高级页面待联调)
|
||||
|
||||
剩余重点:
|
||||
@@ -227,17 +235,17 @@
|
||||
- 完成模型服务商配置、模型配置、路由规则和调用日志基础能力
|
||||
- 接入 Embedding,生成并保存 `rag_chunk_embedding`
|
||||
- 补齐索引任务、重试、重建索引和最近任务接口
|
||||
- 接入 OpenAI-compatible / Spring AI 适配层并实现最小模型调用链路
|
||||
- 扩展 Agent 会话、工具调用与任务编排能力
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
结合当前代码状态,接下来建议重点做:
|
||||
|
||||
- 实现模型服务商和模型配置表:支持 Ollama、硅基流动、百炼等 OpenAI-compatible 来源
|
||||
- 实现 Embedding 网关:对已落库切片调用 Embedding 模型并写入 `rag_chunk_embedding`
|
||||
- 完成 RAG 全量向量化链路,确保知识库可稳定召回
|
||||
- 为 Agent 调试链路补齐会话持久化与多轮上下文管理
|
||||
- 建立 Agent 工具注册与调用协议,沉淀最小工具集
|
||||
- 把 `indexStatus` 从手工字段推进为真实状态流转
|
||||
- 补齐重建索引、失败重试、最近任务接口和前端展示
|
||||
- 接入模型路由,实现本地小模型与云端大模型的成本优先调用链路
|
||||
|
||||
## 8. 文档用途说明
|
||||
|
||||
|
||||
49
README.md
49
README.md
@@ -3,8 +3,8 @@
|
||||
Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。
|
||||
项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。
|
||||
|
||||
当前项目已经完成基础工程、公共模块、RAG 元数据管理、文档上传、文档解析入口、解析快照、手动切片入口、前端知识库与知识文档管理页面。
|
||||
Agent 运行时、RAG 向量化、检索问答、模型服务商配置与更多平台管理能力会在后续阶段逐步实现。
|
||||
当前项目已经完成基础工程、公共模块、RAG 元数据管理、文档上传、文档解析入口、解析快照、手动切片入口、模型服务商配置基础能力、Agent 定义管理与调试页面。
|
||||
会话持久化、工具调用编排、RAG 全量向量化与检索问答能力会在后续阶段逐步完善。
|
||||
|
||||
## 项目愿景
|
||||
|
||||
@@ -13,7 +13,7 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台:
|
||||
- Agent 运行时:支持对话、工具调用、记忆、任务执行和流程编排。
|
||||
- RAG 知识库:支持文档导入、解析、切片、向量化、检索和基于上下文的回答生成。
|
||||
- 模型抽象:通过 Spring AI 统一接入聊天模型、Embedding 模型和重排序模型。
|
||||
- 管理控制台:提供会话、Agent、知识库、文档、提示词和系统配置的 Web 管理界面。
|
||||
- 管理控制台:提供 Agent、知识库、文档、模型配置和系统配置的 Web 管理界面。
|
||||
- 多环境部署:支持本地开发、测试环境和生产环境的配置隔离。
|
||||
|
||||
## 当前技术栈
|
||||
@@ -36,7 +36,7 @@ common_agent
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # Axios 封装与各模块 API
|
||||
│ │ ├── layouts/ # AdminLayout 管理后台布局
|
||||
│ │ ├── pages/ # 业务页面(工作台、枚举、附件、知识库、文档)
|
||||
│ │ ├── pages/ # 业务页面(系统、RAG、Agent)
|
||||
│ │ ├── router/ # Vue Router 配置
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── styles/ # 全局样式
|
||||
@@ -59,14 +59,16 @@ common_agent
|
||||
│ │ │ ├── handler/ # GlobalExceptionHandler
|
||||
│ │ │ ├── mapper/ # SysAttachmentMapper, SysEnumMapper
|
||||
│ │ │ └── service/ # 接口与实现
|
||||
│ │ └── rag/ # RAG 知识库模块
|
||||
│ │ ├── constant/ # RagSystemConstants
|
||||
│ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||
│ │ ├── dto/ # 请求/响应 DTO
|
||||
│ │ ├── entity/ # RagStore, RagDocument, RagChunk, RagChunkEmbedding
|
||||
│ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum, RagChunkStrategyEnum
|
||||
│ │ ├── mapper/ # RagDocumentMapper, RagStoreMapper
|
||||
│ │ └── service/ # 接口与实现
|
||||
│ │ ├── rag/ # RAG 知识库模块
|
||||
│ │ │ ├── constant/ # RagSystemConstants
|
||||
│ │ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||
│ │ │ ├── dto/ # 请求/响应 DTO
|
||||
│ │ │ ├── entity/ # RagStore, RagDocument, RagChunk, RagChunkEmbedding
|
||||
│ │ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum, RagChunkStrategyEnum
|
||||
│ │ │ ├── mapper/ # RagDocumentMapper, RagStoreMapper
|
||||
│ │ │ └── service/ # 接口与实现
|
||||
│ │ ├── modelprovider/ # 模型服务商、模型配置、路由、网关与调用日志
|
||||
│ │ └── agent/ # Agent 定义管理与调试链路
|
||||
│ ├── main/resources/
|
||||
│ │ ├── application.yaml # 环境选择
|
||||
│ │ ├── application-dev.yaml # 开发环境配置
|
||||
@@ -74,8 +76,12 @@ common_agent
|
||||
│ └── test/java/ # 单元测试(结构稳定性测试 + 前端 API 测试)
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md # 架构说明
|
||||
│ └── ROADMAP.md # 开发路线图
|
||||
│ ├── ROADMAP.md # 开发路线图
|
||||
│ ├── MODEL_PROVIDER_REQUIREMENTS.md # 模型服务商配置与路由需求
|
||||
│ ├── MODEL_PROVIDER_DESIGN.md # 模型服务商配置与路由设计
|
||||
│ └── MODEL_PROVIDER_SCHEMA.sql # 模型平台与Agent核心表结构
|
||||
├── AGENT.md # 平台设计草案
|
||||
├── agent-page-apis.md # Agent页面后端接口清单
|
||||
├── pom.xml
|
||||
└── README.md
|
||||
```
|
||||
@@ -152,6 +158,8 @@ npm run build
|
||||
| 知识库 | 完整 CRUD + 双栏详情 |
|
||||
| 知识文档 | 条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除 |
|
||||
| 切片任务 | 解析成功/失败文档概览 + 切片入口 |
|
||||
| Agent管理 | Agent 定义 CRUD + 知识库绑定 |
|
||||
| Agent调试 | 普通对话 / RAG 对话切换 + 引用切片回显 |
|
||||
|
||||
当前 UI 规范:
|
||||
|
||||
@@ -171,16 +179,24 @@ npm run build
|
||||
|
||||
## RAG 当前能力边界
|
||||
|
||||
当前 RAG 已经从元数据管理推进到"上传 + 解析 + 手动切片"阶段:
|
||||
当前 RAG 已经从元数据管理推进到"上传 + 解析 + 手动切片 + Agent 调试召回"阶段:
|
||||
|
||||
- 知识库:支持列表、条件查询、详情、总览、单库文档概览、新增、编辑、删除。
|
||||
- 知识文档:支持列表、条件查询、详情、新增/编辑、删除、批量上传。
|
||||
- 文档解析:基于 Apache Tika 支持 TXT/Markdown/LOG、PDF、Word、Excel 文本抽取,解析时更新 `parseStatus` 并保存解析快照。
|
||||
- 文档切片:支持按解析快照进行手动异步切片,已落地定长切片和分隔符切片,写入 `rag_chunk`。
|
||||
- 向量表:`rag_chunk_embedding` 实体、Mapper、Service 已有结构,向量写入、检索召回和重排序仍待接入。
|
||||
- 模型配置:已补充模型服务商配置与路由需求/设计文档,后续用于统一接入 Ollama、硅基流动、百炼等来源。
|
||||
- 向量表:`rag_chunk_embedding` 实体、Mapper、Service 已有结构,向量写入与召回 SQL 已用于 Agent 调试链路,RAG 检索问答接口仍待补齐。
|
||||
- 模型配置:模型服务商、模型配置、路由规则、调用日志基础能力已落地,Embedding/Chat 网关可用于 RAG 与 Agent 调试调用。
|
||||
- 前端:知识库页、知识文档页、RAG 工作台和切片任务页已经接入当前接口,检索配置、最近任务、重建索引仍是后续能力。
|
||||
|
||||
## Agent 当前能力边界
|
||||
|
||||
- Agent 定义:支持 `agent_definition` 的列表、查询、详情、新增/更新、删除。
|
||||
- Agent 对话:支持 `POST /api/agents/{agentId}/chat`,`ragEnabled=true` 时走 RAG 召回,`false` 时走普通对话。
|
||||
- RAG 对话流程:用户问题向量化 -> 按知识库召回 TopK 切片 -> 组装系统提示词与上下文 -> Chat 模型回答。
|
||||
- 调试回显:返回答案、请求 ID 和引用切片,便于前端页面展示与排障。
|
||||
- 当前限制:尚未持久化 `agent_session/agent_message`,工具调用和任务编排仍在规划中。
|
||||
|
||||
## 规划模块
|
||||
|
||||
- `agent-core`:Agent 执行模型、工具注册、记忆和编排能力。
|
||||
@@ -197,6 +213,7 @@ npm run build
|
||||
- [模型服务商配置与路由需求](docs/MODEL_PROVIDER_REQUIREMENTS.md)
|
||||
- [模型服务商配置与路由设计](docs/MODEL_PROVIDER_DESIGN.md)
|
||||
- [平台设计草案](AGENT.md)
|
||||
- [Agent 页面接口清单](agent-page-apis.md)
|
||||
|
||||
## 参考资料
|
||||
|
||||
|
||||
159
agent-page-apis.md
Normal file
159
agent-page-apis.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Agent 页面后端接口清单
|
||||
|
||||
本文对应前端页面:[AgentManagePage.vue](frontend/src/pages/agent/AgentManagePage.vue) 和 [AgentDebugPage.vue](frontend/src/pages/agent/AgentDebugPage.vue)。
|
||||
|
||||
## 1. 页面目标
|
||||
|
||||
Agent 页面分为两块:
|
||||
|
||||
- Agent 管理:维护 `agent_definition` 基础配置(编码、名称、知识库绑定、状态、系统提示词)。
|
||||
- Agent 调试:选择 Agent 发起对话,支持普通对话与 RAG 对话切换,并回显引用切片。
|
||||
|
||||
## 2. Agent 管理接口
|
||||
|
||||
### 2.1 查询全部 Agent
|
||||
|
||||
- `POST /api/agents/list`
|
||||
|
||||
返回类型:
|
||||
|
||||
- `RequestResult<List<AgentDefinitionResponse>>`
|
||||
|
||||
### 2.2 条件查询 Agent
|
||||
|
||||
- `POST /api/agents/query`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识助手",
|
||||
"status": "ENABLED",
|
||||
"storeId": 1001
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 查询 Agent 详情
|
||||
|
||||
- `GET /api/agents/detail?id={id}`
|
||||
|
||||
### 2.4 新增或更新 Agent
|
||||
|
||||
- `POST /api/agents/save`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识问答助手",
|
||||
"systemPrompt": "你是企业知识助手,请优先基于知识库回答。",
|
||||
"storeId": 1001,
|
||||
"status": "ENABLED",
|
||||
"remark": "客服场景"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `id` 为空时新增,非空时更新。
|
||||
- `agentCode` 全局唯一。
|
||||
- `storeId` 必须指向已存在的 `rag_store`。
|
||||
- `status` 默认 `ENABLED`,可选 `ENABLED` / `DISABLED`。
|
||||
|
||||
### 2.5 删除 Agent
|
||||
|
||||
- `POST /api/agents/delete?id={id}`
|
||||
|
||||
## 3. Agent 调试接口
|
||||
|
||||
### 3.1 发起对话
|
||||
|
||||
- `POST /api/agents/{agentId}/chat`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{ "role": "user", "content": "请说明请假流程" }
|
||||
],
|
||||
"ragEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"resultcode": "0",
|
||||
"message": null,
|
||||
"data": {
|
||||
"agentId": 1,
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识问答助手",
|
||||
"storeId": 1001,
|
||||
"storeName": "企业知识库",
|
||||
"answer": "根据知识库,先提交 OA 审批单。",
|
||||
"modelRequestId": "f4215d13d0b3493e963297f15428e2f2",
|
||||
"references": [
|
||||
{
|
||||
"chunkId": 9001,
|
||||
"documentId": 8001,
|
||||
"chunkContent": "请假流程:员工先在OA提交审批单...",
|
||||
"score": 0.9123
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 对话模式说明
|
||||
|
||||
### 4.1 `ragEnabled=true`(默认)
|
||||
|
||||
执行路径:
|
||||
|
||||
1. 从消息列表中提取最后一条 `role=user` 的问题。
|
||||
2. 读取该 Agent 绑定知识库的生效 Embedding 配置。
|
||||
3. 生成查询向量并在 `rag_chunk_embedding` 按知识库 TopK 召回切片。
|
||||
4. 将系统提示词、召回片段和会话消息组装后调用 Chat 模型。
|
||||
5. 返回回答 + 引用切片 + `modelRequestId`。
|
||||
|
||||
### 4.2 `ragEnabled=false`
|
||||
|
||||
执行路径:
|
||||
|
||||
- 跳过向量化与召回,直接使用会话消息调用 Chat 模型,返回普通对话结果。
|
||||
|
||||
## 5. 调试联调前置条件
|
||||
|
||||
### 5.1 普通对话前置条件
|
||||
|
||||
- Agent 状态为 `ENABLED`。
|
||||
- Agent 已绑定存在的知识库。
|
||||
- 已配置可用的 Chat 路由(`taskType=CHAT_SIMPLE` 或 `RAG_ANSWER`)。
|
||||
|
||||
### 5.2 RAG 对话前置条件
|
||||
|
||||
- 满足普通对话前置条件。
|
||||
- 知识库存在生效 `rag_store_model_config` 且已绑定 Embedding 模型。
|
||||
- 目标知识库至少有可用向量数据(`rag_chunk_embedding`)。
|
||||
|
||||
## 6. 常见失败提示
|
||||
|
||||
- `Agent已停用,暂不支持对话`:需启用 Agent。
|
||||
- `当前知识库未配置Embedding模型,无法执行检索对话`:需先配置知识库 Embedding 模型。
|
||||
- `未召回到可用知识切片,请先完成知识库切片与向量化`:需补齐切片向量化流程。
|
||||
|
||||
## 7. 相关代码入口
|
||||
|
||||
- `src/main/java/com/bruce/agent/controller/AgentDefinitionController.java`
|
||||
- `src/main/java/com/bruce/agent/service/impl/AgentDefinitionServiceImpl.java`
|
||||
- `src/main/java/com/bruce/agent/entity/AgentDefinition.java`
|
||||
- `src/main/java/com/bruce/modelprovider/gateway/ChatModelGatewayImpl.java`
|
||||
- `frontend/src/api/agent.ts`
|
||||
- `frontend/src/pages/agent/AgentManagePage.vue`
|
||||
- `frontend/src/pages/agent/AgentDebugPage.vue`
|
||||
@@ -135,8 +135,50 @@
|
||||
|
||||
- 知识库 CRUD、文档 CRUD、批量上传、Tika 文本解析、解析快照和状态流转已完成。
|
||||
- `rag_chunk` 已支持基于解析快照的手动异步切片,当前已落地定长切片和分隔符切片。
|
||||
- `rag_chunk_embedding` 的结构层已就绪,尚未实现模型调用、向量化、索引任务和检索问答。
|
||||
- 模型服务商配置与路由已有需求/设计文档,后续会作为 Embedding、Chat 和 Rerank 的统一接入层。
|
||||
- `rag_chunk_embedding` 已支持按知识库向量相似度召回 TopK,用于 Agent 调试链路引用回显。
|
||||
- RAG 对外检索问答接口、索引任务化和重排序能力仍在后续建设中。
|
||||
|
||||
### 3.3 Agent 模块
|
||||
|
||||
包路径:`com.bruce.agent`
|
||||
|
||||
职责:
|
||||
|
||||
- 维护 Agent 定义主数据(CRUD + 编码唯一性 + 绑定知识库校验)。
|
||||
- 提供 Agent 调试对话接口,支持普通对话与 RAG 对话模式切换。
|
||||
- 在 RAG 对话模式下,完成“问题向量化 -> 切片召回 -> 上下文组装 -> Chat 模型回答”的最小链路。
|
||||
- 返回引用切片和请求 ID,便于前端调试与调用追踪。
|
||||
|
||||
关键类:
|
||||
|
||||
| 类 | 路径 |
|
||||
|----|------|
|
||||
| AgentDefinition | `agent/entity/AgentDefinition.java` |
|
||||
| AgentDefinitionController | `agent/controller/AgentDefinitionController.java` |
|
||||
| AgentDefinitionServiceImpl | `agent/service/impl/AgentDefinitionServiceImpl.java` |
|
||||
| AgentDefinitionResponse | `agent/dto/response/AgentDefinitionResponse.java` |
|
||||
| AgentChatResponse | `agent/dto/response/AgentChatResponse.java` |
|
||||
| ChatModelGateway | `modelprovider/gateway/ChatModelGateway.java` |
|
||||
| ChatModelGatewayImpl | `modelprovider/gateway/ChatModelGatewayImpl.java` |
|
||||
| ChatRequest | `modelprovider/gateway/ChatRequest.java` |
|
||||
| ChatResult | `modelprovider/gateway/ChatResult.java` |
|
||||
|
||||
接口列表:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/agents/list` | 查询全部 Agent |
|
||||
| POST | `/api/agents/query` | Agent 条件查询 |
|
||||
| GET | `/api/agents/detail` | 获取 Agent 详情 |
|
||||
| POST | `/api/agents/save` | 新增/更新 Agent |
|
||||
| POST | `/api/agents/delete` | 删除 Agent |
|
||||
| POST | `/api/agents/{agentId}/chat` | Agent 调试对话 |
|
||||
|
||||
当前边界:
|
||||
|
||||
- `agent_definition` 与前端 Agent 管理页已完成联调。
|
||||
- 对话入口已支持 `ragEnabled` 开关,`true` 走 RAG 召回,`false` 走普通对话。
|
||||
- 尚未落地会话持久化(`agent_session` / `agent_message`)和工具调用编排。
|
||||
|
||||
## 4. 数据模型关系
|
||||
|
||||
@@ -150,8 +192,9 @@
|
||||
| `rag_document` | 知识库文档表 | 关联 `rag_store.id` 和 `sys_attachment.id` |
|
||||
| `rag_chunk` | 知识切片表 | 关联 `rag_store.id` 和 `rag_document.id` |
|
||||
| `rag_chunk_embedding` | 切片向量表 | 关联 `rag_store.id`、`rag_document.id` 和 `rag_chunk.id` |
|
||||
| `agent_definition` | Agent 定义表 | 关联 `rag_store.id` |
|
||||
|
||||
`rag_document` 是 RAG 模块与附件模块的连接点,`rag_chunk` 和 `rag_chunk_embedding` 是下一步检索链路的核心落点。
|
||||
`rag_document` 是 RAG 模块与附件模块的连接点,`rag_chunk` 和 `rag_chunk_embedding` 是检索链路核心落点,`agent_definition` 负责把 Agent 与知识库绑定到同一调用链路。
|
||||
|
||||
## 5. 配置与运行
|
||||
|
||||
@@ -183,16 +226,15 @@
|
||||
|
||||
## 7. 当前不足
|
||||
|
||||
- RAG 尚未进入"可检索链路",当前完成上传、解析和手动切片,但未完成向量化和召回。
|
||||
- 模型服务商配置、模型路由和调用日志尚未落地代码。
|
||||
- Agent 运行时相关模型与服务尚未开始建设。
|
||||
- RAG 尚未形成独立检索问答接口,当前召回能力主要用于 Agent 调试链路。
|
||||
- Agent 运行时尚未持久化会话,工具调用与任务编排仍未落地。
|
||||
- 前端部分页面(附件管理、检索配置、最近任务)为占位或后续能力提示。
|
||||
- 缺少鉴权、租户、操作日志。
|
||||
|
||||
## 8. 建议演进方向
|
||||
|
||||
1. 补 RAG 最小检索闭环:解析文本 → 生成切片 → 生成向量 → 检索召回。
|
||||
2. 建设模型服务商配置与路由层,统一接入 Ollama、硅基流动、百炼等 OpenAI-compatible 来源。
|
||||
3. 建设 Agent 域模型:Agent、Session、Message、Tool、Task。
|
||||
2. 把当前 Agent 调试链路升级为会话化运行:沉淀 Session、Message 和上下文裁剪策略。
|
||||
3. 建设 Agent 工具注册与调用协议,补齐任务状态与执行日志。
|
||||
4. 补齐索引任务、重试、重建索引和前端任务视图。
|
||||
5. 衔接模型供应商、Spring AI 适配层、工作流编排和前端管理台。
|
||||
|
||||
@@ -94,6 +94,23 @@ CREATE TABLE IF NOT EXISTS rag_store_model_config (
|
||||
CONSTRAINT fk_rag_store_model_config_embedding_model_id FOREIGN KEY (embedding_model_id) REFERENCES model_config (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
agent_code VARCHAR(100) NOT NULL,
|
||||
agent_name VARCHAR(200) NOT NULL,
|
||||
system_prompt TEXT,
|
||||
store_id BIGINT NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ENABLED',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_definition_code UNIQUE (agent_code),
|
||||
CONSTRAINT fk_agent_definition_store_id FOREIGN KEY (store_id) REFERENCES rag_store (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_call_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
@@ -112,8 +129,12 @@ CREATE TABLE IF NOT EXISTS model_call_log (
|
||||
request_hash VARCHAR(64),
|
||||
error_code VARCHAR(100),
|
||||
error_message VARCHAR(1000),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_call_log_request_id UNIQUE (request_id)
|
||||
);
|
||||
|
||||
@@ -200,6 +221,20 @@ COMMENT ON COLUMN rag_store_model_config.remark IS '备注';
|
||||
COMMENT ON COLUMN rag_store_model_config.create_by IS '创建者';
|
||||
COMMENT ON COLUMN rag_store_model_config.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE agent_definition IS 'Agent定义表';
|
||||
COMMENT ON COLUMN agent_definition.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_definition.agent_code IS 'Agent编码';
|
||||
COMMENT ON COLUMN agent_definition.agent_name IS 'Agent名称';
|
||||
COMMENT ON COLUMN agent_definition.system_prompt IS '系统提示词';
|
||||
COMMENT ON COLUMN agent_definition.store_id IS '绑定知识库ID';
|
||||
COMMENT ON COLUMN agent_definition.status IS '状态';
|
||||
COMMENT ON COLUMN agent_definition.version IS '版本';
|
||||
COMMENT ON COLUMN agent_definition.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_definition.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_definition.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_definition.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_definition.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE model_call_log IS '模型调用日志表';
|
||||
COMMENT ON COLUMN model_call_log.id IS 'ID';
|
||||
COMMENT ON COLUMN model_call_log.request_id IS '请求唯一ID';
|
||||
@@ -218,5 +253,9 @@ COMMENT ON COLUMN model_call_log.duration_ms IS '耗时(毫秒)';
|
||||
COMMENT ON COLUMN model_call_log.request_hash IS '请求哈希';
|
||||
COMMENT ON COLUMN model_call_log.error_code IS '错误码';
|
||||
COMMENT ON COLUMN model_call_log.error_message IS '错误信息摘要';
|
||||
COMMENT ON COLUMN model_call_log.version IS '版本';
|
||||
COMMENT ON COLUMN model_call_log.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN model_call_log.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_call_log.remark IS '备注';
|
||||
COMMENT ON COLUMN model_call_log.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_call_log.update_by IS '更新者';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Common Agent 开发路线图
|
||||
|
||||
本文档基于 2026-05-25 当前分支代码整理,用来区分"已经完成""建议优先做""中期建设项"。
|
||||
本文档基于 2026-05-27 当前分支代码整理,用来区分"已经完成""建议优先做""中期建设项"。
|
||||
|
||||
## 已完成
|
||||
|
||||
@@ -52,30 +52,34 @@
|
||||
- 知识文档管理页:条件查询 + 批量上传 + 解析重试 + 批量切片入口 + 编辑/启停用/删除。
|
||||
- RAG 工作台与切片任务页:展示文档解析/切片概览并提供切片入口。
|
||||
- RAG 文档批量上传组件:支持锁定知识库或选择知识库上传。
|
||||
- Agent 管理页:支持 Agent 定义新增、编辑、删除、状态管理和知识库绑定。
|
||||
- Agent 调试页:支持普通对话 / RAG 对话切换、请求 ID 与引用切片回显。
|
||||
- API 层:Axios 封装 + Long 类型安全解析 + 统一错误拦截。
|
||||
- 单元测试:Vitest + @vue/test-utils,覆盖路由、布局、页面和 API。
|
||||
|
||||
### 模型平台设计
|
||||
### 模型平台与 Agent 最小链路
|
||||
|
||||
- 已新增模型服务商配置与路由需求文档:`docs/MODEL_PROVIDER_REQUIREMENTS.md`。
|
||||
- 已新增模型服务商配置与路由设计文档:`docs/MODEL_PROVIDER_DESIGN.md`。
|
||||
- 已明确后续通过模型网关统一接入 Ollama、硅基流动、百炼等 OpenAI-compatible 来源。
|
||||
- 模型服务商、模型配置、路由规则、知识库模型绑定、调用日志核心表结构已落地(`docs/MODEL_PROVIDER_SCHEMA.sql`)。
|
||||
- `EmbeddingModelGateway` 和 `ChatModelGateway` 已落地,统一走 OpenAI-compatible 协议调用上游模型。
|
||||
- `AgentDefinitionController` 与 `AgentDefinitionServiceImpl` 已提供 Agent CRUD 与调试对话接口。
|
||||
- Agent 调试链路已接入:问题向量化 -> `rag_chunk_embedding` 召回 -> 组装上下文 -> Chat 回答 -> 返回引用切片。
|
||||
|
||||
### 质量保障
|
||||
|
||||
- 后端结构稳定性单元测试。
|
||||
- 前端组件与 API 单元测试。
|
||||
- Agent 结构与服务单元测试(`AgentComponentStructureTests`、`AgentDefinitionServiceImplTests`)。
|
||||
|
||||
## 短期优先级
|
||||
|
||||
建议优先完成下面几项,把 RAG 上传解析切片链路升级为可检索链路:
|
||||
|
||||
1. 模型服务商配置:新增服务商、模型、路由规则和调用日志基础表。
|
||||
2. Embedding 网关:优先支持 OpenAI-compatible 接口,接入硅基流动或 Ollama Embedding。
|
||||
3. 向量写入:对 `rag_chunk` 调用 Embedding 模型并保存 `rag_chunk_embedding`。
|
||||
4. 索引任务入口:把文档或知识库的 `indexStatus` 推进为真实状态流转。
|
||||
5. 补齐重建索引、失败重试、最近任务接口。
|
||||
6. 前端接入模型配置、检索配置、最近任务和重建索引动作。
|
||||
1. 打通文档切片后的全量向量写入,确保 `rag_chunk_embedding` 可持续更新。
|
||||
2. 新增独立 RAG 检索问答接口,避免仅依赖 Agent 调试入口消费召回能力。
|
||||
3. 索引任务入口:把文档或知识库的 `indexStatus` 推进为真实状态流转。
|
||||
4. 补齐重建索引、失败重试、最近任务接口。
|
||||
5. 落地 Agent 会话持久化(`agent_session`、`agent_message`)与多轮上下文管理。
|
||||
6. 补齐 Agent 工具注册、工具调用协议和任务执行日志。
|
||||
|
||||
## RAG 最小闭环
|
||||
|
||||
@@ -84,14 +88,14 @@
|
||||
1. ~~批量上传文件,自动创建 `sys_attachment` 与 `rag_document`。~~
|
||||
2. ~~调用解析入口,使用 Tika 抽取文本并更新 `parseStatus`。~~
|
||||
3. ~~根据切片策略生成 `rag_chunk`。~~
|
||||
4. 调用 Embedding 模型生成向量并写入 `rag_chunk_embedding`。
|
||||
5. 提供检索接口,按 query 向量召回切片并返回引用元数据。
|
||||
4. 调用 Embedding 模型生成向量并写入 `rag_chunk_embedding`(已被 Agent 调试链路消费)。
|
||||
5. 提供独立检索接口,按 query 向量召回切片并返回引用元数据。
|
||||
|
||||
## Agent 核心能力
|
||||
|
||||
RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
Agent 主线能力按以下顺序继续推进:
|
||||
|
||||
1. Agent 定义管理。
|
||||
1. ~~Agent 定义管理。~~
|
||||
2. 会话与消息模型。
|
||||
3. 工具注册与工具调用协议。
|
||||
4. Prompt 模板管理。
|
||||
@@ -120,6 +124,8 @@ RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
- RAG 文档批量上传接口:POST `/api/rag/documents/batchUpload`。
|
||||
- RAG 文档解析接口:POST `/api/rag/documents/parse`,当前同步解析、保存解析快照并返回解析元数据。
|
||||
- RAG 文档切片接口:POST `/api/rag/documents/chunk`,当前异步生成并替换 `rag_chunk`。
|
||||
- Agent 管理接口:`/api/agents/list`、`/api/agents/query`、`/api/agents/detail`、`/api/agents/save`、`/api/agents/delete`。
|
||||
- Agent 调试接口:POST `/api/agents/{agentId}/chat`,支持 `ragEnabled` 开关。
|
||||
- 大整数 ID 通过 `@JsonSerialize(ToStringSerializer.class)` 输出为字符串。
|
||||
|
||||
## 里程碑
|
||||
@@ -137,7 +143,8 @@ RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
|
||||
### 里程碑 3:Agent 最小运行时
|
||||
|
||||
- 支持一个可配置 Agent、一个会话、一次模型调用、一次工具调用。
|
||||
- 已完成:支持一个可配置 Agent、一次模型调用与 RAG 召回调试链路。
|
||||
- 待完成:会话持久化、工具调用与任务编排。
|
||||
|
||||
### 里程碑 4:平台管理化
|
||||
|
||||
|
||||
228
docs/STUDIO_PROTOTYPE_SCHEMA.sql
Normal file
228
docs/STUDIO_PROTOTYPE_SCHEMA.sql
Normal file
@@ -0,0 +1,228 @@
|
||||
-- Common Agent Studio prototype schema draft.
|
||||
-- These tables extend the current RAG, model-provider and agent_definition data model
|
||||
-- without replacing existing core entities.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS studio_project (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_code VARCHAR(100) NOT NULL,
|
||||
project_name VARCHAR(200) NOT NULL,
|
||||
environment VARCHAR(50) NOT NULL DEFAULT 'DEV',
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
current_version VARCHAR(50),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_studio_project_code UNIQUE (project_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_id BIGINT,
|
||||
workflow_code VARCHAR(100) NOT NULL,
|
||||
workflow_name VARCHAR(200) NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
bound_agent_id BIGINT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_definition_code UNIQUE (workflow_code),
|
||||
CONSTRAINT fk_workflow_definition_project_id FOREIGN KEY (project_id) REFERENCES studio_project (id),
|
||||
CONSTRAINT fk_workflow_definition_agent_id FOREIGN KEY (bound_agent_id) REFERENCES agent_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_version (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workflow_id BIGINT NOT NULL,
|
||||
version_no INTEGER NOT NULL,
|
||||
snapshot_name VARCHAR(100) NOT NULL,
|
||||
graph_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
published_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_version_no UNIQUE (workflow_id, version_no),
|
||||
CONSTRAINT fk_workflow_version_workflow_id FOREIGN KEY (workflow_id) REFERENCES workflow_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_run (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
workflow_id BIGINT,
|
||||
workflow_version_id BIGINT,
|
||||
agent_id BIGINT,
|
||||
run_source VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
input_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
output_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
duration_ms INTEGER,
|
||||
estimated_cost NUMERIC(14, 8),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_run_request_id UNIQUE (request_id),
|
||||
CONSTRAINT fk_workflow_run_workflow_id FOREIGN KEY (workflow_id) REFERENCES workflow_definition (id),
|
||||
CONSTRAINT fk_workflow_run_version_id FOREIGN KEY (workflow_version_id) REFERENCES workflow_version (id),
|
||||
CONSTRAINT fk_workflow_run_agent_id FOREIGN KEY (agent_id) REFERENCES agent_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_run_step (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
run_id BIGINT NOT NULL,
|
||||
node_id VARCHAR(100) NOT NULL,
|
||||
node_type VARCHAR(50) NOT NULL,
|
||||
node_name VARCHAR(200),
|
||||
status VARCHAR(50) NOT NULL,
|
||||
input_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
output_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
duration_ms INTEGER,
|
||||
error_message VARCHAR(1000),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT fk_workflow_run_step_run_id FOREIGN KEY (run_id) REFERENCES workflow_run (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_server (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
server_code VARCHAR(100) NOT NULL,
|
||||
server_name VARCHAR(200) NOT NULL,
|
||||
import_type VARCHAR(50) NOT NULL,
|
||||
endpoint_url VARCHAR(500),
|
||||
package_name VARCHAR(200),
|
||||
manifest_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
auth_type VARCHAR(50),
|
||||
secret_ref VARCHAR(200),
|
||||
health_status VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_mcp_server_code UNIQUE (server_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_capability (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
server_id BIGINT NOT NULL,
|
||||
capability_code VARCHAR(150) NOT NULL,
|
||||
capability_name VARCHAR(200) NOT NULL,
|
||||
capability_type VARCHAR(50) NOT NULL,
|
||||
schema_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_mcp_capability_code UNIQUE (server_id, capability_code),
|
||||
CONSTRAINT fk_mcp_capability_server_id FOREIGN KEY (server_id) REFERENCES mcp_server (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_code VARCHAR(100) NOT NULL,
|
||||
skill_name VARCHAR(200) NOT NULL,
|
||||
skill_type VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_skill_definition_code UNIQUE (skill_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_version (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_id BIGINT NOT NULL,
|
||||
version_no INTEGER NOT NULL,
|
||||
prompt_text TEXT,
|
||||
code_text TEXT,
|
||||
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
variable_schema_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
test_result_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
published_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_skill_version_no UNIQUE (skill_id, version_no),
|
||||
CONSTRAINT fk_skill_version_skill_id FOREIGN KEY (skill_id) REFERENCES skill_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_session (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_code VARCHAR(100) NOT NULL,
|
||||
agent_id BIGINT NOT NULL,
|
||||
workflow_run_id BIGINT,
|
||||
title VARCHAR(200),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_session_code UNIQUE (session_code),
|
||||
CONSTRAINT fk_agent_session_agent_id FOREIGN KEY (agent_id) REFERENCES agent_definition (id),
|
||||
CONSTRAINT fk_agent_session_run_id FOREIGN KEY (workflow_run_id) REFERENCES workflow_run (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id BIGINT NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
citation_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
token_count INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT fk_agent_message_session_id FOREIGN KEY (session_id) REFERENCES agent_session (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_capability_binding (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
owner_type VARCHAR(50) NOT NULL,
|
||||
owner_id BIGINT NOT NULL,
|
||||
capability_type VARCHAR(50) NOT NULL,
|
||||
capability_id BIGINT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_capability_binding UNIQUE (owner_type, owner_id, capability_type, capability_id)
|
||||
);
|
||||
43
frontend/src/api/__tests__/agent.spec.ts
Normal file
43
frontend/src/api/__tests__/agent.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
chatWithAgent,
|
||||
deleteAgent,
|
||||
getAgentById,
|
||||
listAgents,
|
||||
queryAgents,
|
||||
saveAgent,
|
||||
} from '../agent';
|
||||
import { get, post } from '../request';
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('agent api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('maps agent endpoints correctly', () => {
|
||||
listAgents();
|
||||
queryAgents({ agentCode: 'demo' });
|
||||
getAgentById('1001');
|
||||
saveAgent({ agentCode: 'agent_1', agentName: 'Agent 1', storeId: '2001', status: 'ENABLED' });
|
||||
deleteAgent('1001');
|
||||
chatWithAgent('1001', { messages: [{ role: 'user', content: '你好' }] });
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/agents/list');
|
||||
expect(post).toHaveBeenCalledWith('/agents/query', { agentCode: 'demo' });
|
||||
expect(get).toHaveBeenCalledWith('/agents/detail', { params: { id: '1001' } });
|
||||
expect(post).toHaveBeenCalledWith('/agents/save', {
|
||||
agentCode: 'agent_1',
|
||||
agentName: 'Agent 1',
|
||||
storeId: '2001',
|
||||
status: 'ENABLED',
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith('/agents/delete', undefined, { params: { id: '1001' } });
|
||||
expect(post).toHaveBeenCalledWith('/agents/1001/chat', { messages: [{ role: 'user', content: '你好' }] });
|
||||
});
|
||||
});
|
||||
70
frontend/src/api/agent.ts
Normal file
70
frontend/src/api/agent.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { get, post } from './request';
|
||||
|
||||
export interface AgentDefinition {
|
||||
id?: string;
|
||||
agentCode: string;
|
||||
agentName: string;
|
||||
systemPrompt?: string;
|
||||
storeId: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface AgentDefinitionQueryRequest {
|
||||
agentCode?: string;
|
||||
agentName?: string;
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
}
|
||||
|
||||
export interface AgentMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AgentChatRequest {
|
||||
messages: AgentMessage[];
|
||||
ragEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentReferenceChunk {
|
||||
chunkId: string;
|
||||
documentId: string;
|
||||
chunkContent: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface AgentChatResponse {
|
||||
agentId: string;
|
||||
agentCode: string;
|
||||
agentName: string;
|
||||
storeId: string;
|
||||
storeName?: string;
|
||||
answer: string;
|
||||
modelRequestId: string;
|
||||
references: AgentReferenceChunk[];
|
||||
}
|
||||
|
||||
export function listAgents() {
|
||||
return post<AgentDefinition[]>('/agents/list');
|
||||
}
|
||||
|
||||
export function queryAgents(query?: AgentDefinitionQueryRequest) {
|
||||
return post<AgentDefinition[], AgentDefinitionQueryRequest | undefined>('/agents/query', query);
|
||||
}
|
||||
|
||||
export function getAgentById(id: string) {
|
||||
return get<AgentDefinition>('/agents/detail', { params: { id } });
|
||||
}
|
||||
|
||||
export function saveAgent(data: Partial<AgentDefinition> & { id?: string }) {
|
||||
return post<boolean>('/agents/save', data);
|
||||
}
|
||||
|
||||
export function deleteAgent(id: string) {
|
||||
return post<boolean>('/agents/delete', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function chatWithAgent(agentId: string, data: AgentChatRequest) {
|
||||
return post<AgentChatResponse, AgentChatRequest>(`/agents/${agentId}/chat`, data);
|
||||
}
|
||||
143
frontend/src/data/studioMock.ts
Normal file
143
frontend/src/data/studioMock.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
export type PipelineStatus = 'done' | 'running' | 'blocked' | 'idle';
|
||||
|
||||
export interface LifecycleStep {
|
||||
name: string;
|
||||
description: string;
|
||||
status: PipelineStatus;
|
||||
}
|
||||
|
||||
export interface RecentRun {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
latency: string;
|
||||
cost: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
parseStatus: string;
|
||||
indexStatus: string;
|
||||
chunks: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WorkflowNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
description: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WorkflowEdge {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TraceStep {
|
||||
node: string;
|
||||
status: string;
|
||||
duration: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
export const lifecycleSteps: LifecycleStep[] = [
|
||||
{ name: '知识接入', description: '上传、解析、切片、向量化', status: 'done' },
|
||||
{ name: '能力编排', description: 'Workflow 连接模型、工具与 Skill', status: 'running' },
|
||||
{ name: '对话调试', description: '验证引用、成本、延迟与回答质量', status: 'running' },
|
||||
{ name: '发布观测', description: '版本快照、运行追踪、异常排查', status: 'idle' },
|
||||
];
|
||||
|
||||
export const readinessChecklist = [
|
||||
{ label: '知识库已绑定 Embedding 模型', done: true },
|
||||
{ label: 'Workflow 草稿存在未发布节点变更', done: false },
|
||||
{ label: 'Agent 已绑定默认知识库与 Skill', done: true },
|
||||
{ label: '生产环境路由规则仍需压测', done: false },
|
||||
];
|
||||
|
||||
export const recentRuns: RecentRun[] = [
|
||||
{ id: 'run-1842', name: '售前问答 Agent', type: 'Agent', status: '成功', latency: '1.42s', cost: '¥0.018' },
|
||||
{ id: 'run-1841', name: '合同知识召回', type: 'Workflow', status: '成功', latency: '860ms', cost: '¥0.006' },
|
||||
{ id: 'run-1840', name: 'MCP: jira.search', type: 'MCP', status: '重试', latency: '2.8s', cost: '¥0.000' },
|
||||
];
|
||||
|
||||
export const knowledgeStores = [
|
||||
{ id: '1001', name: '产品制度库', docs: 128, health: 96, status: '可检索' },
|
||||
{ id: '1002', name: '交付项目资料', docs: 64, health: 82, status: '索引中' },
|
||||
{ id: '1003', name: '客服 FAQ', docs: 214, health: 91, status: '可检索' },
|
||||
];
|
||||
|
||||
export const knowledgeDocuments: KnowledgeDocument[] = [
|
||||
{ id: 'doc-01', name: '售前方案模板.pdf', parseStatus: 'PARSED', indexStatus: 'INDEXED', chunks: 42, updatedAt: '10分钟前' },
|
||||
{ id: 'doc-02', name: '项目实施手册.docx', parseStatus: 'PARSED', indexStatus: 'INDEXING', chunks: 88, updatedAt: '23分钟前' },
|
||||
{ id: 'doc-03', name: '服务条款更新.md', parseStatus: 'FAILED', indexStatus: 'PENDING', chunks: 0, updatedAt: '今天 09:12' },
|
||||
{ id: 'doc-04', name: '客服高频问题.xlsx', parseStatus: 'PARSED', indexStatus: 'INDEXED', chunks: 119, updatedAt: '昨天' },
|
||||
];
|
||||
|
||||
export const ingestionSteps: LifecycleStep[] = [
|
||||
{ name: '上传', description: '4 个文件已入库 sys_attachment', status: 'done' },
|
||||
{ name: '解析', description: 'Tika 抽取文本并保存快照', status: 'done' },
|
||||
{ name: '切片', description: '固定长度 800 / overlap 120', status: 'running' },
|
||||
{ name: '向量化', description: 'Qwen3 Embedding 1024 维', status: 'idle' },
|
||||
{ name: '可检索', description: '等待索引任务完成', status: 'idle' },
|
||||
];
|
||||
|
||||
export const workflowNodes: WorkflowNode[] = [
|
||||
{ id: 'start', type: 'START', label: 'Start', description: '接收用户问题', x: 4, y: 42 },
|
||||
{ id: 'retrieve', type: 'KNOWLEDGE_RETRIEVAL', label: 'Knowledge Retrieval', description: 'TopK=6 / score>0.72', x: 25, y: 18 },
|
||||
{ id: 'llm', type: 'LLM', label: 'LLM', description: 'RAG_ANSWER 路由', x: 47, y: 42 },
|
||||
{ id: 'mcp', type: 'MCP_TOOL', label: 'MCP Tool', description: 'jira.search / docs.lookup', x: 47, y: 70 },
|
||||
{ id: 'skill', type: 'SKILL', label: 'Skill', description: '答案审校与引用整理', x: 69, y: 42 },
|
||||
{ id: 'answer', type: 'ANSWER', label: 'Answer', description: '返回回答与引用', x: 88, y: 42 },
|
||||
];
|
||||
|
||||
export const workflowEdges: WorkflowEdge[] = [
|
||||
{ from: 'start', to: 'retrieve' },
|
||||
{ from: 'retrieve', to: 'llm' },
|
||||
{ from: 'llm', to: 'skill' },
|
||||
{ from: 'mcp', to: 'skill' },
|
||||
{ from: 'skill', to: 'answer' },
|
||||
];
|
||||
|
||||
export const traceSteps: TraceStep[] = [
|
||||
{ node: 'Start', status: '完成', duration: '4ms', output: '用户问题已标准化' },
|
||||
{ node: 'Knowledge Retrieval', status: '完成', duration: '218ms', output: '召回 6 个切片' },
|
||||
{ node: 'LLM', status: '完成', duration: '1.12s', output: '生成 612 tokens' },
|
||||
{ node: 'Skill', status: '完成', duration: '88ms', output: '引用格式已校验' },
|
||||
];
|
||||
|
||||
export const chatMessages = [
|
||||
{ role: 'user', content: '如果客户要求私有化部署,售前方案里必须说明哪些内容?' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '建议说明部署拓扑、模型服务商、知识库索引策略、权限边界、日志留存周期和故障恢复方式。当前回答引用了 3 个知识切片。',
|
||||
},
|
||||
];
|
||||
|
||||
export const citations = [
|
||||
{ title: '售前方案模板.pdf', score: '0.91', text: '私有化部署章节应覆盖基础设施、网络、安全与运维边界。' },
|
||||
{ title: '项目实施手册.docx', score: '0.87', text: '交付计划需包含数据导入、索引重建与验收标准。' },
|
||||
{ title: '服务条款更新.md', score: '0.82', text: '客户数据默认不出域,模型调用日志需保留审计字段。' },
|
||||
];
|
||||
|
||||
export const mcpCapabilities = [
|
||||
{ name: 'jira.search', type: 'tool', status: '已启用', description: '按项目、状态、负责人检索任务' },
|
||||
{ name: 'docs.lookup', type: 'resource', status: '已启用', description: '读取外部文档中心条目' },
|
||||
{ name: 'deploy.trigger', type: 'tool', status: '待授权', description: '触发测试环境部署流水线' },
|
||||
];
|
||||
|
||||
export const skillVersions = [
|
||||
{ version: 'v4', status: 'Draft', updatedAt: '刚刚', note: '增加引用一致性检查' },
|
||||
{ version: 'v3', status: 'Published', updatedAt: '昨天', note: '生产环境当前版本' },
|
||||
{ version: 'v2', status: 'Archived', updatedAt: '5天前', note: '旧版回答润色策略' },
|
||||
];
|
||||
|
||||
export const modelRoutes = [
|
||||
{ task: 'RAG_ANSWER', primary: 'qwen-plus', fallback: 'deepseek-v3', latency: '1800ms', status: '启用' },
|
||||
{ task: 'RAG_EMBEDDING', primary: 'Qwen3-Embedding', fallback: '无', latency: '900ms', status: '启用' },
|
||||
{ task: 'AGENT_PLAN', primary: 'gpt-4.1', fallback: 'qwen-max', latency: '3200ms', status: '草稿' },
|
||||
];
|
||||
@@ -1,64 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Box,
|
||||
ChatDotRound,
|
||||
Collection,
|
||||
Connection,
|
||||
Cpu,
|
||||
DataBoard,
|
||||
Document,
|
||||
Grid,
|
||||
Histogram,
|
||||
List,
|
||||
Setting,
|
||||
MagicStick,
|
||||
Monitor,
|
||||
Operation,
|
||||
UploadFilled,
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const systemMenuItems = [
|
||||
{ path: '/system/enums', label: '系统枚举', icon: Grid },
|
||||
{ path: '/system/model/providers', label: '模型服务商', icon: Setting },
|
||||
{ path: '/system/model/configs', label: '模型配置', icon: Setting },
|
||||
{ path: '/system/model/routes', label: '路由规则', icon: Setting },
|
||||
{ path: '/system/model/call-logs', label: '调用日志', icon: Setting },
|
||||
];
|
||||
|
||||
const ragMenuItems = [
|
||||
{ path: '/rag/stores', label: '知识库', icon: Collection },
|
||||
{ path: '/rag/workbench', label: 'RAG工作台', icon: Histogram },
|
||||
{ path: '/rag/documents', label: '知识文档', icon: Document },
|
||||
{ path: '/rag/tasks/chunk', label: '切片任务', icon: List },
|
||||
const studioMenuItems = [
|
||||
{ path: '/studio', label: '工作台', icon: DataBoard },
|
||||
{ path: '/knowledge', label: '知识资产', icon: Collection },
|
||||
{ path: '/knowledge/ingestion', label: '文件解析', icon: UploadFilled },
|
||||
{ path: '/workflows', label: 'Workflow', icon: Connection },
|
||||
{ path: '/agents', label: 'Agent 对话', icon: ChatDotRound },
|
||||
{ path: '/mcp', label: 'MCP', icon: Operation },
|
||||
{ path: '/skills', label: 'Skills', icon: MagicStick },
|
||||
{ path: '/models', label: '模型', icon: Cpu },
|
||||
{ path: '/observability', label: '观测', icon: Monitor },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="admin-layout">
|
||||
<el-aside class="admin-sidebar" width="232px">
|
||||
<el-container class="admin-layout studio-shell">
|
||||
<el-aside class="admin-sidebar studio-sidebar" width="248px">
|
||||
<div class="brand">
|
||||
<el-icon :size="24">
|
||||
<Box />
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span>Common Agent</span>
|
||||
<div>
|
||||
<span>Common Agent Studio</span>
|
||||
<small>AI Agent Development Platform</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-menu class="side-menu" :default-active="$route.path" router>
|
||||
<el-sub-menu index="system">
|
||||
<template #title>系统管理</template>
|
||||
<el-menu-item v-for="item in systemMenuItems" :key="item.path" :index="item.path">
|
||||
<el-menu class="side-menu studio-menu" :default-active="$route.path" router>
|
||||
<el-menu-item v-for="item in studioMenuItems" :key="item.path" :index="item.path">
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="rag">
|
||||
<template #title>RAG</template>
|
||||
<el-menu-item v-for="item in ragMenuItems" :key="item.path" :index="item.path">
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
|
||||
<div class="sidebar-status">
|
||||
<span>Dev 环境</span>
|
||||
<strong>Draft / Published</strong>
|
||||
</div>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-main class="admin-main">
|
||||
<el-container class="studio-content-shell">
|
||||
<header class="studio-topbar">
|
||||
<div class="project-switcher">
|
||||
<span>项目</span>
|
||||
<strong>通用 AI Agent 开发平台</strong>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<span class="run-status">运行状态正常</span>
|
||||
<el-button>版本快照</el-button>
|
||||
<el-button type="primary">发布</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<el-main class="admin-main studio-main">
|
||||
<RouterView />
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
@@ -6,16 +6,16 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import AdminLayout from '../AdminLayout.vue';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ meta: { title: '系统枚举' } }),
|
||||
useRoute: () => ({ meta: { title: '工作台' } }),
|
||||
}));
|
||||
|
||||
describe('AdminLayout', () => {
|
||||
it('does not render a duplicate page header above the main page content', () => {
|
||||
it('renders the Studio shell navigation without legacy admin groups', () => {
|
||||
const wrapper = mount(AdminLayout, {
|
||||
global: {
|
||||
plugins: [createPinia(), ElementPlus],
|
||||
mocks: {
|
||||
$route: { path: '/system/enums' },
|
||||
$route: { path: '/studio' },
|
||||
},
|
||||
stubs: {
|
||||
RouterView: { template: '<main data-test="router-view" />' },
|
||||
@@ -25,5 +25,12 @@ describe('AdminLayout', () => {
|
||||
|
||||
expect(wrapper.find('.admin-header').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-test="router-view"]').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Common Agent Studio');
|
||||
expect(wrapper.text()).toContain('知识资产');
|
||||
expect(wrapper.text()).toContain('Workflow');
|
||||
expect(wrapper.text()).toContain('观测');
|
||||
expect(wrapper.text()).not.toContain('系统管理');
|
||||
expect(wrapper.text()).not.toContain('RAG');
|
||||
expect(wrapper.text()).not.toContain('Agent管理');
|
||||
});
|
||||
});
|
||||
|
||||
270
frontend/src/pages/agent/AgentDebugPage.vue
Normal file
270
frontend/src/pages/agent/AgentDebugPage.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { chatWithAgent, listAgents, type AgentDefinition, type AgentMessage, type AgentReferenceChunk } from '@/api/agent';
|
||||
|
||||
interface ChatBubble {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
references?: AgentReferenceChunk[];
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const sending = ref(false);
|
||||
const agents = ref<AgentDefinition[]>([]);
|
||||
const selectedAgentId = ref('');
|
||||
const inputText = ref('');
|
||||
const messages = ref<ChatBubble[]>([]);
|
||||
const ragEnabled = ref(true);
|
||||
|
||||
const selectedAgent = computed(() => agents.value.find((agent) => agent.id === selectedAgentId.value));
|
||||
|
||||
async function loadAgents() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await listAgents();
|
||||
agents.value = (response.data ?? []).filter((item) => item.status === 'ENABLED');
|
||||
if (!selectedAgentId.value && agents.value.length > 0) {
|
||||
const firstAgent = agents.value[0];
|
||||
selectedAgentId.value = firstAgent && firstAgent.id ? firstAgent.id : '';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestMessages(nextUserText: string): AgentMessage[] {
|
||||
const historyMessages: AgentMessage[] = messages.value.map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
}));
|
||||
historyMessages.push({ role: 'user', content: nextUserText });
|
||||
return historyMessages;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const trimmed = inputText.value.trim();
|
||||
if (!selectedAgentId.value) {
|
||||
ElMessage.warning('请先选择Agent');
|
||||
return;
|
||||
}
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const requestMessages = buildRequestMessages(trimmed);
|
||||
const userBubble: ChatBubble = {
|
||||
id: `${Date.now()}_u`,
|
||||
role: 'user',
|
||||
content: trimmed,
|
||||
};
|
||||
messages.value.push(userBubble);
|
||||
inputText.value = '';
|
||||
|
||||
sending.value = true;
|
||||
try {
|
||||
const response = await chatWithAgent(selectedAgentId.value, {
|
||||
messages: requestMessages,
|
||||
ragEnabled: ragEnabled.value,
|
||||
});
|
||||
const result = response.data;
|
||||
messages.value.push({
|
||||
id: `${Date.now()}_a`,
|
||||
role: 'assistant',
|
||||
content: result?.answer ?? '',
|
||||
references: result?.references ?? [],
|
||||
requestId: result?.modelRequestId,
|
||||
});
|
||||
} finally {
|
||||
sending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
messages.value = [];
|
||||
}
|
||||
|
||||
onMounted(loadAgents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel agent-debug">
|
||||
<div class="page-panel__header">
|
||||
<h2>Agent 调试</h2>
|
||||
<span>Chat Debugger</span>
|
||||
</div>
|
||||
|
||||
<div class="debug-toolbar">
|
||||
<el-select v-model="selectedAgentId" class="debug-toolbar__agent" :loading="loading" placeholder="请选择Agent">
|
||||
<el-option
|
||||
v-for="item in agents"
|
||||
:key="item.id"
|
||||
:label="`${item.agentName}(${item.agentCode})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-switch v-model="ragEnabled" active-text="RAG对话" inactive-text="普通对话" />
|
||||
<el-button @click="loadAgents">刷新Agent</el-button>
|
||||
<el-button @click="clearChat">清空会话</el-button>
|
||||
</div>
|
||||
|
||||
<div class="debug-chat">
|
||||
<div v-for="bubble in messages" :key="bubble.id" class="chat-row" :class="`chat-row--${bubble.role}`">
|
||||
<div class="chat-bubble">
|
||||
<div class="chat-bubble__role">{{ bubble.role === 'user' ? '用户' : '助手' }}</div>
|
||||
<div class="chat-bubble__content">{{ bubble.content }}</div>
|
||||
<template v-if="bubble.role === 'assistant'">
|
||||
<div v-if="bubble.references?.length" class="chat-bubble__refs">
|
||||
<div class="chat-bubble__refs-title">引用切片</div>
|
||||
<ul>
|
||||
<li v-for="reference in bubble.references" :key="reference.chunkId">
|
||||
<span class="ref-meta">#{{ reference.chunkId }} · 相似度 {{ (reference.score ?? 0).toFixed(4) }}</span>
|
||||
<span>{{ reference.chunkContent }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="bubble.requestId" class="chat-bubble__request-id">requestId: {{ bubble.requestId }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="messages.length === 0" class="chat-empty">
|
||||
选择Agent后输入问题,发起对话调试。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-input">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="none"
|
||||
:disabled="sending || !selectedAgent"
|
||||
placeholder="输入问题,回车发送(Shift+Enter换行)"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
/>
|
||||
<el-button type="primary" :loading="sending" :disabled="!selectedAgent" @click="sendMessage">发送</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.debug-toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 16px 22px 12px;
|
||||
}
|
||||
|
||||
.debug-toolbar__agent {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.debug-chat {
|
||||
flex: 1;
|
||||
min-height: 420px;
|
||||
max-height: 58vh;
|
||||
padding: 12px 22px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--app-border-soft);
|
||||
border-bottom: 1px solid var(--app-border-soft);
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.chat-row {
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.chat-row--user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-row--assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
width: min(80%, 860px);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.chat-row--user .chat-bubble {
|
||||
background: #eef5ff;
|
||||
border-color: #d3e5ff;
|
||||
}
|
||||
|
||||
.chat-bubble__role {
|
||||
margin-bottom: 6px;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-bubble__content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-bubble__refs {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-bubble__refs-title {
|
||||
margin-bottom: 8px;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-bubble__refs ul {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.chat-bubble__refs li {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ref-meta {
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-bubble__request-id {
|
||||
margin-top: 8px;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-empty {
|
||||
color: var(--app-text-muted);
|
||||
text-align: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.debug-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 22px 18px;
|
||||
}
|
||||
|
||||
.debug-input .el-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
195
frontend/src/pages/agent/AgentManagePage.vue
Normal file
195
frontend/src/pages/agent/AgentManagePage.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { deleteAgent, queryAgents, saveAgent, type AgentDefinition } from '@/api/agent';
|
||||
import { listRagStores, type RagStore } from '@/api/ragStores';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const agents = ref<AgentDefinition[]>([]);
|
||||
const stores = ref<RagStore[]>([]);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 'ENABLED' },
|
||||
{ label: '禁用', value: 'DISABLED' },
|
||||
];
|
||||
|
||||
const editForm = reactive<AgentDefinition>({
|
||||
agentCode: '',
|
||||
agentName: '',
|
||||
systemPrompt: '',
|
||||
storeId: '',
|
||||
status: 'ENABLED',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (editForm.id ? '编辑Agent' : '新增Agent'));
|
||||
|
||||
function resetForm(row?: AgentDefinition) {
|
||||
editForm.id = row?.id;
|
||||
editForm.agentCode = row?.agentCode ?? '';
|
||||
editForm.agentName = row?.agentName ?? '';
|
||||
editForm.systemPrompt = row?.systemPrompt ?? '';
|
||||
editForm.storeId = row?.storeId ?? stores.value[0]?.id ?? '';
|
||||
editForm.status = row?.status ?? 'ENABLED';
|
||||
editForm.remark = row?.remark ?? '';
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
const response = await listRagStores();
|
||||
stores.value = response.data ?? [];
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryAgents();
|
||||
agents.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: AgentDefinition) {
|
||||
resetForm(row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitAgent() {
|
||||
if (!editForm.agentCode || !editForm.agentName || !editForm.storeId) {
|
||||
ElMessage.warning('请填写Agent编码、名称和绑定知识库');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveAgent({ ...editForm });
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await loadAgents();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAgent(row: AgentDefinition) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除Agent「${row.agentName || row.agentCode}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
await deleteAgent(row.id);
|
||||
ElMessage.success('已删除');
|
||||
await loadAgents();
|
||||
}
|
||||
|
||||
function storeLabel(storeId?: string) {
|
||||
const store = stores.value.find((item) => item.id === storeId);
|
||||
return store?.storeName ?? store?.storeCode ?? storeId ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadStores();
|
||||
resetForm();
|
||||
await loadAgents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>Agent 管理</h2>
|
||||
<span>Agent Config</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__actions">
|
||||
<el-button :icon="RefreshRight" @click="loadAgents">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增Agent</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="agents" row-key="id">
|
||||
<el-table-column prop="agentCode" label="Agent编码" min-width="140" />
|
||||
<el-table-column prop="agentName" label="Agent名称" min-width="140" />
|
||||
<el-table-column label="知识库" min-width="140">
|
||||
<template #default="{ row }">{{ storeLabel(row.storeId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ENABLED' ? 'success' : 'info'">
|
||||
{{ row.status === 'ENABLED' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="systemPrompt" label="系统提示词" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="removeAgent(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="720px">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Agent编码" required>
|
||||
<el-input v-model="editForm.agentCode" placeholder="如 AGENT_RAG_HELPER" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Agent名称" required>
|
||||
<el-input v-model="editForm.agentName" placeholder="如 知识问答助手" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定知识库" required>
|
||||
<el-select v-model="editForm.storeId">
|
||||
<el-option
|
||||
v-for="store in stores"
|
||||
:key="store.id"
|
||||
:label="`${store.storeName}(${store.storeCode})`"
|
||||
:value="store.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统提示词">
|
||||
<el-input v-model="editForm.systemPrompt" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="editForm.status">
|
||||
<el-radio-button v-for="item in statusOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitAgent">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main class="not-found">
|
||||
<h1>404</h1>
|
||||
<RouterLink to="/rag/workbench">返回RAG工作台</RouterLink>
|
||||
<RouterLink to="/studio">返回工作台</RouterLink>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
69
frontend/src/pages/studio/AgentWorkspacePage.vue
Normal file
69
frontend/src/pages/studio/AgentWorkspacePage.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ChatDotRound, Coin, Timer } from '@element-plus/icons-vue';
|
||||
|
||||
import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page agent-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">AgentWorkspaceView</p>
|
||||
<h1>Agent 对话调试</h1>
|
||||
</div>
|
||||
<el-button type="primary">发布 Agent</el-button>
|
||||
</header>
|
||||
|
||||
<div class="agent-layout">
|
||||
<section class="studio-panel chat-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>售前问答 Agent</h2>
|
||||
<span>POST /api/agents/1001/runs</span>
|
||||
</div>
|
||||
<el-tag>Draft</el-tag>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<article v-for="message in chatMessages" :key="message.content" :class="message.role">
|
||||
<strong>{{ message.role === 'user' ? '用户' : 'Agent' }}</strong>
|
||||
<p>{{ message.content }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="chat-composer">
|
||||
<span>输入调试问题,运行会写入 agent_session / agent_message 草案</span>
|
||||
<el-button type="primary"><el-icon><ChatDotRound /></el-icon> 发送</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel citation-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>引用切片</h2>
|
||||
<span>3 个来源</span>
|
||||
</div>
|
||||
<article v-for="citation in citations" :key="citation.title" class="citation-card">
|
||||
<strong>{{ citation.title }}</strong>
|
||||
<el-tag type="success">score {{ citation.score }}</el-tag>
|
||||
<p>{{ citation.text }}</p>
|
||||
</article>
|
||||
</aside>
|
||||
|
||||
<aside class="studio-panel run-inspector">
|
||||
<div class="panel-heading compact">
|
||||
<h2>运行追踪</h2>
|
||||
<span>modelRequestId: f4215d</span>
|
||||
</div>
|
||||
<div class="metric-mini">
|
||||
<span><el-icon><Timer /></el-icon> 1.42s</span>
|
||||
<span><el-icon><Coin /></el-icon> ¥0.018</span>
|
||||
<span>1,248 tokens</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li v-for="step in traceSteps" :key="step.node">
|
||||
<time>{{ step.duration }}</time>
|
||||
<span>{{ step.node }} · {{ step.output }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
71
frontend/src/pages/studio/IngestionPipelinePage.vue
Normal file
71
frontend/src/pages/studio/IngestionPipelinePage.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
import { ingestionSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page ingestion-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">IngestionPipelineView</p>
|
||||
<h1>文件解析管道</h1>
|
||||
</div>
|
||||
<el-button type="primary">启动索引任务</el-button>
|
||||
</header>
|
||||
|
||||
<div class="ingestion-layout">
|
||||
<section class="studio-panel upload-panel">
|
||||
<div class="upload-dropzone">
|
||||
<el-icon><UploadFilled /></el-icon>
|
||||
<strong>拖拽文件到这里</strong>
|
||||
<span>支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。</span>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
</div>
|
||||
<div class="pipeline-timeline">
|
||||
<article v-for="step in ingestionSteps" :key="step.name" :class="`is-${step.status}`">
|
||||
<div class="timeline-dot" />
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.description }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel preview-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>解析与切片预览</h2>
|
||||
<span>GET /api/knowledge/ingestion-runs/run-20260531</span>
|
||||
</div>
|
||||
<div class="preview-split">
|
||||
<article>
|
||||
<h3>解析文本</h3>
|
||||
<p>私有化部署章节应覆盖基础设施、网络、安全与运维边界。平台需说明模型服务商、知识库索引策略与日志留存周期...</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>切片 #24</h3>
|
||||
<p>chunk_size=800, overlap=120, strategy=FIXED_LENGTH。该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="pipeline-controls">
|
||||
<label>切片策略 <strong>固定长度</strong></label>
|
||||
<label>Chunk Size <strong>800</strong></label>
|
||||
<label>Overlap <strong>120</strong></label>
|
||||
<label>Embedding <strong>Qwen3 1024d</strong></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel task-log-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>任务日志</h2>
|
||||
<span>run-20260531</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li><time>23:08:12</time><span>上传 4 个文件并创建 rag_document</span></li>
|
||||
<li><time>23:08:24</time><span>Tika 解析完成 3 个文件</span></li>
|
||||
<li class="warn"><time>23:08:31</time><span>服务条款更新.md 编码检测失败,等待重试</span></li>
|
||||
<li><time>23:08:40</time><span>切片任务进行中 68 / 119</span></li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
94
frontend/src/pages/studio/KnowledgeWorkspacePage.vue
Normal file
94
frontend/src/pages/studio/KnowledgeWorkspacePage.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { DataAnalysis, Document, Setting } from '@element-plus/icons-vue';
|
||||
|
||||
import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page workspace-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">KnowledgeWorkspaceView</p>
|
||||
<h1>知识资产</h1>
|
||||
</div>
|
||||
<el-button type="primary">新建知识库</el-button>
|
||||
</header>
|
||||
|
||||
<div class="three-column-layout">
|
||||
<aside class="studio-panel collection-rail">
|
||||
<div class="panel-heading compact">
|
||||
<h2>知识集合</h2>
|
||||
<span>{{ knowledgeStores.length }} 个库</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="store in knowledgeStores"
|
||||
:key="store.id"
|
||||
class="collection-item"
|
||||
:class="{ active: store.id === '1001' }"
|
||||
>
|
||||
<strong>{{ store.name }}</strong>
|
||||
<span>{{ store.docs }} 文档 · 健康度 {{ store.health }}%</span>
|
||||
<em>{{ store.status }}</em>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel knowledge-main">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>产品制度库</h2>
|
||||
<span>绑定旧数据语义:rag_store / rag_document / rag_chunk_embedding</span>
|
||||
</div>
|
||||
<el-tag type="success">可检索</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="config-grid">
|
||||
<article>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<strong>Embedding 模型</strong>
|
||||
<span>Qwen3-Embedding · 1024 维</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<strong>检索配置</strong>
|
||||
<span>TopK 6 · Score ≥ 0.72 · Rerank 关闭</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><Document /></el-icon>
|
||||
<strong>索引版本</strong>
|
||||
<span>index_version 14 · Draft 快照</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>文档</span><span>解析</span><span>索引</span><span>切片</span><span>更新</span>
|
||||
</div>
|
||||
<div v-for="doc in knowledgeDocuments" :key="doc.id" class="table-row">
|
||||
<strong>{{ doc.name }}</strong>
|
||||
<span>{{ doc.parseStatus }}</span>
|
||||
<span>{{ doc.indexStatus }}</span>
|
||||
<span>{{ doc.chunks }}</span>
|
||||
<span>{{ doc.updatedAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="studio-panel inspector-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>状态 Inspector</h2>
|
||||
<span>聚合接口</span>
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>Workspace API</dt>
|
||||
<dd>GET /api/knowledge/workspaces/1001</dd>
|
||||
<dt>文档健康度</dt>
|
||||
<dd>96% · 1 个解析失败</dd>
|
||||
<dt>待处理任务</dt>
|
||||
<dd>2 个文档等待向量化</dd>
|
||||
<dt>发布影响</dt>
|
||||
<dd>更新后需要 Workflow 重新验证引用质量</dd>
|
||||
</dl>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
49
frontend/src/pages/studio/McpImportPage.vue
Normal file
49
frontend/src/pages/studio/McpImportPage.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, Upload } from '@element-plus/icons-vue';
|
||||
|
||||
import { mcpCapabilities } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page mcp-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">McpImportView</p>
|
||||
<h1>MCP 导入</h1>
|
||||
</div>
|
||||
<el-button type="primary"><el-icon><Upload /></el-icon> 导入 Server</el-button>
|
||||
</header>
|
||||
|
||||
<div class="mcp-layout">
|
||||
<section class="studio-panel import-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>外部能力接入</h2>
|
||||
<span>POST /api/mcp/import</span>
|
||||
</div>
|
||||
<div class="import-options">
|
||||
<button class="active"><el-icon><Link /></el-icon><strong>URL</strong><span>https://mcp.example.com/sse</span></button>
|
||||
<button><strong>npm package</strong><span>@acme/mcp-jira</span></button>
|
||||
<button><strong>JSON Manifest</strong><span>粘贴 server 能力声明</span></button>
|
||||
</div>
|
||||
<div class="manifest-box">
|
||||
<span>{ "server": "jira", "transport": "sse", "auth": "oauth2" }</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel capability-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>能力预览</h2>
|
||||
<span>GET /api/mcp/servers/jira/capabilities</span>
|
||||
</div>
|
||||
<div class="capability-grid">
|
||||
<article v-for="item in mcpCapabilities" :key="item.name">
|
||||
<el-tag>{{ item.type }}</el-tag>
|
||||
<strong>{{ item.name }}</strong>
|
||||
<p>{{ item.description }}</p>
|
||||
<span>{{ item.status }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
34
frontend/src/pages/studio/ModelWorkspacePage.vue
Normal file
34
frontend/src/pages/studio/ModelWorkspacePage.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { modelRoutes } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page model-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">ModelRoutingView</p>
|
||||
<h1>模型与路由</h1>
|
||||
</div>
|
||||
<el-button type="primary">新增路由</el-button>
|
||||
</header>
|
||||
|
||||
<div class="studio-panel model-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>任务路由规则</h2>
|
||||
<span>保留 model_provider / model_config / model_route_rule 语义</span>
|
||||
</div>
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>任务</span><span>主模型</span><span>Fallback</span><span>最大延迟</span><span>状态</span>
|
||||
</div>
|
||||
<div v-for="route in modelRoutes" :key="route.task" class="table-row">
|
||||
<strong>{{ route.task }}</strong>
|
||||
<span>{{ route.primary }}</span>
|
||||
<span>{{ route.fallback }}</span>
|
||||
<span>{{ route.latency }}</span>
|
||||
<el-tag :type="route.status === '启用' ? 'success' : 'warning'">{{ route.status }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
49
frontend/src/pages/studio/ObservabilityPage.vue
Normal file
49
frontend/src/pages/studio/ObservabilityPage.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { recentRuns, traceSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page observability-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">ObservabilityView</p>
|
||||
<h1>运行观测</h1>
|
||||
</div>
|
||||
<el-button>导出日志</el-button>
|
||||
</header>
|
||||
|
||||
<div class="observability-layout">
|
||||
<section class="studio-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>运行记录</h2>
|
||||
<span>workflow_run / workflow_run_step / model_call_log</span>
|
||||
</div>
|
||||
<div class="run-table">
|
||||
<div class="run-row run-head">
|
||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
||||
<strong>{{ run.name }}</strong>
|
||||
<span>{{ run.type }}</span>
|
||||
<el-tag :type="run.status === '成功' ? 'success' : 'warning'">{{ run.status }}</el-tag>
|
||||
<span>{{ run.latency }}</span>
|
||||
<span>{{ run.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>步骤日志</h2>
|
||||
<span>run-1842</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li v-for="step in traceSteps" :key="step.node">
|
||||
<time>{{ step.duration }}</time>
|
||||
<span>{{ step.node }} · {{ step.status }} · {{ step.output }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
56
frontend/src/pages/studio/SkillWorkspacePage.vue
Normal file
56
frontend/src/pages/studio/SkillWorkspacePage.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { skillVersions } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page skill-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">SkillWorkspaceView</p>
|
||||
<h1>Skill 编辑与使用</h1>
|
||||
</div>
|
||||
<el-button type="primary">测试 Skill</el-button>
|
||||
</header>
|
||||
|
||||
<div class="skill-layout">
|
||||
<section class="studio-panel skill-editor">
|
||||
<div class="panel-heading">
|
||||
<h2>引用审校 Skill</h2>
|
||||
<span>PUT /api/skills/skill-citation/draft</span>
|
||||
</div>
|
||||
<div class="editor-tabs">
|
||||
<button class="active">Prompt</button>
|
||||
<button>Code</button>
|
||||
<button>Config</button>
|
||||
</div>
|
||||
<pre class="prompt-editor">你是回答审校器。请检查答案是否完整引用知识库切片,并输出:
|
||||
1. answer_quality
|
||||
2. missing_citations
|
||||
3. rewrite_suggestion</pre>
|
||||
<div class="variable-grid">
|
||||
<label>变量 <strong>answer</strong></label>
|
||||
<label>变量 <strong>citations[]</strong></label>
|
||||
<label>输出 <strong>quality_score</strong></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel test-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>测试面板</h2>
|
||||
<span>POST /api/skills/skill-citation/test</span>
|
||||
</div>
|
||||
<div class="test-result">
|
||||
<strong>quality_score: 0.86</strong>
|
||||
<p>建议补充“日志留存周期”的引用来源,并将私有化部署边界写得更明确。</p>
|
||||
</div>
|
||||
<div class="version-list">
|
||||
<article v-for="version in skillVersions" :key="version.version">
|
||||
<strong>{{ version.version }}</strong>
|
||||
<span>{{ version.status }}</span>
|
||||
<em>{{ version.note }} · {{ version.updatedAt }}</em>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
95
frontend/src/pages/studio/StudioDashboardPage.vue
Normal file
95
frontend/src/pages/studio/StudioDashboardPage.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowRight, Check, Warning } from '@element-plus/icons-vue';
|
||||
|
||||
import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page dashboard-page">
|
||||
<header class="studio-hero">
|
||||
<div>
|
||||
<p class="studio-kicker">项目 / Common Agent Studio</p>
|
||||
<h1>从知识接入到 Agent 发布的一体化工作台</h1>
|
||||
<p>
|
||||
使用新的聚合 ViewModel 驱动原型:知识资产、Workflow、MCP、Skill、Agent 调试与观测都围绕一次发布旅程组织。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-button type="primary">新建 Workflow</el-button>
|
||||
<el-button>导入 MCP</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="lifecycle-strip">
|
||||
<article v-for="(step, index) in lifecycleSteps" :key="step.name" class="lifecycle-step" :class="`is-${step.status}`">
|
||||
<div class="step-index">{{ index + 1 }}</div>
|
||||
<div>
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.description }}</span>
|
||||
</div>
|
||||
<el-icon v-if="index < lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="studio-panel readiness-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>发布就绪检查</h2>
|
||||
<span>ViewModel: StudioDashboardView</span>
|
||||
</div>
|
||||
<el-tag type="warning">Draft</el-tag>
|
||||
</div>
|
||||
<ul class="check-list">
|
||||
<li v-for="item in readinessChecklist" :key="item.label" :class="{ done: item.done }">
|
||||
<el-icon>
|
||||
<Check v-if="item.done" />
|
||||
<span v-else class="pending-dot" />
|
||||
</el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel metrics-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>运行概览</h2>
|
||||
<span>环境: Dev</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<div><strong>27</strong><span>今日运行</span></div>
|
||||
<div><strong>96.4%</strong><span>成功率</span></div>
|
||||
<div><strong>1.28s</strong><span>P50 延迟</span></div>
|
||||
<div><strong>¥4.82</strong><span>预估成本</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel recent-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>最近运行</h2>
|
||||
<span>Run Trace</span>
|
||||
</div>
|
||||
<div class="run-table">
|
||||
<div class="run-row run-head">
|
||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
||||
<strong>{{ run.name }}</strong>
|
||||
<span>{{ run.type }}</span>
|
||||
<el-tag :type="run.status === '成功' ? 'success' : 'warning'">{{ run.status }}</el-tag>
|
||||
<span>{{ run.latency }}</span>
|
||||
<span>{{ run.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel warning-panel">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<div>
|
||||
<h2>生产发布前仍需确认路由兜底</h2>
|
||||
<p>AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
112
frontend/src/pages/studio/WorkflowBuilderPage.vue
Normal file
112
frontend/src/pages/studio/WorkflowBuilderPage.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { Connection, Cpu, VideoPlay } from '@element-plus/icons-vue';
|
||||
|
||||
import { traceSteps, workflowEdges, workflowNodes } from '@/data/studioMock';
|
||||
|
||||
const nodeById = Object.fromEntries(workflowNodes.map((node) => [node.id, node]));
|
||||
const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
const from = nodeById[edge.from];
|
||||
const to = nodeById[edge.to];
|
||||
|
||||
if (!from || !to) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${edge.from}-${edge.to}`,
|
||||
x1: from.x + 5,
|
||||
y1: from.y + 4,
|
||||
x2: to.x,
|
||||
y2: to.y + 4,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page workflow-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">WorkflowBuilderView · Draft / Published</p>
|
||||
<h1>Workflow 图形化编排</h1>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button>保存草稿</el-button>
|
||||
<el-button type="primary"><el-icon><VideoPlay /></el-icon> 运行测试</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workflow-layout">
|
||||
<aside class="studio-panel node-library">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点库</h2>
|
||||
<span>JSON Graph</span>
|
||||
</div>
|
||||
<button>Start</button>
|
||||
<button>LLM</button>
|
||||
<button>Knowledge Retrieval</button>
|
||||
<button>MCP Tool</button>
|
||||
<button>Skill</button>
|
||||
<button>Condition</button>
|
||||
<button>Answer</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel workflow-canvas">
|
||||
<div class="canvas-toolbar">
|
||||
<span><el-icon><Connection /></el-icon> workflow-support-rag</span>
|
||||
<span>版本快照 v7</span>
|
||||
<span>环境: Dev</span>
|
||||
</div>
|
||||
<div class="canvas-surface">
|
||||
<svg class="edge-layer" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<line
|
||||
v-for="edge in canvasEdges"
|
||||
:key="edge.id"
|
||||
:x1="edge.x1"
|
||||
:y1="edge.y1"
|
||||
:x2="edge.x2"
|
||||
:y2="edge.y2"
|
||||
/>
|
||||
</svg>
|
||||
<article
|
||||
v-for="node in workflowNodes"
|
||||
:key="node.id"
|
||||
class="workflow-node"
|
||||
:class="{ selected: node.id === 'llm' }"
|
||||
:style="{ left: `${node.x}%`, top: `${node.y}%` }"
|
||||
>
|
||||
<span>{{ node.type }}</span>
|
||||
<strong>{{ node.label }}</strong>
|
||||
<em>{{ node.description }}</em>
|
||||
</article>
|
||||
</div>
|
||||
<div class="run-trace-drawer">
|
||||
<strong>Run Trace</strong>
|
||||
<div v-for="step in traceSteps" :key="step.node">
|
||||
<span>{{ step.node }}</span>
|
||||
<em>{{ step.status }} · {{ step.duration }} · {{ step.output }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="studio-panel inspector-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点 Inspector</h2>
|
||||
<span>LLM</span>
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>任务类型</dt>
|
||||
<dd>RAG_ANSWER</dd>
|
||||
<dt>输入 Schema</dt>
|
||||
<dd>question, retrieved_chunks, conversation</dd>
|
||||
<dt>输出 Schema</dt>
|
||||
<dd>answer, citations, safety_flags</dd>
|
||||
<dt>路由策略</dt>
|
||||
<dd>primary qwen-plus / fallback deepseek-v3</dd>
|
||||
</dl>
|
||||
<button class="blue-command"><el-icon><Cpu /></el-icon> 打开模型路由</button>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -3,19 +3,19 @@ import { describe, expect, it } from 'vitest';
|
||||
import { routes } from '../index';
|
||||
|
||||
describe('router', () => {
|
||||
it('defines the admin shell routes', () => {
|
||||
it('defines the Studio product routes', () => {
|
||||
const paths = routes.map((route) => route.path);
|
||||
|
||||
expect(paths).toContain('/');
|
||||
expect(paths).toContain('/rag/stores');
|
||||
expect(paths).toContain('/rag/workbench');
|
||||
expect(paths).toContain('/rag/documents');
|
||||
expect(paths).toContain('/rag/tasks/chunk');
|
||||
expect(paths).toContain('/system/enums');
|
||||
expect(paths).toContain('/system/model/providers');
|
||||
expect(paths).toContain('/system/model/configs');
|
||||
expect(paths).toContain('/system/model/routes');
|
||||
expect(paths).toContain('/system/model/call-logs');
|
||||
expect(paths).toContain('/studio');
|
||||
expect(paths).toContain('/knowledge');
|
||||
expect(paths).toContain('/knowledge/ingestion');
|
||||
expect(paths).toContain('/workflows');
|
||||
expect(paths).toContain('/agents');
|
||||
expect(paths).toContain('/mcp');
|
||||
expect(paths).toContain('/skills');
|
||||
expect(paths).toContain('/models');
|
||||
expect(paths).toContain('/observability');
|
||||
expect(paths).toContain('/:pathMatch(.*)*');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import NotFoundPage from '@/pages/common/NotFoundPage.vue';
|
||||
import RagDocumentsPage from '@/pages/rag/RagDocumentsPage.vue';
|
||||
import RagStoresPage from '@/pages/rag/RagStoresPage.vue';
|
||||
import RagChunkTasksPage from '@/pages/rag/tasks/RagChunkTasksPage.vue';
|
||||
import RagWorkbenchPage from '@/pages/rag/workbench/RagWorkbenchPage.vue';
|
||||
import SystemEnumsPage from '@/pages/system/SystemEnumsPage.vue';
|
||||
import ModelProvidersPage from '@/pages/system/ModelProvidersPage.vue';
|
||||
import ModelConfigsPage from '@/pages/system/ModelConfigsPage.vue';
|
||||
import ModelRouteRulesPage from '@/pages/system/ModelRouteRulesPage.vue';
|
||||
import ModelCallLogsPage from '@/pages/system/ModelCallLogsPage.vue';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import NotFoundPage from '@/pages/common/NotFoundPage.vue';
|
||||
import AgentWorkspacePage from '@/pages/studio/AgentWorkspacePage.vue';
|
||||
import IngestionPipelinePage from '@/pages/studio/IngestionPipelinePage.vue';
|
||||
import KnowledgeWorkspacePage from '@/pages/studio/KnowledgeWorkspacePage.vue';
|
||||
import McpImportPage from '@/pages/studio/McpImportPage.vue';
|
||||
import ModelWorkspacePage from '@/pages/studio/ModelWorkspacePage.vue';
|
||||
import ObservabilityPage from '@/pages/studio/ObservabilityPage.vue';
|
||||
import SkillWorkspacePage from '@/pages/studio/SkillWorkspacePage.vue';
|
||||
import StudioDashboardPage from '@/pages/studio/StudioDashboardPage.vue';
|
||||
import WorkflowBuilderPage from '@/pages/studio/WorkflowBuilderPage.vue';
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/rag/workbench',
|
||||
redirect: '/studio',
|
||||
},
|
||||
{
|
||||
path: '/system/enums',
|
||||
name: 'system-enums',
|
||||
component: SystemEnumsPage,
|
||||
meta: { title: '系统枚举' },
|
||||
path: '/studio',
|
||||
name: 'studio-dashboard',
|
||||
component: StudioDashboardPage,
|
||||
meta: { title: '工作台' },
|
||||
},
|
||||
{
|
||||
path: '/system/model/providers',
|
||||
name: 'system-model-providers',
|
||||
component: ModelProvidersPage,
|
||||
meta: { title: '模型服务商' },
|
||||
path: '/knowledge',
|
||||
name: 'knowledge-workspace',
|
||||
component: KnowledgeWorkspacePage,
|
||||
meta: { title: '知识资产' },
|
||||
},
|
||||
{
|
||||
path: '/system/model/configs',
|
||||
name: 'system-model-configs',
|
||||
component: ModelConfigsPage,
|
||||
meta: { title: '模型配置' },
|
||||
path: '/knowledge/ingestion',
|
||||
name: 'knowledge-ingestion',
|
||||
component: IngestionPipelinePage,
|
||||
meta: { title: '文件解析' },
|
||||
},
|
||||
{
|
||||
path: '/system/model/routes',
|
||||
name: 'system-model-routes',
|
||||
component: ModelRouteRulesPage,
|
||||
meta: { title: '路由规则' },
|
||||
path: '/workflows',
|
||||
name: 'workflow-builder',
|
||||
component: WorkflowBuilderPage,
|
||||
meta: { title: 'Workflow' },
|
||||
},
|
||||
{
|
||||
path: '/system/model/call-logs',
|
||||
name: 'system-model-call-logs',
|
||||
component: ModelCallLogsPage,
|
||||
meta: { title: '调用日志' },
|
||||
path: '/agents',
|
||||
name: 'agent-workspace',
|
||||
component: AgentWorkspacePage,
|
||||
meta: { title: 'Agent 对话' },
|
||||
},
|
||||
{
|
||||
path: '/rag/stores',
|
||||
name: 'rag-stores',
|
||||
component: RagStoresPage,
|
||||
meta: { title: '知识库' },
|
||||
path: '/mcp',
|
||||
name: 'mcp-import',
|
||||
component: McpImportPage,
|
||||
meta: { title: 'MCP 导入' },
|
||||
},
|
||||
{
|
||||
path: '/rag/workbench',
|
||||
name: 'rag-workbench',
|
||||
component: RagWorkbenchPage,
|
||||
meta: { title: 'RAG工作台' },
|
||||
path: '/skills',
|
||||
name: 'skill-workspace',
|
||||
component: SkillWorkspacePage,
|
||||
meta: { title: 'Skill 编辑' },
|
||||
},
|
||||
{
|
||||
path: '/rag/documents',
|
||||
name: 'rag-documents',
|
||||
component: RagDocumentsPage,
|
||||
meta: { title: '知识文档' },
|
||||
path: '/models',
|
||||
name: 'model-workspace',
|
||||
component: ModelWorkspacePage,
|
||||
meta: { title: '模型路由' },
|
||||
},
|
||||
{
|
||||
path: '/rag/tasks/chunk',
|
||||
name: 'rag-chunk-tasks',
|
||||
component: RagChunkTasksPage,
|
||||
meta: { title: '切片任务' },
|
||||
path: '/observability',
|
||||
name: 'observability',
|
||||
component: ObservabilityPage,
|
||||
meta: { title: '观测' },
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@@ -81,73 +81,14 @@ export const routes: RouteRecordRaw[] = [
|
||||
];
|
||||
|
||||
const routerRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/rag/workbench',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'system/enums',
|
||||
name: 'system-enums',
|
||||
component: SystemEnumsPage,
|
||||
meta: { title: '系统枚举' },
|
||||
},
|
||||
{
|
||||
path: 'system/model/providers',
|
||||
name: 'system-model-providers',
|
||||
component: ModelProvidersPage,
|
||||
meta: { title: '模型服务商' },
|
||||
},
|
||||
{
|
||||
path: 'system/model/configs',
|
||||
name: 'system-model-configs',
|
||||
component: ModelConfigsPage,
|
||||
meta: { title: '模型配置' },
|
||||
},
|
||||
{
|
||||
path: 'system/model/routes',
|
||||
name: 'system-model-routes',
|
||||
component: ModelRouteRulesPage,
|
||||
meta: { title: '路由规则' },
|
||||
},
|
||||
{
|
||||
path: 'system/model/call-logs',
|
||||
name: 'system-model-call-logs',
|
||||
component: ModelCallLogsPage,
|
||||
meta: { title: '调用日志' },
|
||||
},
|
||||
{
|
||||
path: 'rag/stores',
|
||||
name: 'rag-stores',
|
||||
component: RagStoresPage,
|
||||
meta: { title: '知识库' },
|
||||
},
|
||||
{
|
||||
path: 'rag/workbench',
|
||||
name: 'rag-workbench',
|
||||
component: RagWorkbenchPage,
|
||||
meta: { title: 'RAG工作台' },
|
||||
},
|
||||
{
|
||||
path: 'rag/documents',
|
||||
name: 'rag-documents',
|
||||
component: RagDocumentsPage,
|
||||
meta: { title: '知识文档' },
|
||||
},
|
||||
{
|
||||
path: 'rag/tasks/chunk',
|
||||
name: 'rag-chunk-tasks',
|
||||
component: RagChunkTasksPage,
|
||||
meta: { title: '切片任务' },
|
||||
},
|
||||
],
|
||||
children: routes.filter((route) => route.path !== '/:pathMatch(.*)*'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
name: 'not-found-shell',
|
||||
component: NotFoundPage,
|
||||
meta: { title: '页面不存在' },
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -436,3 +436,14 @@
|
||||
- 模型服务商与 Embedding 模型配置
|
||||
- 检索配置
|
||||
- 检索测试/召回预览
|
||||
|
||||
## 7. 与 Agent 调试链路的关联
|
||||
|
||||
当前 RAG 切片与向量数据已经被 Agent 调试页直接消费:
|
||||
|
||||
- Agent 调试接口 `POST /api/agents/{agentId}/chat` 在 `ragEnabled=true` 时会读取 `rag_chunk_embedding` 进行 TopK 召回。
|
||||
- 若未完成切片向量化,Agent 调试会返回“未召回到可用知识切片”。
|
||||
|
||||
关联文档:
|
||||
|
||||
- [Agent 页面后端接口清单](agent-page-apis.md)
|
||||
|
||||
35
script/sql/agent_definition.sql
Normal file
35
script/sql/agent_definition.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
DROP TABLE IF EXISTS agent_definition;
|
||||
|
||||
CREATE TABLE agent_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
agent_code VARCHAR(100) NOT NULL,
|
||||
agent_name VARCHAR(200) NOT NULL,
|
||||
system_prompt TEXT,
|
||||
store_id BIGINT NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ENABLED',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_definition_code UNIQUE (agent_code),
|
||||
CONSTRAINT fk_agent_definition_store_id FOREIGN KEY (store_id) REFERENCES rag_store (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_agent_definition_store_id ON agent_definition (store_id);
|
||||
CREATE INDEX idx_agent_definition_status ON agent_definition (status);
|
||||
|
||||
COMMENT ON TABLE agent_definition IS 'Agent定义表';
|
||||
COMMENT ON COLUMN agent_definition.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_definition.agent_code IS 'Agent编码';
|
||||
COMMENT ON COLUMN agent_definition.agent_name IS 'Agent名称';
|
||||
COMMENT ON COLUMN agent_definition.system_prompt IS '系统提示词';
|
||||
COMMENT ON COLUMN agent_definition.store_id IS '绑定知识库ID';
|
||||
COMMENT ON COLUMN agent_definition.status IS '状态';
|
||||
COMMENT ON COLUMN agent_definition.version IS '版本';
|
||||
COMMENT ON COLUMN agent_definition.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_definition.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_definition.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_definition.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_definition.update_by IS '更新者';
|
||||
20
script/sql/model_call_log_patch.sql
Normal file
20
script/sql/model_call_log_patch.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- model_call_log 补丁脚本
|
||||
-- 目的:对齐 BaseEntity 字段,避免 MyBatis 查询 create_by / update_by / update_time / version 报错
|
||||
|
||||
ALTER TABLE model_call_log
|
||||
ADD COLUMN IF NOT EXISTS create_by VARCHAR(64);
|
||||
|
||||
ALTER TABLE model_call_log
|
||||
ADD COLUMN IF NOT EXISTS update_by VARCHAR(64);
|
||||
|
||||
ALTER TABLE model_call_log
|
||||
ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
|
||||
|
||||
ALTER TABLE model_call_log
|
||||
ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
COMMENT ON COLUMN model_call_log.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_call_log.update_by IS '更新者';
|
||||
COMMENT ON COLUMN model_call_log.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_call_log.version IS '版本';
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.bruce.agent.controller;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/agents")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentDefinitionController {
|
||||
|
||||
private final IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@PostMapping("/list")
|
||||
public RequestResult<List<AgentDefinitionResponse>> list() {
|
||||
return RequestResult.success(agentDefinitionService.listResponses());
|
||||
}
|
||||
|
||||
@PostMapping("/query")
|
||||
public RequestResult<List<AgentDefinitionResponse>> query(@RequestBody(required = false) AgentDefinitionQueryRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.query(request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
public RequestResult<AgentDefinitionResponse> detail(@RequestParam("id") Long id) {
|
||||
return RequestResult.success(agentDefinitionService.getResponseById(id));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
public RequestResult<Boolean> save(@RequestBody AgentDefinitionSaveRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.saveOrUpdate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||
return RequestResult.success(agentDefinitionService.removeById(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{agentId}/chat")
|
||||
public RequestResult<AgentChatResponse> chat(@PathVariable("agentId") Long agentId,
|
||||
@RequestBody AgentChatRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.chat(agentId, request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AgentChatRequest {
|
||||
private List<AgentMessage> messages;
|
||||
private Boolean ragEnabled;
|
||||
|
||||
@Data
|
||||
public static class AgentMessage {
|
||||
private String role;
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionQueryRequest {
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String status;
|
||||
private Long storeId;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionSaveRequest {
|
||||
private Long id;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String systemPrompt;
|
||||
private Long storeId;
|
||||
private String status;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bruce.agent.dto.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AgentChatResponse {
|
||||
private Long agentId;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String answer;
|
||||
private String modelRequestId;
|
||||
private List<ReferenceChunk> references;
|
||||
|
||||
@Data
|
||||
public static class ReferenceChunk {
|
||||
private Long chunkId;
|
||||
private Long documentId;
|
||||
private String chunkContent;
|
||||
private Double score;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bruce.agent.dto.response;
|
||||
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionResponse {
|
||||
private Long id;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String systemPrompt;
|
||||
private Long storeId;
|
||||
private String status;
|
||||
private String remark;
|
||||
|
||||
public static AgentDefinitionResponse fromEntity(AgentDefinition entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
AgentDefinitionResponse response = new AgentDefinitionResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/bruce/agent/entity/AgentDefinition.java
Normal file
29
src/main/java/com/bruce/agent/entity/AgentDefinition.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.bruce.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("agent_definition")
|
||||
public class AgentDefinition extends BaseEntity {
|
||||
|
||||
@TableField("agent_code")
|
||||
private String agentCode;
|
||||
|
||||
@TableField("agent_name")
|
||||
private String agentName;
|
||||
|
||||
@TableField("system_prompt")
|
||||
private String systemPrompt;
|
||||
|
||||
@TableField("store_id")
|
||||
private Long storeId;
|
||||
|
||||
private String status;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.agent.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AgentDefinitionMapper extends BaseMapper<AgentDefinition> {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.bruce.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAgentDefinitionService extends IService<AgentDefinition> {
|
||||
List<AgentDefinitionResponse> listResponses();
|
||||
|
||||
List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request);
|
||||
|
||||
AgentDefinitionResponse getResponseById(Long id);
|
||||
|
||||
boolean saveOrUpdate(AgentDefinitionSaveRequest request);
|
||||
|
||||
AgentChatResponse chat(Long agentId, AgentChatRequest request);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package com.bruce.agent.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.mapper.AgentDefinitionMapper;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import com.bruce.modelprovider.client.OpenAiChatMessage;
|
||||
import com.bruce.modelprovider.entity.RagStoreModelConfig;
|
||||
import com.bruce.modelprovider.gateway.ChatModelGateway;
|
||||
import com.bruce.modelprovider.gateway.ChatRequest;
|
||||
import com.bruce.modelprovider.gateway.ChatResult;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingModelGateway;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingRequest;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingResult;
|
||||
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
|
||||
import com.bruce.rag.dto.response.RagChunkRecallResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.mapper.RagChunkEmbeddingMapper;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMapper, AgentDefinition>
|
||||
implements IAgentDefinitionService {
|
||||
|
||||
private static final int DEFAULT_TOP_K = 5;
|
||||
|
||||
private final IRagStoreService ragStoreService;
|
||||
private final IRagStoreModelConfigService ragStoreModelConfigService;
|
||||
private final RagChunkEmbeddingMapper ragChunkEmbeddingMapper;
|
||||
private final EmbeddingModelGateway embeddingModelGateway;
|
||||
private final ChatModelGateway chatModelGateway;
|
||||
|
||||
@Override
|
||||
public List<AgentDefinitionResponse> listResponses() {
|
||||
return lambdaQuery()
|
||||
.orderByAsc(AgentDefinition::getAgentCode)
|
||||
.list()
|
||||
.stream()
|
||||
.map(AgentDefinitionResponse::fromEntity)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request) {
|
||||
AgentDefinitionQueryRequest queryRequest = request == null ? new AgentDefinitionQueryRequest() : request;
|
||||
return lambdaQuery()
|
||||
.eq(StringUtils.hasText(queryRequest.getAgentCode()), AgentDefinition::getAgentCode, trimToNull(queryRequest.getAgentCode()))
|
||||
.like(StringUtils.hasText(queryRequest.getAgentName()), AgentDefinition::getAgentName, trimToNull(queryRequest.getAgentName()))
|
||||
.eq(StringUtils.hasText(queryRequest.getStatus()), AgentDefinition::getStatus, trimToNull(queryRequest.getStatus()))
|
||||
.eq(queryRequest.getStoreId() != null, AgentDefinition::getStoreId, queryRequest.getStoreId())
|
||||
.orderByAsc(AgentDefinition::getAgentCode)
|
||||
.list()
|
||||
.stream()
|
||||
.map(AgentDefinitionResponse::fromEntity)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentDefinitionResponse getResponseById(Long id) {
|
||||
return AgentDefinitionResponse.fromEntity(getById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdate(AgentDefinitionSaveRequest request) {
|
||||
validateSaveRequest(request);
|
||||
if (ragStoreService.getById(request.getStoreId()) == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不存在,ID: " + request.getStoreId());
|
||||
}
|
||||
AgentDefinition duplicate = lambdaQuery()
|
||||
.eq(AgentDefinition::getAgentCode, request.getAgentCode().trim())
|
||||
.ne(request.getId() != null, AgentDefinition::getId, request.getId())
|
||||
.one();
|
||||
if (duplicate != null) {
|
||||
throw new IllegalArgumentException("Agent编码已存在: " + request.getAgentCode().trim());
|
||||
}
|
||||
AgentDefinition entity = request.getId() == null ? new AgentDefinition() : getById(request.getId());
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + request.getId());
|
||||
}
|
||||
entity.setAgentCode(request.getAgentCode().trim());
|
||||
entity.setAgentName(request.getAgentName().trim());
|
||||
entity.setSystemPrompt(trimToNull(request.getSystemPrompt()));
|
||||
entity.setStoreId(request.getStoreId());
|
||||
entity.setStatus(StringUtils.hasText(request.getStatus())
|
||||
? request.getStatus().trim()
|
||||
: EnableStatusEnum.ENABLED.name());
|
||||
entity.setRemark(trimToNull(request.getRemark()));
|
||||
return request.getId() == null ? save(entity) : updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentChatResponse chat(Long agentId, AgentChatRequest request) {
|
||||
if (agentId == null) {
|
||||
throw new IllegalArgumentException("Agent ID不能为空");
|
||||
}
|
||||
if (request == null || request.getMessages() == null || request.getMessages().isEmpty()) {
|
||||
throw new IllegalArgumentException("对话消息不能为空");
|
||||
}
|
||||
AgentDefinition agent = getById(agentId);
|
||||
if (agent == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + agentId);
|
||||
}
|
||||
if (!EnableStatusEnum.ENABLED.name().equals(agent.getStatus())) {
|
||||
throw new IllegalArgumentException("Agent已停用,暂不支持对话");
|
||||
}
|
||||
if (agent.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("Agent未绑定知识库,请先保存知识库配置");
|
||||
}
|
||||
RagStore store = ragStoreService.getById(agent.getStoreId());
|
||||
if (store == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不存在,ID: " + agent.getStoreId());
|
||||
}
|
||||
|
||||
String queryText = resolveLatestUserMessage(request.getMessages());
|
||||
boolean ragEnabled = request.getRagEnabled() == null || request.getRagEnabled();
|
||||
List<RagChunkRecallResponse> recalls = List.of();
|
||||
if (ragEnabled) {
|
||||
RagStoreModelConfig storeModelConfig = ragStoreModelConfigService.getActiveEntity(agent.getStoreId());
|
||||
if (storeModelConfig == null || storeModelConfig.getEmbeddingModelId() == null) {
|
||||
throw new IllegalArgumentException("当前知识库未配置Embedding模型,无法执行检索对话");
|
||||
}
|
||||
EmbeddingRequest embeddingRequest = new EmbeddingRequest();
|
||||
embeddingRequest.setTexts(List.of(queryText));
|
||||
embeddingRequest.setTaskType("RAG_QUERY_EMBEDDING");
|
||||
embeddingRequest.setMatchScope("RAG_STORE");
|
||||
embeddingRequest.setScopeId(agent.getStoreId());
|
||||
embeddingRequest.setBizType("AGENT_CHAT");
|
||||
embeddingRequest.setBizId(String.valueOf(agentId));
|
||||
embeddingRequest.setExpectedDimension(storeModelConfig.getEmbeddingDimension());
|
||||
EmbeddingResult queryEmbedding = embeddingModelGateway.embed(embeddingRequest);
|
||||
if (queryEmbedding.getVectors() == null || queryEmbedding.getVectors().isEmpty()) {
|
||||
throw new IllegalArgumentException("查询向量生成失败,请检查Embedding模型配置");
|
||||
}
|
||||
|
||||
String queryVector = toVectorLiteral(queryEmbedding.getVectors().getFirst());
|
||||
recalls = ragChunkEmbeddingMapper.queryTopKByStore(
|
||||
agent.getStoreId(),
|
||||
queryVector,
|
||||
DEFAULT_TOP_K
|
||||
);
|
||||
if (recalls.isEmpty()) {
|
||||
throw new IllegalArgumentException("未召回到可用知识切片,请先完成知识库切片与向量化");
|
||||
}
|
||||
}
|
||||
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
chatRequest.setTaskType(ragEnabled ? "RAG_ANSWER" : "CHAT_SIMPLE");
|
||||
chatRequest.setMatchScope("AGENT");
|
||||
chatRequest.setScopeId(agentId);
|
||||
chatRequest.setBizType("AGENT_CHAT");
|
||||
chatRequest.setBizId(String.valueOf(agentId));
|
||||
chatRequest.setMessages(buildChatMessages(agent, recalls, request.getMessages(), ragEnabled));
|
||||
|
||||
ChatResult chatResult = chatModelGateway.chat(chatRequest);
|
||||
AgentChatResponse response = new AgentChatResponse();
|
||||
response.setAgentId(agent.getId());
|
||||
response.setAgentCode(agent.getAgentCode());
|
||||
response.setAgentName(agent.getAgentName());
|
||||
response.setStoreId(agent.getStoreId());
|
||||
response.setStoreName(store.getStoreName());
|
||||
response.setAnswer(chatResult.getContent());
|
||||
response.setModelRequestId(chatResult.getCallLog().getRequestId());
|
||||
response.setReferences(toReferenceChunks(recalls));
|
||||
return response;
|
||||
}
|
||||
|
||||
private void validateSaveRequest(AgentDefinitionSaveRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("保存请求不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getAgentCode())) {
|
||||
throw new IllegalArgumentException("Agent编码不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getAgentName())) {
|
||||
throw new IllegalArgumentException("Agent名称不能为空");
|
||||
}
|
||||
if (request.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveLatestUserMessage(List<AgentChatRequest.AgentMessage> messages) {
|
||||
for (int index = messages.size() - 1; index >= 0; index--) {
|
||||
AgentChatRequest.AgentMessage message = messages.get(index);
|
||||
if (message != null
|
||||
&& "user".equalsIgnoreCase(message.getRole())
|
||||
&& StringUtils.hasText(message.getContent())) {
|
||||
return message.getContent();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("缺少用户提问内容");
|
||||
}
|
||||
|
||||
private List<OpenAiChatMessage> buildChatMessages(AgentDefinition agent,
|
||||
List<RagChunkRecallResponse> recalls,
|
||||
List<AgentChatRequest.AgentMessage> rawMessages,
|
||||
boolean ragEnabled) {
|
||||
List<OpenAiChatMessage> messages = new ArrayList<>();
|
||||
OpenAiChatMessage instructionMessage = new OpenAiChatMessage();
|
||||
instructionMessage.setRole("system");
|
||||
instructionMessage.setContent(buildSystemInstruction(agent));
|
||||
messages.add(instructionMessage);
|
||||
|
||||
if (ragEnabled) {
|
||||
OpenAiChatMessage contextMessage = new OpenAiChatMessage();
|
||||
contextMessage.setRole("system");
|
||||
contextMessage.setContent(buildContextText(recalls));
|
||||
messages.add(contextMessage);
|
||||
}
|
||||
|
||||
for (AgentChatRequest.AgentMessage rawMessage : rawMessages) {
|
||||
if (rawMessage == null || !StringUtils.hasText(rawMessage.getContent())) {
|
||||
continue;
|
||||
}
|
||||
OpenAiChatMessage message = new OpenAiChatMessage();
|
||||
message.setRole(normalizeRole(rawMessage.getRole()));
|
||||
message.setContent(rawMessage.getContent());
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private String buildSystemInstruction(AgentDefinition agent) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (StringUtils.hasText(agent.getSystemPrompt())) {
|
||||
builder.append(agent.getSystemPrompt().trim()).append("\n\n");
|
||||
}
|
||||
builder.append("请优先基于已给出的知识库引用片段回答。");
|
||||
builder.append("如果引用无法支持结论,请明确告知“知识库中暂无直接依据”。");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String buildContextText(List<RagChunkRecallResponse> recalls) {
|
||||
StringBuilder builder = new StringBuilder("以下是知识库召回片段:\n");
|
||||
for (int i = 0; i < recalls.size(); i++) {
|
||||
RagChunkRecallResponse recall = recalls.get(i);
|
||||
builder.append(i + 1)
|
||||
.append(". [chunkId=")
|
||||
.append(recall.getChunkId())
|
||||
.append(", score=")
|
||||
.append(String.format("%.4f", recall.getScore() == null ? 0D : recall.getScore()))
|
||||
.append("] ")
|
||||
.append(recall.getChunkContent())
|
||||
.append("\n");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private List<AgentChatResponse.ReferenceChunk> toReferenceChunks(List<RagChunkRecallResponse> recalls) {
|
||||
return recalls.stream().map(recall -> {
|
||||
AgentChatResponse.ReferenceChunk chunk = new AgentChatResponse.ReferenceChunk();
|
||||
chunk.setChunkId(recall.getChunkId());
|
||||
chunk.setDocumentId(recall.getDocumentId());
|
||||
chunk.setChunkContent(recall.getChunkContent());
|
||||
chunk.setScore(recall.getScore());
|
||||
return chunk;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private String normalizeRole(String role) {
|
||||
if (!StringUtils.hasText(role)) {
|
||||
return "user";
|
||||
}
|
||||
String normalized = role.trim().toLowerCase();
|
||||
if ("system".equals(normalized) || "assistant".equals(normalized) || "user".equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
|
||||
private String toVectorLiteral(List<Double> vector) {
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int index = 0; index < vector.size(); index++) {
|
||||
if (index > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(vector.get(index));
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.bruce.modelprovider.client;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OpenAiChatCompletionResult {
|
||||
private String upstreamRequestId;
|
||||
private String content;
|
||||
private Integer promptTokens;
|
||||
private Integer completionTokens;
|
||||
private Integer totalTokens;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.modelprovider.client;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OpenAiChatMessage {
|
||||
private String role;
|
||||
private String content;
|
||||
}
|
||||
@@ -13,6 +13,10 @@ public interface OpenAiCompatibleModelClient {
|
||||
* 方法 embeddings,用于定义接口能力契约。
|
||||
*/
|
||||
List<List<Double>> embeddings(ModelProvider provider, ModelConfig model, List<String> texts, Integer expectedDimension);
|
||||
/**
|
||||
* 方法 chatCompletions,用于定义接口能力契约。
|
||||
*/
|
||||
OpenAiChatCompletionResult chatCompletions(ModelProvider provider, ModelConfig model, List<OpenAiChatMessage> messages);
|
||||
/**
|
||||
* 方法 health,用于定义接口能力契约。
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.bruce.modelprovider.client;
|
||||
|
||||
import com.bruce.modelprovider.config.AiSecretProperties;
|
||||
import com.bruce.modelprovider.entity.ModelConfig;
|
||||
import com.bruce.modelprovider.entity.ModelProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -22,11 +25,17 @@ import java.util.Map;
|
||||
* 4. API Key 从 `secretRef` 对应环境变量读取,不在代码中硬编码。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
/**
|
||||
* OpenAiCompatibleModelClientImpl,负责模型平台对应层的职责。
|
||||
*/
|
||||
public class OpenAiCompatibleModelClientImpl implements OpenAiCompatibleModelClient {
|
||||
|
||||
/**
|
||||
* 统一读取独立 AI 配置文件中的密钥映射。
|
||||
*/
|
||||
private final AiSecretProperties aiSecretProperties;
|
||||
|
||||
/**
|
||||
* 调用上游 Embedding 接口并解析向量数组。
|
||||
*/
|
||||
@@ -74,6 +83,63 @@ public class OpenAiCompatibleModelClientImpl implements OpenAiCompatibleModelCli
|
||||
return vectors;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public OpenAiChatCompletionResult chatCompletions(ModelProvider provider, ModelConfig model, List<OpenAiChatMessage> messages) {
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
throw new IllegalArgumentException("聊天消息不能为空");
|
||||
}
|
||||
RestClient client = RestClient.builder().baseUrl(provider.getBaseUrl()).build();
|
||||
|
||||
List<Map<String, String>> payloadMessages = new ArrayList<>();
|
||||
for (OpenAiChatMessage message : messages) {
|
||||
if (message == null || !StringUtils.hasText(message.getContent())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> item = new HashMap<>();
|
||||
item.put("role", StringUtils.hasText(message.getRole()) ? message.getRole().trim() : "user");
|
||||
item.put("content", message.getContent());
|
||||
payloadMessages.add(item);
|
||||
}
|
||||
if (payloadMessages.isEmpty()) {
|
||||
throw new IllegalArgumentException("聊天消息内容不能为空");
|
||||
}
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("model", model.getUpstreamModel());
|
||||
body.put("messages", payloadMessages);
|
||||
|
||||
RestClient.RequestBodySpec request = client.post().uri("/chat/completions")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(body);
|
||||
String apiKey = resolveApiKey(provider);
|
||||
if (apiKey != null) {
|
||||
request = request.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);
|
||||
}
|
||||
|
||||
Map<String, Object> response = request.retrieve().body(Map.class);
|
||||
if (response == null || !(response.get("choices") instanceof List<?> choices) || choices.isEmpty()) {
|
||||
throw new IllegalStateException("上游Chat响应缺少choices字段");
|
||||
}
|
||||
Object first = choices.getFirst();
|
||||
if (!(first instanceof Map<?, ?> firstChoice)
|
||||
|| !(firstChoice.get("message") instanceof Map<?, ?> message)
|
||||
|| !(message.get("content") instanceof String content)
|
||||
|| !StringUtils.hasText(content)) {
|
||||
throw new IllegalStateException("上游Chat响应缺少message.content");
|
||||
}
|
||||
|
||||
OpenAiChatCompletionResult result = new OpenAiChatCompletionResult();
|
||||
result.setUpstreamRequestId(String.valueOf(response.get("id")));
|
||||
result.setContent(content);
|
||||
if (response.get("usage") instanceof Map<?, ?> usage) {
|
||||
result.setPromptTokens(toInteger(usage.get("prompt_tokens")));
|
||||
result.setCompletionTokens(toInteger(usage.get("completion_tokens")));
|
||||
result.setTotalTokens(toInteger(usage.get("total_tokens")));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 `/models` 做健康探测:成功返回 true,异常返回 false。
|
||||
*/
|
||||
@@ -98,14 +164,34 @@ public class OpenAiCompatibleModelClientImpl implements OpenAiCompatibleModelCli
|
||||
|
||||
/**
|
||||
* 读取服务商密钥:
|
||||
* 有 secretRef 时从环境变量读取;首期不使用数据库密钥明文。
|
||||
* 1) 优先读取 Spring AI 独立配置文件(ai-config.ini);
|
||||
* 2) 再读取环境变量,兼容原有部署方式;
|
||||
* 3) 最后回退数据库密文/占位字段(兼容历史数据)。
|
||||
*/
|
||||
private String resolveApiKey(ModelProvider provider) {
|
||||
if (provider.getSecretRef() != null && !provider.getSecretRef().isBlank()) {
|
||||
return System.getenv(provider.getSecretRef().trim());
|
||||
String secretRef = provider.getSecretRef().trim();
|
||||
String fromSpringConfig = aiSecretProperties.getApiKeyBySecretRef(secretRef);
|
||||
if (StringUtils.hasText(fromSpringConfig)) {
|
||||
return fromSpringConfig;
|
||||
}
|
||||
String fromEnv = System.getenv(secretRef);
|
||||
if (StringUtils.hasText(fromEnv)) {
|
||||
return fromEnv.trim();
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(provider.getApiKeyCipher())) {
|
||||
return provider.getApiKeyCipher().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bruce.modelprovider.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
/**
|
||||
* 加载独立 AI 配置文件。
|
||||
* <p>
|
||||
* 说明:
|
||||
* 1. 该文件使用 INI 扩展名,但内容采用 key=value 形式,Spring 可直接按 Properties 解析;
|
||||
* 2. ignoreResourceNotFound=true,允许某些环境不提供该文件,避免启动失败;
|
||||
* 3. 具体键值由 {@link AiSecretProperties} 统一绑定与读取。
|
||||
*/
|
||||
@Configuration
|
||||
@PropertySource(value = "classpath:ai-config.ini", ignoreResourceNotFound = true)
|
||||
public class AiConfigFilePropertySourceConfig {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.bruce.modelprovider.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 密钥配置绑定。
|
||||
* <p>
|
||||
* 支持从 ai-config.ini 读取如下配置:
|
||||
* ai.secret-refs[SILICONFLOW_API_KEY]=your-key
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "ai")
|
||||
public class AiSecretProperties {
|
||||
|
||||
/**
|
||||
* key 为 secretRef(例如 SILICONFLOW_API_KEY),value 为实际密钥。
|
||||
*/
|
||||
private Map<String, String> secretRefs = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 根据 secretRef 获取配置文件中的密钥,并做空白清理。
|
||||
*/
|
||||
public String getApiKeyBySecretRef(String secretRef) {
|
||||
if (!StringUtils.hasText(secretRef)) {
|
||||
return null;
|
||||
}
|
||||
String value = secretRefs.get(secretRef.trim());
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.bruce.modelprovider.gateway;
|
||||
|
||||
public interface ChatModelGateway {
|
||||
ChatResult chat(ChatRequest request);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.bruce.modelprovider.gateway;
|
||||
|
||||
import com.bruce.modelprovider.client.OpenAiChatCompletionResult;
|
||||
import com.bruce.modelprovider.client.OpenAiChatMessage;
|
||||
import com.bruce.modelprovider.client.OpenAiCompatibleModelClient;
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import com.bruce.modelprovider.entity.ModelConfig;
|
||||
import com.bruce.modelprovider.entity.ModelProvider;
|
||||
import com.bruce.modelprovider.enums.ModelCallStatusEnum;
|
||||
import com.bruce.modelprovider.route.ModelRouteContext;
|
||||
import com.bruce.modelprovider.route.ModelRouteDecision;
|
||||
import com.bruce.modelprovider.service.IModelCallLogService;
|
||||
import com.bruce.modelprovider.service.IModelProviderService;
|
||||
import com.bruce.modelprovider.service.IModelRouteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ChatModelGatewayImpl implements ChatModelGateway {
|
||||
|
||||
private final IModelRouteService modelRouteService;
|
||||
private final IModelProviderService modelProviderService;
|
||||
private final IModelCallLogService modelCallLogService;
|
||||
private final OpenAiCompatibleModelClient openAiCompatibleModelClient;
|
||||
|
||||
@Override
|
||||
public ChatResult chat(ChatRequest request) {
|
||||
if (request == null || request.getMessages() == null || request.getMessages().isEmpty()) {
|
||||
throw new IllegalArgumentException("聊天请求不能为空");
|
||||
}
|
||||
long start = System.currentTimeMillis();
|
||||
ModelCallLog callLog = new ModelCallLog();
|
||||
callLog.setRequestId(UUID.randomUUID().toString().replace("-", ""));
|
||||
callLog.setTaskType(request.getTaskType());
|
||||
callLog.setBizType(request.getBizType());
|
||||
callLog.setBizId(request.getBizId());
|
||||
callLog.setCallType("CHAT");
|
||||
callLog.setRequestHash(buildRequestHash(request.getMessages()));
|
||||
try {
|
||||
ModelRouteContext routeContext = new ModelRouteContext();
|
||||
routeContext.setTaskType(request.getTaskType());
|
||||
routeContext.setMatchScope(request.getMatchScope());
|
||||
routeContext.setScopeId(request.getScopeId());
|
||||
routeContext.setRequiredModelType("CHAT");
|
||||
routeContext.setBizType(request.getBizType());
|
||||
routeContext.setBizId(request.getBizId());
|
||||
ModelRouteDecision decision = modelRouteService.route(routeContext);
|
||||
|
||||
ModelCallExecution execution = executeWithFallback(
|
||||
decision.getPrimaryModel(),
|
||||
decision.getFallbackModels(),
|
||||
request.getMessages()
|
||||
);
|
||||
|
||||
callLog.setProviderId(execution.provider().getId());
|
||||
callLog.setModelId(execution.model().getId());
|
||||
callLog.setStatus(ModelCallStatusEnum.SUCCESS.name());
|
||||
callLog.setPromptTokens(execution.result().getPromptTokens());
|
||||
callLog.setCompletionTokens(execution.result().getCompletionTokens());
|
||||
callLog.setTotalTokens(execution.result().getTotalTokens());
|
||||
callLog.setDurationMs((int) (System.currentTimeMillis() - start));
|
||||
modelCallLogService.save(callLog);
|
||||
|
||||
ChatResult result = new ChatResult();
|
||||
result.setModelId(execution.model().getId());
|
||||
result.setModelName(execution.model().getModelName());
|
||||
result.setContent(execution.result().getContent());
|
||||
result.setUpstreamRequestId(execution.result().getUpstreamRequestId());
|
||||
result.setPromptTokens(execution.result().getPromptTokens());
|
||||
result.setCompletionTokens(execution.result().getCompletionTokens());
|
||||
result.setTotalTokens(execution.result().getTotalTokens());
|
||||
result.setCallLog(callLog);
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
callLog.setStatus(ModelCallStatusEnum.FAILED.name());
|
||||
callLog.setDurationMs((int) (System.currentTimeMillis() - start));
|
||||
callLog.setErrorCode("CHAT_COMPLETION_FAILED");
|
||||
String message = ex.getMessage();
|
||||
callLog.setErrorMessage(message == null ? "unknown" : message.substring(0, Math.min(message.length(), 1000)));
|
||||
modelCallLogService.save(callLog);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelCallExecution executeWithFallback(ModelConfig primaryModel,
|
||||
List<ModelConfig> fallbackModels,
|
||||
List<OpenAiChatMessage> messages) {
|
||||
ModelProvider primaryProvider = requireAvailableProvider(primaryModel.getProviderId());
|
||||
try {
|
||||
OpenAiChatCompletionResult result = openAiCompatibleModelClient.chatCompletions(primaryProvider, primaryModel, messages);
|
||||
return new ModelCallExecution(primaryProvider, primaryModel, result);
|
||||
} catch (Exception primaryEx) {
|
||||
for (ModelConfig fallbackModel : fallbackModels) {
|
||||
try {
|
||||
ModelProvider fallbackProvider = requireAvailableProvider(fallbackModel.getProviderId());
|
||||
OpenAiChatCompletionResult result = openAiCompatibleModelClient.chatCompletions(
|
||||
fallbackProvider,
|
||||
fallbackModel,
|
||||
messages
|
||||
);
|
||||
return new ModelCallExecution(fallbackProvider, fallbackModel, result);
|
||||
} catch (Exception ignored) {
|
||||
// continue fallback chain
|
||||
}
|
||||
}
|
||||
throw primaryEx;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelProvider requireAvailableProvider(Long providerId) {
|
||||
ModelProvider provider = modelProviderService.getById(providerId);
|
||||
if (provider == null || !Boolean.TRUE.equals(provider.getEnabled())) {
|
||||
throw new IllegalStateException("模型服务商不可用");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private String buildRequestHash(List<OpenAiChatMessage> messages) {
|
||||
String plainText = messages.stream()
|
||||
.map(message -> (StringUtils.hasText(message.getRole()) ? message.getRole() : "user") + ":" + message.getContent())
|
||||
.reduce((left, right) -> left + "|" + right)
|
||||
.orElse("");
|
||||
return DigestUtils.md5DigestAsHex(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private record ModelCallExecution(ModelProvider provider, ModelConfig model, OpenAiChatCompletionResult result) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bruce.modelprovider.gateway;
|
||||
|
||||
import com.bruce.modelprovider.client.OpenAiChatMessage;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
private List<OpenAiChatMessage> messages;
|
||||
private String taskType;
|
||||
private String matchScope;
|
||||
private Long scopeId;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bruce.modelprovider.gateway;
|
||||
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChatResult {
|
||||
private Long modelId;
|
||||
private String modelName;
|
||||
private String content;
|
||||
private String upstreamRequestId;
|
||||
private Integer promptTokens;
|
||||
private Integer completionTokens;
|
||||
private Integer totalTokens;
|
||||
private ModelCallLog callLog;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bruce.rag.dto.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RagChunkRecallResponse {
|
||||
private Long chunkId;
|
||||
private Long documentId;
|
||||
private String chunkContent;
|
||||
private Double score;
|
||||
}
|
||||
@@ -1,9 +1,32 @@
|
||||
package com.bruce.rag.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.rag.dto.response.RagChunkRecallResponse;
|
||||
import com.bruce.rag.entity.RagChunkEmbedding;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface RagChunkEmbeddingMapper extends BaseMapper<RagChunkEmbedding> {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
e.chunk_id AS chunkId,
|
||||
e.document_id AS documentId,
|
||||
c.chunk_content AS chunkContent,
|
||||
1 - (e.embedding <=> CAST(#{queryVector} AS vector)) AS score
|
||||
FROM rag_chunk_embedding e
|
||||
INNER JOIN rag_chunk c ON c.id = e.chunk_id
|
||||
WHERE e.store_id = #{storeId}
|
||||
AND e.enabled = TRUE
|
||||
AND c.enabled = TRUE
|
||||
ORDER BY e.embedding <=> CAST(#{queryVector} AS vector)
|
||||
LIMIT #{topK}
|
||||
""")
|
||||
List<RagChunkRecallResponse> queryTopKByStore(@Param("storeId") Long storeId,
|
||||
@Param("queryVector") String queryVector,
|
||||
@Param("topK") int topK);
|
||||
}
|
||||
|
||||
5
src/main/resources/ai-config.ini
Normal file
5
src/main/resources/ai-config.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
# AI 独立配置文件(建议仅本地/环境覆盖使用,不提交真实密钥)
|
||||
# 格式:ai.secret-refs[<secret_ref>]=<api_key>
|
||||
|
||||
ai.secret-refs[SILICONFLOW_API_KEY]=your-key
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.bruce.agent;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.controller.AgentDefinitionController;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.mapper.AgentDefinitionMapper;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.impl.AgentDefinitionServiceImpl;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AgentComponentStructureTests {
|
||||
|
||||
@Test
|
||||
void agentComponentsShouldReuseMybatisPlusBaseTypes() {
|
||||
assertTrue(BaseMapper.class.isAssignableFrom(AgentDefinitionMapper.class));
|
||||
assertTrue(IService.class.isAssignableFrom(IAgentDefinitionService.class));
|
||||
assertTrue(ServiceImpl.class.isAssignableFrom(AgentDefinitionServiceImpl.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentControllerShouldExposeRequestResultMethods() throws NoSuchMethodException {
|
||||
Method listMethod = AgentDefinitionController.class.getMethod("list");
|
||||
Method queryMethod = AgentDefinitionController.class.getMethod("query", AgentDefinitionQueryRequest.class);
|
||||
Method detailMethod = AgentDefinitionController.class.getMethod("detail", Long.class);
|
||||
Method saveMethod = AgentDefinitionController.class.getMethod("save", AgentDefinitionSaveRequest.class);
|
||||
Method deleteMethod = AgentDefinitionController.class.getMethod("delete", Long.class);
|
||||
Method chatMethod = AgentDefinitionController.class.getMethod("chat", Long.class, AgentChatRequest.class);
|
||||
|
||||
Method listServiceMethod = IAgentDefinitionService.class.getMethod("listResponses");
|
||||
Method queryServiceMethod = IAgentDefinitionService.class.getMethod("query", AgentDefinitionQueryRequest.class);
|
||||
Method detailServiceMethod = IAgentDefinitionService.class.getMethod("getResponseById", Long.class);
|
||||
Method saveServiceMethod = IAgentDefinitionService.class.getMethod("saveOrUpdate", AgentDefinitionSaveRequest.class);
|
||||
Method chatServiceMethod = IAgentDefinitionService.class.getMethod("chat", Long.class, AgentChatRequest.class);
|
||||
|
||||
assertEquals(RequestResult.class, listMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, queryMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, detailMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, saveMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, deleteMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, chatMethod.getReturnType());
|
||||
|
||||
assertEquals(List.class, listServiceMethod.getReturnType());
|
||||
assertEquals(List.class, queryServiceMethod.getReturnType());
|
||||
assertEquals(AgentDefinitionResponse.class, detailServiceMethod.getReturnType());
|
||||
assertEquals(boolean.class, saveServiceMethod.getReturnType());
|
||||
assertEquals(AgentChatResponse.class, chatServiceMethod.getReturnType());
|
||||
assertEquals(AgentDefinitionResponse.class, AgentDefinitionResponse.class.getMethod("fromEntity", AgentDefinition.class).getReturnType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.bruce.agent;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.service.impl.AgentDefinitionServiceImpl;
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import com.bruce.modelprovider.entity.RagStoreModelConfig;
|
||||
import com.bruce.modelprovider.gateway.ChatRequest;
|
||||
import com.bruce.modelprovider.gateway.ChatResult;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingRequest;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingResult;
|
||||
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
|
||||
import com.bruce.rag.dto.response.RagChunkRecallResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.mapper.RagChunkEmbeddingMapper;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import com.bruce.modelprovider.gateway.ChatModelGateway;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingModelGateway;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentDefinitionServiceImplTests {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private AgentDefinitionServiceImpl agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private IRagStoreService ragStoreService;
|
||||
|
||||
@Mock
|
||||
private IRagStoreModelConfigService ragStoreModelConfigService;
|
||||
|
||||
@Mock
|
||||
private RagChunkEmbeddingMapper ragChunkEmbeddingMapper;
|
||||
|
||||
@Mock
|
||||
private EmbeddingModelGateway embeddingModelGateway;
|
||||
|
||||
@Mock
|
||||
private ChatModelGateway chatModelGateway;
|
||||
|
||||
@Test
|
||||
void saveOrUpdateShouldValidateBoundStoreExists() {
|
||||
AgentDefinitionSaveRequest request = new AgentDefinitionSaveRequest();
|
||||
request.setAgentCode("A_1");
|
||||
request.setAgentName("Agent 1");
|
||||
request.setStoreId(1001L);
|
||||
|
||||
when(ragStoreService.getById(1001L)).thenReturn(null);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.saveOrUpdate(request));
|
||||
assertTrue(exception.getMessage().contains("绑定知识库不存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldRejectDisabledAgent() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("DISABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("你好");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.chat(1001L, request));
|
||||
assertTrue(exception.getMessage().contains("停用"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldRejectAgentWithoutStore() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setStatus("ENABLED");
|
||||
agent.setStoreId(null);
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("你好");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.chat(1001L, request));
|
||||
assertTrue(exception.getMessage().contains("未绑定知识库"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldUseStoreScopedRecallAndReturnAnswer() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setAgentCode("AGENT_1");
|
||||
agent.setAgentName("知识助手");
|
||||
agent.setSystemPrompt("你是企业知识助手");
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("ENABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
RagStore store = new RagStore();
|
||||
store.setId(2001L);
|
||||
store.setStoreName("企业知识库");
|
||||
when(ragStoreService.getById(2001L)).thenReturn(store);
|
||||
|
||||
RagStoreModelConfig modelConfig = new RagStoreModelConfig();
|
||||
modelConfig.setStoreId(2001L);
|
||||
modelConfig.setEmbeddingModelId(3001L);
|
||||
modelConfig.setEmbeddingDimension(1024);
|
||||
when(ragStoreModelConfigService.getActiveEntity(2001L)).thenReturn(modelConfig);
|
||||
|
||||
EmbeddingResult embeddingResult = new EmbeddingResult();
|
||||
embeddingResult.setVectors(List.of(List.of(0.12, 0.34, 0.56)));
|
||||
when(embeddingModelGateway.embed(any(EmbeddingRequest.class))).thenReturn(embeddingResult);
|
||||
|
||||
RagChunkRecallResponse recall = new RagChunkRecallResponse();
|
||||
recall.setChunkId(4001L);
|
||||
recall.setDocumentId(5001L);
|
||||
recall.setChunkContent("公司请假流程:先提交审批单。");
|
||||
recall.setScore(0.91);
|
||||
when(ragChunkEmbeddingMapper.queryTopKByStore(anyLong(), anyString(), anyInt()))
|
||||
.thenReturn(List.of(recall));
|
||||
|
||||
ModelCallLog callLog = new ModelCallLog();
|
||||
callLog.setRequestId("req_001");
|
||||
ChatResult chatResult = new ChatResult();
|
||||
chatResult.setContent("根据知识库,先在OA提交请假审批。");
|
||||
chatResult.setCallLog(callLog);
|
||||
when(chatModelGateway.chat(any(ChatRequest.class))).thenReturn(chatResult);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("请假流程是什么?");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
AgentChatResponse response = agentDefinitionService.chat(1001L, request);
|
||||
|
||||
assertEquals(1001L, response.getAgentId());
|
||||
assertEquals(2001L, response.getStoreId());
|
||||
assertEquals("企业知识库", response.getStoreName());
|
||||
assertEquals("根据知识库,先在OA提交请假审批。", response.getAnswer());
|
||||
assertEquals("req_001", response.getModelRequestId());
|
||||
assertEquals(1, response.getReferences().size());
|
||||
assertEquals(4001L, response.getReferences().getFirst().getChunkId());
|
||||
|
||||
ArgumentCaptor<EmbeddingRequest> embeddingRequestCaptor = ArgumentCaptor.forClass(EmbeddingRequest.class);
|
||||
verify(embeddingModelGateway).embed(embeddingRequestCaptor.capture());
|
||||
EmbeddingRequest embeddingRequest = embeddingRequestCaptor.getValue();
|
||||
assertEquals("RAG_QUERY_EMBEDDING", embeddingRequest.getTaskType());
|
||||
assertEquals("RAG_STORE", embeddingRequest.getMatchScope());
|
||||
assertEquals(2001L, embeddingRequest.getScopeId());
|
||||
|
||||
verify(ragChunkEmbeddingMapper).queryTopKByStore(anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldSupportSimpleModeWithoutRagRecall() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setAgentCode("AGENT_1");
|
||||
agent.setAgentName("知识助手");
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("ENABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
RagStore store = new RagStore();
|
||||
store.setId(2001L);
|
||||
store.setStoreName("企业知识库");
|
||||
when(ragStoreService.getById(2001L)).thenReturn(store);
|
||||
|
||||
ModelCallLog callLog = new ModelCallLog();
|
||||
callLog.setRequestId("req_simple_001");
|
||||
ChatResult chatResult = new ChatResult();
|
||||
chatResult.setContent("这是普通对话回答。");
|
||||
chatResult.setCallLog(callLog);
|
||||
when(chatModelGateway.chat(any(ChatRequest.class))).thenReturn(chatResult);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
request.setRagEnabled(false);
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("直接聊聊今天安排");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
AgentChatResponse response = agentDefinitionService.chat(1001L, request);
|
||||
|
||||
assertEquals("这是普通对话回答。", response.getAnswer());
|
||||
assertTrue(response.getReferences().isEmpty());
|
||||
verify(embeddingModelGateway, never()).embed(any(EmbeddingRequest.class));
|
||||
verify(ragChunkEmbeddingMapper, never()).queryTopKByStore(anyLong(), anyString(), anyInt());
|
||||
|
||||
ArgumentCaptor<ChatRequest> chatRequestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
|
||||
verify(chatModelGateway).chat(chatRequestCaptor.capture());
|
||||
assertEquals("CHAT_SIMPLE", chatRequestCaptor.getValue().getTaskType());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user