mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-08-31 00:50:02 +08:00
feat: Revamp conversation configuration management and enhance localization
- Removed outdated DEVELOPMENT.md, consolidating development instructions into relevant documentation. - Updated conversation configuration to include new parameters such as max rounds, embedding thresholds, and rewrite prompts. - Enhanced UI components to support new conversation settings, including AgentSettings.vue and related views. - Improved localization in English, Russian, and Chinese for new conversation configuration fields and UI elements. - Implemented logic to apply tenant-specific conversation settings, ensuring defaults are respected.
This commit is contained in:
-267
@@ -1,267 +0,0 @@
|
||||
# WeKnora 开发环境快速入门
|
||||
|
||||
> 无需重新构建 Docker 镜像,实现秒级代码热更新!
|
||||
|
||||
## 🚀 快速开始(3 种方式)
|
||||
|
||||
### 方式 1:推荐 - 使用 Make 命令
|
||||
|
||||
**终端 1** - 启动基础设施:
|
||||
```bash
|
||||
make dev-start
|
||||
```
|
||||
|
||||
**终端 2** - 启动后端:
|
||||
```bash
|
||||
make dev-app
|
||||
```
|
||||
|
||||
**终端 3** - 启动前端:
|
||||
```bash
|
||||
make dev-frontend
|
||||
```
|
||||
|
||||
访问 http://localhost:5173 开始开发!
|
||||
|
||||
### 方式 2:使用脚本命令
|
||||
|
||||
```bash
|
||||
# 终端 1
|
||||
./scripts/dev.sh start
|
||||
|
||||
# 终端 2
|
||||
./scripts/dev.sh app
|
||||
|
||||
# 终端 3
|
||||
./scripts/dev.sh frontend
|
||||
```
|
||||
|
||||
### 方式 3:一键启动(交互式)
|
||||
|
||||
```bash
|
||||
./scripts/quick-dev.sh
|
||||
```
|
||||
|
||||
按照提示选择是否启动后端和前端。
|
||||
|
||||
## 📋 常用命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `make dev-start` | 启动基础设施(postgres, redis, minio 等) |
|
||||
| `make dev-stop` | 停止所有服务 |
|
||||
| `make dev-restart` | 重启所有服务 |
|
||||
| `make dev-status` | 查看服务状态 |
|
||||
| `make dev-logs` | 查看服务日志 |
|
||||
| `make dev-app` | 启动后端(需先启动基础设施) |
|
||||
| `make dev-frontend` | 启动前端(需先启动基础设施) |
|
||||
|
||||
## 🎯 访问地址
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 前端开发服务器 | http://localhost:5173 |
|
||||
| 后端 API | http://localhost:8080 |
|
||||
| PostgreSQL | localhost:5432 |
|
||||
| Redis | localhost:6379 |
|
||||
| MinIO Console | http://localhost:9001 |
|
||||
| Neo4j Browser | http://localhost:7474 |
|
||||
| Jaeger UI | http://localhost:16686 |
|
||||
|
||||
## 💡 开发工作流对比
|
||||
|
||||
### ❌ 旧方式(慢)
|
||||
|
||||
```bash
|
||||
# 每次修改代码后
|
||||
sh scripts/build_images.sh -p # 重新构建镜像(2-5分钟)
|
||||
sh scripts/start_all.sh --no-pull # 重启容器
|
||||
```
|
||||
|
||||
### ✅ 新方式(快)
|
||||
|
||||
```bash
|
||||
# 首次启动(只需一次)
|
||||
make dev-start
|
||||
|
||||
# 修改后端代码
|
||||
# → Ctrl+C 停止 → make dev-app 重启(5-10秒)
|
||||
|
||||
# 修改前端代码
|
||||
# → 自动热重载(无需任何操作)
|
||||
```
|
||||
|
||||
**时间对比**:
|
||||
- 旧方式:每次修改 2-5 分钟
|
||||
- 新方式首次:1-2 分钟
|
||||
- 新方式后续:
|
||||
- 后端修改:5-10 秒
|
||||
- 前端修改:实时热重载
|
||||
|
||||
## 🔥 进阶:后端热重载
|
||||
|
||||
安装 Air 实现后端代码修改后自动重启:
|
||||
|
||||
```bash
|
||||
# 安装 Air
|
||||
go install github.com/cosmtrek/air@latest
|
||||
|
||||
# 确保 $GOPATH/bin 在 PATH 中
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
|
||||
# 使用 Air 启动(自动检测)
|
||||
make dev-app
|
||||
# 或直接运行
|
||||
air
|
||||
```
|
||||
|
||||
修改 Go 代码后,Air 会自动重新编译和重启,无需手动操作!
|
||||
|
||||
## 📝 开发场景示例
|
||||
|
||||
### 场景 1:只修改前端
|
||||
|
||||
```bash
|
||||
# 一次性启动基础设施
|
||||
make dev-start
|
||||
|
||||
# 启动前端,修改代码自动热重载
|
||||
make dev-frontend
|
||||
```
|
||||
|
||||
### 场景 2:只修改后端
|
||||
|
||||
```bash
|
||||
# 启动基础设施
|
||||
make dev-start
|
||||
|
||||
# 启动后端(如果安装了 Air,支持热重载)
|
||||
make dev-app
|
||||
```
|
||||
|
||||
### 场景 3:同时开发前后端
|
||||
|
||||
```bash
|
||||
# 终端 1:启动基础设施
|
||||
make dev-start
|
||||
|
||||
# 终端 2:启动后端
|
||||
make dev-app
|
||||
|
||||
# 终端 3:启动前端
|
||||
make dev-frontend
|
||||
```
|
||||
|
||||
## 🛠 VS Code 调试配置
|
||||
|
||||
创建 `.vscode/launch.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch WeKnora Server",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"env": {
|
||||
"DB_HOST": "localhost",
|
||||
"DB_PORT": "5432",
|
||||
"DOCREADER_ADDR": "localhost:50051",
|
||||
"MINIO_ENDPOINT": "localhost:9000",
|
||||
"REDIS_ADDR": "localhost:6379",
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
|
||||
"NEO4J_URI": "bolt://localhost:7687"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
然后按 F5 开始调试!
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题:启动后端时报错连接不到数据库
|
||||
|
||||
**解决**:确保先运行了 `make dev-start` 并等待 30 秒让服务完全启动。
|
||||
|
||||
查看服务状态:
|
||||
```bash
|
||||
make dev-status
|
||||
```
|
||||
|
||||
### 问题:前端访问 API 时报 CORS 错误
|
||||
|
||||
**解决**:前端已配置代理,确保:
|
||||
1. 后端运行在 `localhost:8080`
|
||||
2. 前端运行在 `localhost:5173`
|
||||
3. 查看 `frontend/vite.config.ts` 的代理配置
|
||||
|
||||
### 问题:端口被占用
|
||||
|
||||
**解决**:修改 `.env` 文件中的端口配置:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DB_PORT=5433 # 默认 5432
|
||||
REDIS_PORT=6380 # 默认 6379
|
||||
MINIO_PORT=9001 # 默认 9000
|
||||
```
|
||||
|
||||
然后重启服务:
|
||||
```bash
|
||||
make dev-restart
|
||||
```
|
||||
|
||||
### 问题:DocReader 需要重新构建
|
||||
|
||||
**解决**:DocReader 仍使用 Docker 镜像,需要重新构建:
|
||||
|
||||
```bash
|
||||
sh scripts/build_images.sh -d
|
||||
make dev-restart
|
||||
```
|
||||
|
||||
## 🎯 生产环境部署
|
||||
|
||||
开发完成后需要部署时:
|
||||
|
||||
```bash
|
||||
# 构建所有镜像
|
||||
sh scripts/build_images.sh
|
||||
|
||||
# 或只构建特定镜像
|
||||
sh scripts/build_images.sh -p # 后端
|
||||
sh scripts/build_images.sh -f # 前端
|
||||
sh scripts/build_images.sh -d # DocReader
|
||||
|
||||
# 启动生产环境
|
||||
sh scripts/start_all.sh
|
||||
```
|
||||
|
||||
## 📚 更多文档
|
||||
|
||||
- [完整开发指南](docs/开发指南.md)
|
||||
- [API 文档](docs/API.md)
|
||||
- [Agent 开发](docs/AGENT.md)
|
||||
|
||||
## 💪 最佳实践
|
||||
|
||||
1. **日常开发**:使用 `make dev-*` 命令,享受快速迭代
|
||||
2. **提交前测试**:使用完整 Docker 环境测试集成功能
|
||||
3. **生产部署**:使用构建镜像的方式部署
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
使用开发模式,你可以:
|
||||
|
||||
✅ **不重新构建镜像** - 直接在本地运行代码
|
||||
✅ **秒级热更新** - 前端自动热重载,后端快速重启
|
||||
✅ **完整调试支持** - IDE 断点调试,实时查看变量
|
||||
✅ **节省时间** - 每次修改从 2-5 分钟降至 5-10 秒
|
||||
|
||||
祝你开发愉快!🚀
|
||||
|
||||
-527
@@ -1,527 +0,0 @@
|
||||
# WeKnora Agent Mode 文档
|
||||
|
||||
## 概述
|
||||
|
||||
WeKnora Agent Mode 是基于 ReAct (Reasoning + Acting) 框架实现的智能代理系统,能够通过工具调用和迭代推理来回答复杂问题。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **ReAct 框架**: Thought → Action → Observation 循环
|
||||
- **工具系统**: 7个知识库相关工具
|
||||
- **可选规划**: 可在执行前生成任务计划
|
||||
- **事件追踪**: 完整的执行过程可视化
|
||||
- **灵活配置**: 支持多种参数调整
|
||||
|
||||
## 架构设计
|
||||
|
||||
### Agent 工作流程
|
||||
|
||||
```
|
||||
用户查询
|
||||
↓
|
||||
[可选] 生成执行计划
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ ReAct 循环开始 │
|
||||
├─────────────────┤
|
||||
│ 1. Think (思考) │ → LLM 分析当前状态
|
||||
│ 2. Act (行动) │ → 调用工具获取信息
|
||||
│ 3. Observe (观察)│ → 处理工具返回结果
|
||||
│ [可选] Reflect │ → 反思当前步骤
|
||||
│ 4. Decide (决策)│ → 继续或结束?
|
||||
└─────────────────┘
|
||||
↓
|
||||
生成最终答案
|
||||
```
|
||||
|
||||
### 组件结构
|
||||
|
||||
```
|
||||
internal/agent/
|
||||
├── engine.go # Agent 执行引擎
|
||||
├── prompts.go # System prompts
|
||||
└── tools/ # 工具系统
|
||||
├── tool.go
|
||||
├── registry.go
|
||||
├── knowledge_search.go
|
||||
├── multi_kb_search.go
|
||||
├── list_knowledge_bases.go
|
||||
├── get_chunk_detail.go
|
||||
├── get_related_chunks.go
|
||||
├── query_knowledge_graph.go
|
||||
└── get_document_info.go
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### Agent 配置结构
|
||||
|
||||
```go
|
||||
type AgentConfig struct {
|
||||
Enabled bool // 是否启用 Agent 模式
|
||||
EnablePlanning bool // 是否先规划
|
||||
MaxIterations int // 最大迭代次数 (1-20)
|
||||
ReflectionEnabled bool // 是否启用反思
|
||||
AllowedTools []string // 允许的工具列表
|
||||
Temperature float64 // LLM 温度 (0-2)
|
||||
ThinkingModelID string // 推理模型 ID
|
||||
KnowledgeBases []string // 可访问的知识库 ID
|
||||
}
|
||||
```
|
||||
|
||||
### 全局配置 (config.yaml)
|
||||
|
||||
```yaml
|
||||
agent:
|
||||
enabled: true
|
||||
default_max_iterations: 5
|
||||
default_temperature: 0.7
|
||||
reflection_enabled: false
|
||||
default_tools:
|
||||
- knowledge_search
|
||||
- multi_kb_search
|
||||
- list_knowledge_bases
|
||||
- get_chunk_detail
|
||||
- get_related_chunks
|
||||
- query_knowledge_graph
|
||||
- get_document_info
|
||||
```
|
||||
|
||||
## 可用工具
|
||||
|
||||
### 1. knowledge_search
|
||||
搜索指定知识库中的相关内容。
|
||||
|
||||
**参数:**
|
||||
- `knowledge_base_id` (必需): 知识库ID
|
||||
- `query` (必需): 搜索查询内容
|
||||
- `top_k` (可选): 返回结果数量,默认5
|
||||
|
||||
**示例:**
|
||||
```
|
||||
knowledge_search(knowledge_base_id="kb123", query="什么是RAG", top_k=5)
|
||||
```
|
||||
|
||||
### 2. multi_kb_search
|
||||
在多个知识库中智能搜索,自动选择最相关的知识库。
|
||||
|
||||
**参数:**
|
||||
- `query` (必需): 搜索查询内容
|
||||
- `top_k` (可选): 每个知识库返回的结果数量,默认3
|
||||
|
||||
**适用场景:** 跨知识库查询,不确定信息在哪个知识库中
|
||||
|
||||
### 3. list_knowledge_bases
|
||||
列出当前可访问的所有知识库。
|
||||
|
||||
**参数:** 无
|
||||
|
||||
**用途:** 了解有哪些知识库可以搜索
|
||||
|
||||
### 4. get_chunk_detail
|
||||
获取指定chunk的详细信息,包括完整内容、来源文档。
|
||||
|
||||
**参数:**
|
||||
- `chunk_id` (必需): Chunk ID
|
||||
|
||||
**用途:** 当搜索结果不够详细时获取完整内容
|
||||
|
||||
### 5. get_related_chunks
|
||||
获取与指定chunk相关的其他chunks。
|
||||
|
||||
**参数:**
|
||||
- `chunk_id` (必需): Chunk ID
|
||||
- `relation_type` (可选): "sequential" (顺序) 或 "semantic" (语义)
|
||||
- `limit` (可选): 返回数量,默认5
|
||||
|
||||
**用途:** 发现相关信息,扩展上下文
|
||||
|
||||
### 6. query_knowledge_graph
|
||||
查询知识图谱中的实体和关系。
|
||||
|
||||
**参数:**
|
||||
- `knowledge_base_id` (必需): 知识库ID
|
||||
- `query` (必需): 查询内容(实体名称或查询文本)
|
||||
|
||||
**前提:** 知识库已配置知识图谱抽取
|
||||
|
||||
### 7. get_document_info
|
||||
获取文档的元数据信息。
|
||||
|
||||
**参数:**
|
||||
- `knowledge_id` (必需): 文档/知识ID
|
||||
|
||||
**用途:** 了解文档的整体情况
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 创建 Agent Session
|
||||
|
||||
```bash
|
||||
POST /api/v1/sessions
|
||||
|
||||
{
|
||||
"knowledge_base_id": "kb123",
|
||||
"session_strategy": {
|
||||
"summary_model_id": "model123",
|
||||
...
|
||||
},
|
||||
"agent_config": {
|
||||
"enabled": true,
|
||||
"enable_planning": false,
|
||||
"max_iterations": 5,
|
||||
"reflection_enabled": false,
|
||||
"allowed_tools": [
|
||||
"knowledge_search",
|
||||
"multi_kb_search",
|
||||
"list_knowledge_bases"
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"knowledge_bases": ["kb123", "kb456"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 发起 Agent 查询
|
||||
|
||||
```bash
|
||||
POST /api/v1/sessions/{session_id}/agent-qa
|
||||
|
||||
{
|
||||
"query": "请解释RAG技术的工作原理"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
Agent 会返回 SSE (Server-Sent Events) 流式响应:
|
||||
|
||||
1. **知识引用** (可选)
|
||||
```json
|
||||
{
|
||||
"response_type": "references",
|
||||
"knowledge_references": [...]
|
||||
}
|
||||
```
|
||||
|
||||
2. **Agent 思考过程** (可选,用于调试)
|
||||
```json
|
||||
{
|
||||
"response_type": "agent_thought",
|
||||
"content": "我需要先搜索相关知识..."
|
||||
}
|
||||
```
|
||||
|
||||
3. **工具调用** (可选)
|
||||
```json
|
||||
{
|
||||
"response_type": "agent_action",
|
||||
"content": "调用工具: knowledge_search"
|
||||
}
|
||||
```
|
||||
|
||||
4. **最终答案**
|
||||
```json
|
||||
{
|
||||
"response_type": "answer",
|
||||
"content": "RAG技术的工作原理是...",
|
||||
"done": true
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 工具选择策略
|
||||
|
||||
- **已知知识库**: 使用 `knowledge_search`
|
||||
- **不确定位置**: 使用 `multi_kb_search`
|
||||
- **需要上下文**: 使用 `get_related_chunks`
|
||||
- **探索阶段**: 先用 `list_knowledge_bases`
|
||||
|
||||
### 2. 迭代次数设置
|
||||
|
||||
- **简单查询**: 3-5 次
|
||||
- **复杂问题**: 5-10 次
|
||||
- **探索性任务**: 10-15 次
|
||||
- **最大限制**: 20 次 (防止无限循环)
|
||||
|
||||
### 3. 温度参数
|
||||
|
||||
- **精确查询**: 0.3-0.5 (更确定性)
|
||||
- **创造性任务**: 0.7-1.0 (更多样性)
|
||||
- **默认推荐**: 0.7
|
||||
|
||||
### 4. Planning 启用时机
|
||||
|
||||
- **复杂多步骤任务**: 启用
|
||||
- **简单直接查询**: 禁用
|
||||
- **探索性问题**: 启用
|
||||
|
||||
### 5. Reflection 启用时机
|
||||
|
||||
- **关键任务**: 启用 (提高准确性)
|
||||
- **快速响应**: 禁用 (减少延迟)
|
||||
- **默认**: 禁用
|
||||
|
||||
## 工作原理详解
|
||||
|
||||
### ReAct Prompt Template
|
||||
|
||||
```
|
||||
你是一个智能知识库助手。你的任务是通过使用提供的工具来回答用户问题。
|
||||
|
||||
工作流程:
|
||||
1. 分析用户问题,确定需要什么信息
|
||||
2. 使用合适的工具获取信息(可以多次调用不同工具)
|
||||
3. 基于获取的信息,提供准确、完整的答案
|
||||
|
||||
注意事项:
|
||||
- 优先使用 multi_kb_search 进行跨知识库搜索
|
||||
- 如果需要特定知识库,先用 list_knowledge_bases 查看可用知识库
|
||||
- 如果搜索结果不够详细,使用 get_chunk_detail 获取完整内容
|
||||
- 使用 get_related_chunks 发现相关信息
|
||||
- 如果涉及实体关系,使用 query_knowledge_graph
|
||||
- 引用信息时,说明来源(chunk_id 或 knowledge_base)
|
||||
- 如果找不到相关信息,诚实告知用户
|
||||
|
||||
当前可访问的知识库:
|
||||
{knowledge_bases}
|
||||
|
||||
{plan_context}
|
||||
```
|
||||
|
||||
### 工具调用解析
|
||||
|
||||
Agent 会尝试从 LLM 输出中解析工具调用,支持的格式:
|
||||
|
||||
```
|
||||
tool_name(arg1="value1", arg2="value2")
|
||||
```
|
||||
|
||||
例如:
|
||||
```
|
||||
knowledge_search(knowledge_base_id="kb123", query="RAG技术")
|
||||
```
|
||||
|
||||
### 循环控制
|
||||
|
||||
Agent 会在以下情况停止:
|
||||
|
||||
1. LLM 输出包含 "最终答案" 或 "Final Answer"
|
||||
2. 达到最大迭代次数
|
||||
3. 工具调用失败且无法恢复
|
||||
4. 检测到重复循环模式
|
||||
|
||||
## 事件追踪
|
||||
|
||||
Agent 执行过程中会发送以下事件 (通过 Event Bus):
|
||||
|
||||
### agent.plan
|
||||
```go
|
||||
type AgentPlanData struct {
|
||||
Query string
|
||||
Plan []string
|
||||
Duration int64
|
||||
}
|
||||
```
|
||||
|
||||
### agent.step
|
||||
```go
|
||||
type AgentStepData struct {
|
||||
Iteration int
|
||||
Thought string
|
||||
ToolCalls []ToolCall
|
||||
Duration int64
|
||||
}
|
||||
```
|
||||
|
||||
### agent.tool
|
||||
```go
|
||||
type AgentActionData struct {
|
||||
Iteration int
|
||||
ToolName string
|
||||
ToolInput map[string]interface{}
|
||||
ToolOutput string
|
||||
Success bool
|
||||
Error string
|
||||
Duration int64
|
||||
}
|
||||
```
|
||||
|
||||
## 示例场景
|
||||
|
||||
### 场景 1: 简单知识查询
|
||||
|
||||
**用户问题**: "什么是 RAG?"
|
||||
|
||||
**Agent 执行流程**:
|
||||
1. **Think**: 需要搜索 RAG 相关知识
|
||||
2. **Act**: `multi_kb_search(query="RAG")`
|
||||
3. **Observe**: 获得3条相关结果
|
||||
4. **Think**: 信息足够,可以回答
|
||||
5. **Final Answer**: 基于搜索结果生成答案
|
||||
|
||||
**迭代次数**: 2
|
||||
|
||||
### 场景 2: 跨文档关联查询
|
||||
|
||||
**用户问题**: "比较 RAG 和微调的优缺点"
|
||||
|
||||
**Agent 执行流程**:
|
||||
1. **Think**: 需要分别查找 RAG 和微调的信息
|
||||
2. **Act**: `multi_kb_search(query="RAG优缺点")`
|
||||
3. **Observe**: 获得 RAG 相关信息
|
||||
4. **Think**: 还需要微调的信息
|
||||
5. **Act**: `multi_kb_search(query="模型微调优缺点")`
|
||||
6. **Observe**: 获得微调相关信息
|
||||
7. **Think**: 信息完整,进行对比
|
||||
8. **Final Answer**: 综合两者信息生成对比答案
|
||||
|
||||
**迭代次数**: 4
|
||||
|
||||
### 场景 3: 深入探索
|
||||
|
||||
**用户问题**: "详细解释向量数据库的工作原理"
|
||||
|
||||
**Agent 执行流程**:
|
||||
1. **Think**: 先查看有哪些知识库
|
||||
2. **Act**: `list_knowledge_bases()`
|
||||
3. **Observe**: 发现有"数据库技术"知识库
|
||||
4. **Think**: 在该知识库中搜索
|
||||
5. **Act**: `knowledge_search(knowledge_base_id="db_kb", query="向量数据库")`
|
||||
6. **Observe**: 找到相关 chunk
|
||||
7. **Think**: 需要更多细节
|
||||
8. **Act**: `get_chunk_detail(chunk_id="chunk123")`
|
||||
9. **Observe**: 获得完整内容
|
||||
10. **Think**: 查看相关内容
|
||||
11. **Act**: `get_related_chunks(chunk_id="chunk123", relation_type="sequential")`
|
||||
12. **Observe**: 获得上下文
|
||||
13. **Final Answer**: 基于详细信息生成深入解释
|
||||
|
||||
**迭代次数**: 7
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Agent 和普通 RAG 有什么区别?
|
||||
|
||||
**A**:
|
||||
- **普通 RAG**: 一次性检索 → 生成答案
|
||||
- **Agent**: 可以多次迭代,根据中间结果调整策略,调用不同工具
|
||||
|
||||
### Q: 什么时候使用 Agent 模式?
|
||||
|
||||
**A**:
|
||||
- 需要多步推理的复杂问题
|
||||
- 需要在多个知识库中查找信息
|
||||
- 需要深入探索和关联分析
|
||||
- 问题模糊,需要澄清和逐步细化
|
||||
|
||||
### Q: Agent 模式的成本如何?
|
||||
|
||||
**A**:
|
||||
- **Token 消耗**: 比普通 RAG 高 (多次 LLM 调用)
|
||||
- **响应时间**: 较长 (迭代执行)
|
||||
- **准确性**: 通常更高 (多步验证)
|
||||
|
||||
### Q: 如何优化 Agent 性能?
|
||||
|
||||
**A**:
|
||||
1. 合理设置 `max_iterations` (避免过多)
|
||||
2. 选择必要的工具 (`allowed_tools`)
|
||||
3. 禁用不需要的功能 (planning, reflection)
|
||||
4. 使用更快的模型作为 thinking_model
|
||||
|
||||
### Q: 工具调用失败怎么办?
|
||||
|
||||
**A**: Agent 会:
|
||||
1. 在 Observation 中记录错误
|
||||
2. 尝试使用其他工具
|
||||
3. 如果多次失败,会在最终答案中说明
|
||||
|
||||
## 技术限制与注意事项
|
||||
|
||||
### 当前限制
|
||||
|
||||
1. **工具调用解析**: 使用简单的模式匹配,可能无法处理复杂格式
|
||||
2. **Function Calling**: 当前使用 prompt-based 方式,未来可升级为原生 function calling
|
||||
3. **并行工具调用**: 当前不支持,工具按顺序执行
|
||||
4. **图谱查询**: 当前使用 hybrid search,完整图谱功能开发中
|
||||
|
||||
### 未来改进
|
||||
|
||||
- [ ] 支持 LLM 原生 function calling (GPT-4, Claude 等)
|
||||
- [ ] 并行工具执行
|
||||
- [ ] 更智能的循环检测
|
||||
- [ ] 工具结果缓存
|
||||
- [ ] 自定义工具注册
|
||||
- [ ] Agent 执行可视化 UI
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 创建自定义工具
|
||||
|
||||
```go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
type CustomTool struct {
|
||||
BaseTool
|
||||
// 添加依赖
|
||||
}
|
||||
|
||||
func NewCustomTool() *CustomTool {
|
||||
return &CustomTool{
|
||||
BaseTool: NewBaseTool(
|
||||
"custom_tool",
|
||||
"工具描述",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CustomTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"param1": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "参数描述",
|
||||
},
|
||||
},
|
||||
"required": []string{"param1"},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CustomTool) Execute(ctx context.Context, args map[string]interface{}) (*types.ToolResult, error) {
|
||||
// 实现工具逻辑
|
||||
param1 := args["param1"].(string)
|
||||
|
||||
// 执行操作
|
||||
result := doSomething(param1)
|
||||
|
||||
return &types.ToolResult{
|
||||
Success: true,
|
||||
Output: result,
|
||||
Data: map[string]interface{}{},
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 注册自定义工具
|
||||
|
||||
在 `agent_service.go` 的 `registerTools` 方法中添加:
|
||||
|
||||
```go
|
||||
case "custom_tool":
|
||||
registry.RegisterTool(tools.NewCustomTool())
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
WeKnora Agent Mode 提供了强大的迭代推理能力,适用于复杂的知识查询场景。通过合理配置和工具选择,可以在准确性和效率之间找到最佳平衡点。
|
||||
|
||||
如有问题或建议,请提交 Issue 或 Pull Request。
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
# LLM Context Manager - 大模型上下文管理器
|
||||
|
||||
## 概述
|
||||
|
||||
LLM Context Manager 是一个专门用于管理大模型对话上下文的组件,它**独立于消息存储系统**,专注于管理发送给大模型的上下文窗口。
|
||||
|
||||
### 关键特性
|
||||
|
||||
1. **独立管理**: 与消息的数据库存储分离,专门管理发送给 LLM 的上下文
|
||||
2. **Token 限制管理**: 自动监控和管理上下文的 Token 数量
|
||||
3. **智能压缩**: 当上下文超出限制时,自动应用压缩策略
|
||||
4. **按 Session 维护**: 每个会话独立管理其上下文
|
||||
5. **灵活配置**: 支持会话级别的自定义配置
|
||||
|
||||
## 为什么需要 Context Manager?
|
||||
|
||||
### 问题背景
|
||||
|
||||
- **消息存储** vs **LLM 上下文**:
|
||||
- 消息存储:完整保存所有对话历史,用于展示和审计
|
||||
- LLM 上下文:有 Token 限制,需要精简管理
|
||||
|
||||
- **Token 限制**: 不同模型有不同的上下文窗口限制(如 4K, 8K, 16K tokens)
|
||||
|
||||
- **性能优化**: 过长的上下文会增加推理时间和成本
|
||||
|
||||
### 解决方案
|
||||
|
||||
Context Manager 提供:
|
||||
- 自动管理上下文窗口大小
|
||||
- 智能压缩历史消息
|
||||
- 保留最重要的上下文信息
|
||||
- 与消息存储解耦
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Service │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Message Repo │ │ Context Manager │ │
|
||||
│ │ (Complete │ │ (LLM Context │ │
|
||||
│ │ History) │ │ Window) │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ Save all messages │ Manage LLM │
|
||||
│ │ for display │ context │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Database │ │ In-Memory │ │
|
||||
│ │ (Persistent) │ │ (Session-based) │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 压缩策略
|
||||
|
||||
### 1. 滑动窗口策略 (Sliding Window)
|
||||
|
||||
保留最近的 N 条消息,丢弃更早的消息。
|
||||
|
||||
**优点:**
|
||||
- 简单高效
|
||||
- 不需要额外的 LLM 调用
|
||||
- 适合短期对话
|
||||
|
||||
**配置示例:**
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"max_tokens": 8192,
|
||||
"compression_strategy": "sliding_window",
|
||||
"recent_message_count": 20
|
||||
}
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
```
|
||||
原始消息: [system, msg1, msg2, msg3, ..., msg18, msg19, msg20, msg21, msg22]
|
||||
↑
|
||||
保留最近20条消息
|
||||
压缩结果: [system, msg3, msg4, ..., msg20, msg21, msg22]
|
||||
└─────┘ └──────────────────────────────────┘
|
||||
保留系统消息 保留最近的20条消息
|
||||
```
|
||||
|
||||
### 2. 智能压缩策略 (Smart Compression)
|
||||
|
||||
使用 LLM 总结旧消息,保留最近消息的完整内容。
|
||||
|
||||
**优点:**
|
||||
- 保留历史关键信息
|
||||
- 更好的上下文连贯性
|
||||
- 适合长期对话
|
||||
|
||||
**配置示例:**
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"max_tokens": 8192,
|
||||
"compression_strategy": "smart",
|
||||
"recent_message_count": 10
|
||||
}
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
```
|
||||
原始消息: [system, msg1, msg2, ..., msg15, msg16, msg17, msg18, msg19, msg20]
|
||||
└───────────────┘ └─────────────────────────────┘
|
||||
旧消息(总结) 最近消息(保留)
|
||||
|
||||
压缩结果: [system, summary, msg16, msg17, msg18, msg19, msg20]
|
||||
└─────┘ └──────┘ └──────────────────────────────┘
|
||||
系统消息 总结 保留最近的10条消息(完整)
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 默认配置
|
||||
|
||||
如果不设置 `context_config`,系统使用默认配置:
|
||||
- 最大 Token: 8192
|
||||
- 策略: 滑动窗口
|
||||
- 保留消息数: 20
|
||||
|
||||
```go
|
||||
// 不需要特别配置,自动使用默认设置
|
||||
session := &types.Session{
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
// context_config 为 nil,使用默认配置
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自定义配置 - 滑动窗口
|
||||
|
||||
```go
|
||||
session := &types.Session{
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
ContextConfig: &types.ContextConfig{
|
||||
Enabled: true,
|
||||
MaxTokens: 16384, // GPT-4 的上下文窗口
|
||||
CompressionStrategy: types.ContextCompressionSlidingWindow,
|
||||
RecentMessageCount: 30, // 保留最近30条消息
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 自定义配置 - 智能压缩
|
||||
|
||||
```go
|
||||
session := &types.Session{
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
SummaryModelID: "gpt-4", // 用于总结的模型
|
||||
ContextConfig: &types.ContextConfig{
|
||||
Enabled: true,
|
||||
MaxTokens: 8192,
|
||||
CompressionStrategy: types.ContextCompressionSmart,
|
||||
RecentMessageCount: 15, // 保留最近15条完整消息
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 通过 API 创建/更新会话
|
||||
|
||||
```bash
|
||||
# 创建带上下文配置的会话
|
||||
curl -X POST http://localhost:8080/api/v1/sessions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Tenant-ID: 1" \
|
||||
-d '{
|
||||
"knowledge_base_id": "kb-123",
|
||||
"context_config": {
|
||||
"enabled": true,
|
||||
"max_tokens": 8192,
|
||||
"compression_strategy": "sliding_window",
|
||||
"recent_message_count": 20
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 对话流程
|
||||
|
||||
```
|
||||
1. 用户发送消息
|
||||
↓
|
||||
2. 保存消息到数据库 (Message Repo)
|
||||
↓
|
||||
3. 添加消息到上下文管理器 (Context Manager)
|
||||
↓
|
||||
4. Context Manager 检查 Token 限制
|
||||
↓
|
||||
5. 如果超出限制,应用压缩策略
|
||||
↓
|
||||
6. 获取压缩后的上下文
|
||||
↓
|
||||
7. 发送给 LLM
|
||||
↓
|
||||
8. 保存响应到数据库
|
||||
↓
|
||||
9. 添加响应到上下文管理器
|
||||
```
|
||||
|
||||
### 代码示例
|
||||
|
||||
```go
|
||||
// 在 AgentQA 中的使用
|
||||
func (s *sessionService) AgentQA(ctx context.Context, sessionID, query string, assistantMessageID string) (
|
||||
[]*types.SearchResult, <-chan types.StreamResponse, error,
|
||||
) {
|
||||
// 1. 获取会话配置
|
||||
session, err := s.sessionRepo.Get(ctx, tenantID, sessionID)
|
||||
|
||||
// 2. 从上下文管理器获取 LLM 上下文(自动应用压缩)
|
||||
history, err := s.getContextForSession(ctx, session, sessionID)
|
||||
// history 是压缩后的,适合发送给 LLM 的消息列表
|
||||
|
||||
// 3. 执行 Agent
|
||||
eventChan, err := engine.ExecuteStreamWithHistory(ctx, query, history)
|
||||
|
||||
// 4. 保存新消息后,添加到上下文管理器
|
||||
// s.AddMessageToContext(ctx, session, sessionID, newMessage)
|
||||
|
||||
return searchResults, responseChan, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 配置参数说明
|
||||
|
||||
### ContextConfig 字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `enabled` | bool | 是否启用上下文管理 | true |
|
||||
| `max_tokens` | int | 最大 Token 数 | 8192 |
|
||||
| `compression_strategy` | string | 压缩策略: "sliding_window" 或 "smart" | "sliding_window" |
|
||||
| `recent_message_count` | int | 保留的最近消息数 | 20 (sliding_window) <br> 10 (smart) |
|
||||
|
||||
### 推荐配置
|
||||
|
||||
#### 短期对话(客服、问答)
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"max_tokens": 4096,
|
||||
"compression_strategy": "sliding_window",
|
||||
"recent_message_count": 15
|
||||
}
|
||||
```
|
||||
|
||||
#### 长期对话(咨询、助手)
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"max_tokens": 8192,
|
||||
"compression_strategy": "smart",
|
||||
"recent_message_count": 10
|
||||
}
|
||||
```
|
||||
|
||||
#### 高端模型(GPT-4 Turbo, Claude)
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"max_tokens": 16384,
|
||||
"compression_strategy": "smart",
|
||||
"recent_message_count": 20
|
||||
}
|
||||
```
|
||||
|
||||
## 监控和调试
|
||||
|
||||
### 查看上下文统计
|
||||
|
||||
```go
|
||||
// 获取上下文统计信息
|
||||
stats, err := contextManager.GetContextStats(ctx, sessionID)
|
||||
if err == nil {
|
||||
log.Printf("Session %s context stats:", sessionID)
|
||||
log.Printf(" Messages: %d", stats.MessageCount)
|
||||
log.Printf(" Tokens: ~%d", stats.TokenCount)
|
||||
log.Printf(" Compressed: %v", stats.IsCompressed)
|
||||
log.Printf(" Original messages: %d", stats.OriginalMessageCount)
|
||||
}
|
||||
```
|
||||
|
||||
### 日志示例
|
||||
|
||||
```
|
||||
INFO Using custom context config for session abc123: strategy=smart, max_tokens=8192, recent_count=10
|
||||
INFO Context exceeds max tokens (9500 > 8192), applying compression
|
||||
INFO Summarizing 15 old messages
|
||||
INFO Successfully summarized 15 messages
|
||||
INFO Smart compression: 25 -> 11 messages (system: 1, compressed: 1, recent: 10)
|
||||
INFO LLM context stats for session abc123: messages=11, tokens=~7800, compressed=true
|
||||
```
|
||||
|
||||
## 与消息存储的区别
|
||||
|
||||
| 特性 | 消息存储 (Message Repo) | 上下文管理器 (Context Manager) |
|
||||
|------|------------------------|------------------------------|
|
||||
| 目的 | 完整保存对话历史 | 管理 LLM 输入上下文 |
|
||||
| 存储 | 数据库(持久化) | 内存(会话级别) |
|
||||
| 内容 | 所有消息(完整) | 压缩后的消息 |
|
||||
| 大小限制 | 无限制 | 受 Token 限制 |
|
||||
| 使用场景 | 展示历史、审计 | LLM 推理输入 |
|
||||
| 生命周期 | 永久保存 | 会话期间 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **选择合适的策略**:
|
||||
- 短对话 → 滑动窗口(性能更好)
|
||||
- 长对话 → 智能压缩(保留更多上下文)
|
||||
|
||||
2. **配置 Token 限制**:
|
||||
- 设置为模型上下文窗口的 70-80%
|
||||
- 为响应留出足够空间
|
||||
|
||||
3. **调整保留消息数**:
|
||||
- 滑动窗口: 15-30 条消息
|
||||
- 智能压缩: 8-15 条最近消息
|
||||
|
||||
4. **监控压缩效果**:
|
||||
- 定期检查 Token 使用情况
|
||||
- 观察压缩是否影响对话质量
|
||||
|
||||
5. **性能考虑**:
|
||||
- 智能压缩会额外调用 LLM(有成本)
|
||||
- 滑动窗口无额外开销
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
运行以下迁移脚本添加 `context_config` 字段:
|
||||
|
||||
### MySQL
|
||||
```bash
|
||||
mysql -u root -p your_database < migrations/mysql/06-add-context-config-to-sessions.sql
|
||||
```
|
||||
|
||||
### ParadeDB/PostgreSQL
|
||||
```bash
|
||||
psql -U postgres -d your_database -f migrations/paradedb/06-add-context-config-to-sessions.sql
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 上下文管理器的数据会持久化吗?
|
||||
**A**: 不会。上下文管理器是内存级别的,按 Session 维护。重启后会清空,需要从消息历史重新构建。
|
||||
|
||||
### Q2: 如何清空某个会话的上下文?
|
||||
**A**:
|
||||
```go
|
||||
err := contextManager.ClearContext(ctx, sessionID)
|
||||
```
|
||||
|
||||
### Q3: 压缩后的消息会影响数据库中的消息吗?
|
||||
**A**: 不会。压缩只影响发送给 LLM 的上下文,数据库中的消息完整保留。
|
||||
|
||||
### Q4: 智能压缩的总结质量如何保证?
|
||||
**A**:
|
||||
- 使用会话配置的 `summary_model_id` 模型
|
||||
- 可以使用更强的模型(如 GPT-4)进行总结
|
||||
- 总结时使用低 temperature (0.3) 确保一致性
|
||||
|
||||
### Q5: 如何为现有会话启用上下文管理?
|
||||
**A**:
|
||||
```bash
|
||||
# 更新会话配置
|
||||
curl -X PUT http://localhost:8080/api/v1/sessions/{session_id} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Tenant-ID: 1" \
|
||||
-d '{
|
||||
"context_config": {
|
||||
"enabled": true,
|
||||
"max_tokens": 8192,
|
||||
"compression_strategy": "sliding_window",
|
||||
"recent_message_count": 20
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 未来改进
|
||||
|
||||
- [ ] 支持更多压缩策略(如 MapReduce 总结)
|
||||
- [ ] 支持自定义 Token 计算器
|
||||
- [ ] 持久化压缩后的上下文摘要
|
||||
- [ ] 支持跨会话的上下文共享
|
||||
- [ ] 添加更详细的压缩指标和监控
|
||||
- [ ] 支持上下文恢复机制
|
||||
|
||||
## 参考
|
||||
|
||||
- [Agent 文档](./AGENT.md)
|
||||
- [API 文档](./API.md)
|
||||
- [消息管理](../internal/types/message.go)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
## MCP 功能使用说明
|
||||
|
||||
### 功能概述
|
||||
- MCP(Model Context Protocol)让 WeKnora 可以安全地连接外部工具或数据源,扩展 Agent 在推理时可调用的能力。
|
||||
- 在前端 `设置 > MCP 服务`(`frontend/src/views/settings/McpSettings.vue`)中集中管理所有服务,无需手动改配置文件。
|
||||
- 每个服务都包含名称、传输方式(SSE / HTTP Streamable / Stdio)、连接地址或命令、认证信息以及高级超时与重试策略。
|
||||
|
||||
### 入口与界面
|
||||
- 打开控制台左侧菜单 `设置 -> MCP 服务`,即可看到当前租户下的所有 MCP 服务列表。
|
||||
- 列表中可快速启停服务、查看描述,并通过右侧菜单执行“测试 / 编辑 / 删除”。
|
||||
- “添加服务”按钮会弹出 `McpServiceDialog`,用于创建或修改服务。
|
||||
|
||||
### 常用操作流程
|
||||
1. **新建服务**
|
||||
- 点击“添加服务”,填写名称与描述,选择传输方式。
|
||||
- SSE / HTTP Streamable 需提供可访问的服务 URL;Stdio 需配置 `uvx`/`npx` 命令与参数,可附加环境变量。
|
||||
- 根据需要填写 API Key、Bearer Token、超时与重试策略,保存后服务会出现在列表中。
|
||||
2. **启停服务**
|
||||
- 在列表开关中切换启用状态,系统会即时调用后端 `updateMCPService`,失败时会自动回滚状态并弹出提示。
|
||||
3. **连接测试**
|
||||
- 通过更多菜单选择“测试”,前端会调用 `/api/v1/mcp-services/{id}/test` 并弹出 `McpTestResult`。
|
||||
- 成功时会展示服务可用的工具清单(含输入 schema)和资源列表;失败时会显示错误信息,方便排查网络或鉴权问题。
|
||||
4. **编辑 / 删除**
|
||||
- “编辑”会带出原有配置,修改后保存即可。
|
||||
- “删除”需要在弹窗中确认,完成后列表自动刷新。
|
||||
|
||||
### 使用建议
|
||||
- **传输方式选择**:优先使用 SSE 获取流式体验;需要标准 HTTP Streamable 兼容时再切换;本地调试或离线环境适合使用 Stdio 并在同机启动 MCP Server。
|
||||
- **鉴权管理**:将 API Key / Token 保存在“认证配置”中,生产环境建议单独创建最小权限 Key,并定期轮换。
|
||||
- **重试策略**:对公网或第三方服务适当提高 `retry_count` 与 `retry_delay`,避免间歇性超时导致 Agent 中断
|
||||
+2
-151
@@ -1,74 +1,7 @@
|
||||
# 快速开发模式说明
|
||||
|
||||
## 🎯 问题背景
|
||||
解决开发流程中,每次修改 `app`(后端)或 `frontend`(前端)代码后,都需要打包Docker镜像的问题,实现这两个模块的热更新
|
||||
|
||||
之前的开发流程中,每次修改 `app`(后端)或 `frontend`(前端)代码后,都需要:
|
||||
|
||||
```bash
|
||||
# 重新构建 Docker 镜像(耗时 2-5 分钟)
|
||||
sh scripts/build_images.sh -p # 构建后端
|
||||
sh scripts/build_images.sh -f # 构建前端
|
||||
|
||||
# 重启容器
|
||||
sh scripts/start_all.sh --no-pull
|
||||
```
|
||||
|
||||
这个流程非常耗时,严重影响开发效率。
|
||||
|
||||
## ✨ 解决方案
|
||||
|
||||
现在提供了**快速开发模式**,可以直接在本地运行应用和前端,只在 Docker 中启动基础设施服务(数据库、缓存等),实现:
|
||||
|
||||
- ✅ **前端热重载**:修改代码自动刷新,无需重启
|
||||
- ✅ **后端快速重启**:修改代码后 5-10 秒即可重启
|
||||
- ✅ **无需构建镜像**:跳过耗时的镜像构建过程
|
||||
- ✅ **支持调试**:可以使用 IDE 断点调试
|
||||
|
||||
## 📦 新增文件
|
||||
|
||||
### 1. 核心配置文件
|
||||
|
||||
- **`docker-compose.dev.yml`** - 开发环境 Docker Compose 配置
|
||||
- 只启动基础设施服务(postgres, redis, minio, neo4j, docreader, jaeger)
|
||||
- 不启动 app 和 frontend 容器
|
||||
|
||||
### 2. 开发脚本
|
||||
|
||||
- **`scripts/dev.sh`** - 开发环境管理脚本
|
||||
- 支持启动/停止基础设施
|
||||
- 支持本地运行 app 和 frontend
|
||||
- 自动检测并使用 Air(Go 热重载工具)
|
||||
|
||||
- **`scripts/quick-dev.sh`** - 一键启动开发环境脚本
|
||||
- 交互式启动所有服务
|
||||
- 自动创建日志目录
|
||||
|
||||
### 3. 配置文件
|
||||
|
||||
- **`.air.toml`** - Air 热重载配置
|
||||
- 监控 Go 文件变化
|
||||
- 自动重新编译和重启
|
||||
|
||||
- **`frontend/vite.config.ts`** - 更新的 Vite 配置
|
||||
- 添加 API 代理配置
|
||||
- 避免 CORS 问题
|
||||
|
||||
### 4. 文档
|
||||
|
||||
- **`DEVELOPMENT.md`** - 开发环境快速入门指南
|
||||
- **`docs/开发指南.md`** - 完整开发指南
|
||||
- **`docs/快速开发模式说明.md`** - 本文档
|
||||
|
||||
### 5. Makefile 更新
|
||||
|
||||
新增的 Make 命令:
|
||||
- `make dev-start` - 启动开发环境基础设施
|
||||
- `make dev-stop` - 停止开发环境
|
||||
- `make dev-restart` - 重启开发环境
|
||||
- `make dev-logs` - 查看服务日志
|
||||
- `make dev-status` - 查看服务状态
|
||||
- `make dev-app` - 启动后端应用(本地)
|
||||
- `make dev-frontend` - 启动前端(本地)
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
@@ -104,28 +37,7 @@ make dev-frontend
|
||||
./scripts/quick-dev.sh
|
||||
```
|
||||
|
||||
## 📊 效率对比
|
||||
|
||||
### 旧方式(慢)
|
||||
|
||||
| 操作 | 耗时 |
|
||||
|------|------|
|
||||
| 修改代码 | - |
|
||||
| 重新构建镜像 | 2-5 分钟 |
|
||||
| 重启容器 | 30-60 秒 |
|
||||
| **总计** | **2.5-6 分钟** |
|
||||
|
||||
### 新方式(快)
|
||||
|
||||
| 操作 | 耗时 |
|
||||
|------|------|
|
||||
| 首次启动基础设施 | 1-2 分钟(仅一次) |
|
||||
| **后续修改后端** | **5-10 秒** |
|
||||
| **后续修改前端** | **实时热重载** |
|
||||
|
||||
**效率提升:20-60 倍!**
|
||||
|
||||
## 🎓 高级功能
|
||||
|
||||
### 使用 Air 实现后端热重载
|
||||
|
||||
@@ -142,32 +54,6 @@ export PATH=$PATH:$(go env GOPATH)/bin
|
||||
make dev-app
|
||||
```
|
||||
|
||||
### VS Code 调试配置
|
||||
|
||||
创建 `.vscode/launch.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch WeKnora Server",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"env": {
|
||||
"DB_HOST": "localhost",
|
||||
"DOCREADER_ADDR": "localhost:50051",
|
||||
"MINIO_ENDPOINT": "localhost:9000",
|
||||
"REDIS_ADDR": "localhost:6379",
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
|
||||
"NEO4J_URI": "bolt://localhost:7687"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 架构说明
|
||||
|
||||
@@ -217,39 +103,4 @@ make dev-app
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 日常开发
|
||||
|
||||
使用**开发模式**(`make dev-*`),享受快速迭代。
|
||||
|
||||
### 集成测试
|
||||
|
||||
使用**生产模式**(`make start-all`),测试完整环境。
|
||||
|
||||
### 生产部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
make build-images
|
||||
|
||||
# 启动服务
|
||||
make start-all
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
详见 [DEVELOPMENT.md](../DEVELOPMENT.md) 的故障排除部分。
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [开发环境快速入门](../DEVELOPMENT.md)
|
||||
- [完整开发指南](./开发指南.md)
|
||||
- [API 文档](./API.md)
|
||||
|
||||
## 💬 反馈与建议
|
||||
|
||||
如有问题或建议,请提交 [Issue](https://github.com/Tencent/WeKnora/issues)。
|
||||
|
||||
```
|
||||
@@ -43,6 +43,22 @@ export interface ConversationConfig {
|
||||
context_template: string
|
||||
temperature: number
|
||||
max_tokens: number
|
||||
use_custom_system_prompt?: boolean
|
||||
use_custom_context_template?: boolean
|
||||
max_rounds: number
|
||||
embedding_top_k: number
|
||||
keyword_threshold: number
|
||||
vector_threshold: number
|
||||
rerank_top_k: number
|
||||
rerank_threshold: number
|
||||
enable_rewrite: boolean
|
||||
fallback_strategy: string
|
||||
fallback_response: string
|
||||
fallback_prompt?: string
|
||||
summary_model_id?: string
|
||||
rerank_model_id?: string
|
||||
rewrite_prompt_system?: string
|
||||
rewrite_prompt_user?: string
|
||||
}
|
||||
|
||||
export function getSystemInfo(): Promise<{ data: SystemInfo }> {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div v-if="menuVisible" class="user-dropdown" @click.stop>
|
||||
<div class="menu-item" @click="handleQuickNav('models')">
|
||||
<t-icon name="control-platform" class="menu-icon" />
|
||||
<span>{{ $t('settings.modelConfig') }}</span>
|
||||
<span>{{ $t('settings.modelManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="handleQuickNav('ollama')">
|
||||
<t-icon name="server" class="menu-icon" />
|
||||
|
||||
@@ -152,8 +152,10 @@ export default {
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
modelConfig: 'Model Settings',
|
||||
modelManagement: 'Model Management',
|
||||
agentConfig: 'Agent Settings',
|
||||
conversationConfig: 'Conversation Settings',
|
||||
conversationStrategy: 'Conversation Strategy',
|
||||
webSearchConfig: 'Web Search',
|
||||
mcpService: 'MCP Service',
|
||||
systemSettings: 'System Settings',
|
||||
@@ -833,8 +835,8 @@ export default {
|
||||
title: 'Model Configuration',
|
||||
description: 'Select appropriate AI models for the knowledge base',
|
||||
llmLabel: 'LLM Model',
|
||||
llmDesc: 'Large language model used for conversations and Q&A',
|
||||
llmPlaceholder: 'Select an LLM model',
|
||||
llmDesc: 'Large language model used for summarization and abstract generation (optional)',
|
||||
llmPlaceholder: 'Select an LLM model (optional)',
|
||||
embeddingLabel: 'Embedding Model',
|
||||
embeddingDesc: 'Embedding model used for text vectorization',
|
||||
embeddingPlaceholder: 'Select an embedding model',
|
||||
@@ -1368,18 +1370,107 @@ export default {
|
||||
}
|
||||
},
|
||||
conversationSettings: {
|
||||
description: 'Configure default behavior and parameters for conversation modes, including Prompt settings for Agent mode and normal mode',
|
||||
description: 'Configure default behavior and parameters for conversation modes, including prompts for Agent and normal modes',
|
||||
agentMode: 'Agent Mode',
|
||||
normalMode: 'Normal Mode',
|
||||
menus: {
|
||||
modes: 'Mode Settings',
|
||||
models: 'Model Mapping',
|
||||
thresholds: 'Retrieval Thresholds',
|
||||
advanced: 'Advanced Settings'
|
||||
},
|
||||
models: {
|
||||
description: 'Manage thinking/chat models and re-rank models for both Agent and normal modes',
|
||||
chatGroupLabel: 'Thinking / Chat Models',
|
||||
chatGroupDesc: 'Includes Agent reasoning/planning model and the default chat/summary model for normal mode',
|
||||
chatModel: {
|
||||
label: 'Default chat model (normal mode)',
|
||||
desc: 'Used when a conversation does not specify its own model',
|
||||
placeholder: 'Select default chat model'
|
||||
},
|
||||
rerankModel: {
|
||||
label: 'Default ReRank model (normal mode)',
|
||||
desc: 'Used for re-ranking when a session does not override it',
|
||||
placeholder: 'Select default rerank model'
|
||||
},
|
||||
rerankGroupLabel: 'ReRank Models',
|
||||
rerankGroupDesc: 'Includes Agent rerank model and the default rerank model for normal mode'
|
||||
},
|
||||
thresholds: {
|
||||
description: 'Tune retrieval and re-ranking thresholds to balance accuracy and performance'
|
||||
},
|
||||
maxRounds: {
|
||||
label: 'History Rounds',
|
||||
desc: 'Number of rounds kept for context and query rewrite'
|
||||
},
|
||||
embeddingTopK: {
|
||||
label: 'Embedding TopK',
|
||||
desc: 'Number of documents kept after vector retrieval'
|
||||
},
|
||||
keywordThreshold: {
|
||||
label: 'Keyword Threshold',
|
||||
desc: 'Minimum score for keyword retrieval'
|
||||
},
|
||||
vectorThreshold: {
|
||||
label: 'Vector Threshold',
|
||||
desc: 'Minimum similarity for vector retrieval'
|
||||
},
|
||||
rerankTopK: {
|
||||
label: 'ReRank TopK',
|
||||
desc: 'Documents kept after re-ranking'
|
||||
},
|
||||
rerankThreshold: {
|
||||
label: 'ReRank Threshold',
|
||||
desc: 'Minimum score required after re-ranking'
|
||||
},
|
||||
enableRewrite: {
|
||||
label: 'Enable Query Rewrite',
|
||||
desc: 'Automatically rewrite multi-turn queries for better recall'
|
||||
},
|
||||
fallbackStrategy: {
|
||||
label: 'Fallback Strategy',
|
||||
desc: 'How to respond when no relevant documents are found',
|
||||
fixed: 'Fixed response',
|
||||
model: 'Let the model continue answering'
|
||||
},
|
||||
fallbackResponse: {
|
||||
label: 'Fixed fallback response',
|
||||
desc: 'Text returned when using the fixed fallback strategy'
|
||||
},
|
||||
fallbackPrompt: {
|
||||
label: 'Fallback Prompt',
|
||||
desc: 'Prompt used when fallback strategy is “model”'
|
||||
},
|
||||
advanced: {
|
||||
description: 'Configure query rewrite, fallback strategy and other advanced settings'
|
||||
},
|
||||
rewritePrompt: {
|
||||
system: 'Rewrite System Prompt',
|
||||
user: 'Rewrite User Prompt',
|
||||
desc: 'System prompt used during query rewrite',
|
||||
userDesc: 'User prompt used during query rewrite'
|
||||
},
|
||||
chatModel: {
|
||||
label: 'LLM Model',
|
||||
desc: 'Large language model used for summarization and abstract generation'
|
||||
},
|
||||
rerankModel: {
|
||||
label: 'ReRank Model',
|
||||
desc: 'Model for re-ranking search results (optional)'
|
||||
},
|
||||
contextTemplate: {
|
||||
label: 'Retrieval Result Summary Prompt',
|
||||
desc: 'Prompt template for generating answers based on retrieval results in normal mode',
|
||||
placeholder: 'Enter the prompt template for retrieval result summary...'
|
||||
placeholder: 'Enter the prompt template for retrieval result summary...',
|
||||
custom: 'Custom template',
|
||||
disabledHint: 'Currently using the default summary prompt. Enable custom to edit below.'
|
||||
},
|
||||
systemPrompt: {
|
||||
label: 'System Prompt',
|
||||
desc: 'System-level prompt for normal mode conversations',
|
||||
placeholder: 'Enter the system prompt...'
|
||||
placeholder: 'Enter the system prompt...',
|
||||
custom: 'Custom prompt',
|
||||
disabledHint: 'Currently using the default prompt. Enable custom to edit below.'
|
||||
},
|
||||
temperature: {
|
||||
label: 'Temperature',
|
||||
@@ -1389,11 +1480,39 @@ export default {
|
||||
label: 'Max Tokens',
|
||||
desc: 'Maximum number of tokens to generate in the response'
|
||||
},
|
||||
resetSystemPrompt: {
|
||||
header: 'Reset to Default System Prompt',
|
||||
body: 'Are you sure you want to reset to the default system prompt?'
|
||||
},
|
||||
resetContextTemplate: {
|
||||
header: 'Reset to Default Summary Prompt',
|
||||
body: 'Are you sure you want to reset to the default summary prompt?'
|
||||
},
|
||||
toasts: {
|
||||
chatModelSaved: 'LLM model saved',
|
||||
rerankModelSaved: 'ReRank model saved',
|
||||
contextTemplateSaved: 'Retrieval result summary prompt saved',
|
||||
systemPromptSaved: 'System prompt saved',
|
||||
temperatureSaved: 'Temperature saved',
|
||||
maxTokensSaved: 'Max tokens saved'
|
||||
maxTokensSaved: 'Max tokens saved',
|
||||
maxRoundsSaved: 'History rounds saved',
|
||||
embeddingSaved: 'Embedding TopK saved',
|
||||
keywordThresholdSaved: 'Keyword threshold saved',
|
||||
vectorThresholdSaved: 'Vector threshold saved',
|
||||
rerankTopKSaved: 'ReRank TopK saved',
|
||||
rerankThresholdSaved: 'ReRank threshold saved',
|
||||
enableRewriteSaved: 'Query rewrite preference saved',
|
||||
fallbackStrategySaved: 'Fallback strategy saved',
|
||||
fallbackResponseSaved: 'Fallback response saved',
|
||||
fallbackPromptSaved: 'Fallback prompt saved',
|
||||
rewritePromptSystemSaved: 'Rewrite system prompt saved',
|
||||
rewritePromptUserSaved: 'Rewrite user prompt saved',
|
||||
customPromptEnabled: 'Custom prompt enabled',
|
||||
defaultPromptEnabled: 'Using default prompt',
|
||||
customContextTemplateEnabled: 'Custom summary prompt enabled',
|
||||
defaultContextTemplateEnabled: 'Using default summary prompt',
|
||||
resetSystemPromptSuccess: 'Reset to default system prompt',
|
||||
resetContextTemplateSuccess: 'Reset to default summary prompt'
|
||||
}
|
||||
},
|
||||
// New: MCP Settings
|
||||
|
||||
@@ -151,9 +151,12 @@ export default {
|
||||
settings: {
|
||||
title: 'Настройки',
|
||||
modelConfig: 'Настройки модели',
|
||||
modelManagement: 'Управление моделями',
|
||||
agentConfig: 'Настройки агента',
|
||||
webSearchConfig: 'Сетевой поиск',
|
||||
mcpService: 'Сервис MCP',
|
||||
conversationConfig: 'Настройки диалога',
|
||||
conversationStrategy: 'Стратегия диалога',
|
||||
systemSettings: 'Настройки системы',
|
||||
tenantInfo: 'Информация о арендаторе',
|
||||
apiInfo: 'Информация API',
|
||||
|
||||
@@ -243,8 +243,10 @@ export default {
|
||||
settings: {
|
||||
title: "设置",
|
||||
modelConfig: "模型配置",
|
||||
modelManagement: "模型管理",
|
||||
agentConfig: "Agent配置",
|
||||
conversationConfig: "对话设置",
|
||||
conversationStrategy: "对话策略",
|
||||
webSearchConfig: "网络搜索",
|
||||
mcpService: "MCP服务",
|
||||
systemSettings: "系统设置",
|
||||
@@ -1156,8 +1158,8 @@ export default {
|
||||
title: "模型配置",
|
||||
description: "为知识库选择合适的 AI 模型",
|
||||
llmLabel: "LLM 大语言模型",
|
||||
llmDesc: "用于对话和问答的大语言模型",
|
||||
llmPlaceholder: "请选择 LLM 模型",
|
||||
llmDesc: "用于总结和摘要的大语言模型(可选)",
|
||||
llmPlaceholder: "请选择 LLM 模型(可选)",
|
||||
embeddingLabel: "Embedding 嵌入模型",
|
||||
embeddingDesc: "用于文本向量化的嵌入模型",
|
||||
embeddingPlaceholder: "请选择 Embedding 模型",
|
||||
@@ -1375,15 +1377,104 @@ export default {
|
||||
description: "配置对话模式的默认行为和参数,包括Agent模式和普通模式的Prompt设置",
|
||||
agentMode: "Agent模式",
|
||||
normalMode: "普通模式",
|
||||
menus: {
|
||||
modes: "模式设置",
|
||||
models: "模型配置",
|
||||
thresholds: "检索阈值",
|
||||
advanced: "高级设置",
|
||||
},
|
||||
models: {
|
||||
description: "统一管理 Agent 和普通模式使用的对话/总结模型与 ReRank 模型",
|
||||
chatGroupLabel: "思考 / 对话模型",
|
||||
chatGroupDesc: "包含 Agent 推理与规划模型,以及普通模式默认的对话/总结模型",
|
||||
chatModel: {
|
||||
label: "普通模式默认对话模型",
|
||||
desc: "普通模式默认使用的对话/总结模型,当会话未指定模型时生效",
|
||||
placeholder: "请选择默认对话模型",
|
||||
},
|
||||
rerankModel: {
|
||||
label: "普通模式默认 ReRank 模型",
|
||||
desc: "普通模式默认使用的重排序模型",
|
||||
placeholder: "请选择默认 ReRank 模型",
|
||||
},
|
||||
rerankGroupLabel: "ReRank 模型",
|
||||
rerankGroupDesc: "包含 Agent 使用的重排序模型,以及普通模式默认 ReRank 模型",
|
||||
},
|
||||
thresholds: {
|
||||
description: "调整召回与重排序的阈值与 TopK,平衡准确率与性能",
|
||||
},
|
||||
maxRounds: {
|
||||
label: "历史保留轮数",
|
||||
desc: "用于多轮上下文和问题改写的历史轮数",
|
||||
},
|
||||
embeddingTopK: {
|
||||
label: "Embedding TopK",
|
||||
desc: "向量召回阶段保留的文档数量",
|
||||
},
|
||||
keywordThreshold: {
|
||||
label: "关键词阈值",
|
||||
desc: "关键词检索的最低得分阈值",
|
||||
},
|
||||
vectorThreshold: {
|
||||
label: "向量阈值",
|
||||
desc: "向量召回的最低相似度阈值",
|
||||
},
|
||||
rerankTopK: {
|
||||
label: "ReRank TopK",
|
||||
desc: "重排序后进入答案生成的文档数量",
|
||||
},
|
||||
rerankThreshold: {
|
||||
label: "ReRank 阈值",
|
||||
desc: "重排序阶段的最低得分阈值",
|
||||
},
|
||||
enableRewrite: {
|
||||
label: "开启问题改写",
|
||||
desc: "多轮对话自动改写问题以获得更优召回",
|
||||
},
|
||||
fallbackStrategy: {
|
||||
label: "兜底策略",
|
||||
desc: "检索无结果时采用的处理方式",
|
||||
fixed: "固定回复",
|
||||
model: "交给模型继续生成",
|
||||
},
|
||||
fallbackResponse: {
|
||||
label: "固定兜底回复",
|
||||
desc: "当兜底策略为固定回复时返回的文本",
|
||||
},
|
||||
fallbackPrompt: {
|
||||
label: "兜底 Prompt",
|
||||
desc: "当选择模型兜底时使用的提示模板",
|
||||
},
|
||||
advanced: {
|
||||
description: "配置问题改写、兜底策略等高级设置",
|
||||
},
|
||||
rewritePrompt: {
|
||||
system: "Rewrite System Prompt",
|
||||
user: "Rewrite User Prompt",
|
||||
desc: "控制问题改写的系统提示词",
|
||||
userDesc: "控制问题改写的用户提示词",
|
||||
},
|
||||
chatModel: {
|
||||
label: "LLM 模型",
|
||||
desc: "用于总结和摘要的大语言模型",
|
||||
},
|
||||
rerankModel: {
|
||||
label: "ReRank 模型",
|
||||
desc: "用于搜索结果重排序的模型(可选)",
|
||||
},
|
||||
contextTemplate: {
|
||||
label: "总结Prompt",
|
||||
desc: "用于普通模式下基于检索结果生成回答的Prompt模板",
|
||||
placeholder: "请输入检索结果总结的Prompt模板...",
|
||||
custom: "自定义模板",
|
||||
disabledHint: "当前使用系统默认总结 Prompt,开启自定义后才会应用下方内容。",
|
||||
},
|
||||
systemPrompt: {
|
||||
label: "系统Prompt",
|
||||
desc: "用于普通模式对话的系统级Prompt",
|
||||
placeholder: "请输入系统Prompt...",
|
||||
custom: "自定义 Prompt",
|
||||
disabledHint: "当前使用系统默认 Prompt,开启自定义后才会应用下方内容。",
|
||||
},
|
||||
temperature: {
|
||||
label: "温度参数",
|
||||
@@ -1393,11 +1484,39 @@ export default {
|
||||
label: "最大Token数",
|
||||
desc: "生成回答的最大Token数量",
|
||||
},
|
||||
resetSystemPrompt: {
|
||||
header: "恢复默认系统 Prompt",
|
||||
body: "确定要恢复为系统默认的系统 Prompt 吗?",
|
||||
},
|
||||
resetContextTemplate: {
|
||||
header: "恢复默认总结 Prompt",
|
||||
body: "确定要恢复为系统默认的总结 Prompt 吗?",
|
||||
},
|
||||
toasts: {
|
||||
chatModelSaved: "LLM 模型已保存",
|
||||
rerankModelSaved: "ReRank 模型已保存",
|
||||
contextTemplateSaved: "总结Prompt已保存",
|
||||
systemPromptSaved: "系统Prompt已保存",
|
||||
temperatureSaved: "温度参数已保存",
|
||||
maxTokensSaved: "最大Token数已保存",
|
||||
maxRoundsSaved: "历史轮数已保存",
|
||||
embeddingSaved: "Embedding TopK 已保存",
|
||||
keywordThresholdSaved: "关键词阈值已保存",
|
||||
vectorThresholdSaved: "向量阈值已保存",
|
||||
rerankTopKSaved: "ReRank TopK 已保存",
|
||||
rerankThresholdSaved: "ReRank 阈值已保存",
|
||||
enableRewriteSaved: "问题改写开关已保存",
|
||||
fallbackStrategySaved: "兜底策略已保存",
|
||||
fallbackResponseSaved: "兜底回复已保存",
|
||||
fallbackPromptSaved: "兜底 Prompt 已保存",
|
||||
rewritePromptSystemSaved: "改写 System Prompt 已保存",
|
||||
rewritePromptUserSaved: "改写 User Prompt 已保存",
|
||||
customPromptEnabled: "已启用自定义 Prompt",
|
||||
defaultPromptEnabled: "已使用系统默认 Prompt",
|
||||
customContextTemplateEnabled: "已启用自定义总结 Prompt",
|
||||
defaultContextTemplateEnabled: "已使用系统默认总结 Prompt",
|
||||
resetSystemPromptSuccess: "已恢复为系统默认 Prompt",
|
||||
resetContextTemplateSuccess: "已恢复为系统默认总结 Prompt",
|
||||
},
|
||||
},
|
||||
// 新增:MCP 设置
|
||||
|
||||
@@ -226,7 +226,7 @@ onMounted(async () => {
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
/* 确保换行符正确显示 */
|
||||
white-space: pre-line; /* 保留换行符,但合并多个空格 */
|
||||
// white-space: pre-line; /* 保留换行符,但合并多个空格 */
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -91,7 +91,7 @@
|
||||
|
||||
<!-- Agent 配置 -->
|
||||
<div v-if="currentSection === 'agent'" class="section">
|
||||
<AgentSettings />
|
||||
<AgentSettings :active-sub-section="currentSubSection || 'modes'" />
|
||||
</div>
|
||||
|
||||
<!-- 网络搜索配置 -->
|
||||
@@ -156,7 +156,7 @@ const navItems = computed(() => [
|
||||
{
|
||||
key: 'models',
|
||||
icon: 'control-platform',
|
||||
label: t('settings.modelConfig'),
|
||||
label: t('settings.modelManagement'),
|
||||
children: [
|
||||
{ key: 'chat', label: t('model.llmModel') },
|
||||
{ key: 'embedding', label: t('model.embeddingModel') },
|
||||
@@ -165,9 +165,19 @@ const navItems = computed(() => [
|
||||
]
|
||||
},
|
||||
{ key: 'ollama', icon: 'server', label: 'Ollama' },
|
||||
{ key: 'agent', icon: 'chat', label: t('settings.conversationConfig') },
|
||||
{
|
||||
key: 'agent',
|
||||
icon: 'chat',
|
||||
label: t('settings.conversationStrategy'),
|
||||
children: [
|
||||
{ key: 'modes', label: t('conversationSettings.menus.modes') },
|
||||
{ key: 'models', label: t('conversationSettings.menus.models') },
|
||||
{ key: 'thresholds', label: t('conversationSettings.menus.thresholds') },
|
||||
{ key: 'advanced', label: t('conversationSettings.menus.advanced') },
|
||||
]
|
||||
},
|
||||
{ key: 'websearch', icon: 'search', label: t('settings.webSearchConfig') },
|
||||
{ key: 'mcp', icon: 'tools', label: t('settings.mcpService') },
|
||||
{ key: 'mcp', icon: 'tools', label: t('settings.mcpService') },
|
||||
{ key: 'system', icon: 'info-circle', label: t('settings.systemSettings') },
|
||||
{ key: 'tenant', icon: 'user-circle', label: t('settings.tenantInfo') },
|
||||
{ key: 'api', icon: 'secured', label: t('settings.apiInfo') }
|
||||
@@ -183,11 +193,13 @@ const handleNavClick = (item: any) => {
|
||||
} else {
|
||||
expandedMenus.value.push(item.key)
|
||||
}
|
||||
currentSubSection.value = item.children[0].key
|
||||
} else {
|
||||
currentSubSection.value = ''
|
||||
}
|
||||
|
||||
// 切换到对应页面
|
||||
currentSection.value = item.key
|
||||
currentSubSection.value = ''
|
||||
}
|
||||
|
||||
// 子菜单点击处理
|
||||
@@ -222,19 +234,22 @@ const handleClose = () => {
|
||||
watch(() => uiStore.settingsInitialSection, (section) => {
|
||||
if (section && visible.value) {
|
||||
currentSection.value = section
|
||||
if (uiStore.settingsInitialSubSection) {
|
||||
currentSubSection.value = uiStore.settingsInitialSubSection
|
||||
// 展开对应的父菜单
|
||||
const navItem = (navItems.value as any[]).find((item) => item.key === section)
|
||||
if (navItem && navItem.children && navItem.children.length > 0) {
|
||||
if (!expandedMenus.value.includes(section)) {
|
||||
expandedMenus.value.push(section)
|
||||
}
|
||||
// 滚动到对应区域
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-model-type="${uiStore.settingsInitialSubSection}"]`)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 300)
|
||||
currentSubSection.value = uiStore.settingsInitialSubSection || navItem.children[0].key
|
||||
if (uiStore.settingsInitialSubSection) {
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-model-type="${uiStore.settingsInitialSubSection}"]`)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
currentSubSection.value = ''
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
@@ -252,15 +267,13 @@ const handleSettingsNav = (e: CustomEvent) => {
|
||||
if (section) {
|
||||
currentSection.value = section
|
||||
// 如果有子菜单,自动展开
|
||||
const navItem = (navItems as any).find((item: any) => item.key === section)
|
||||
const navItem = (navItems.value as any[]).find((item: any) => item.key === section)
|
||||
if (navItem && navItem.children && navItem.children.length > 0) {
|
||||
if (!expandedMenus.value.includes(section)) {
|
||||
expandedMenus.value.push(section)
|
||||
}
|
||||
// 如果有 subsection,选中对应的子菜单项
|
||||
if (subsection) {
|
||||
currentSubSection.value = subsection
|
||||
}
|
||||
currentSubSection.value = subsection || navItem.children[0].key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package chatpipline
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
@@ -34,7 +33,12 @@ func (p *PluginChatCompletion) ActivationEvents() []types.EventType {
|
||||
func (p *PluginChatCompletion) OnEvent(
|
||||
ctx context.Context, eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting chat completion")
|
||||
pipelineInfo(ctx, "Completion", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"user_question": chatManage.UserContent,
|
||||
"history_rounds": len(chatManage.History),
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
|
||||
// Prepare chat model and options
|
||||
chatModel, opt, err := prepareChatModel(ctx, p.modelService, chatManage)
|
||||
@@ -43,18 +47,30 @@ func (p *PluginChatCompletion) OnEvent(
|
||||
}
|
||||
|
||||
// Prepare messages including conversation history
|
||||
logger.Info(ctx, "Preparing chat messages with history")
|
||||
pipelineInfo(ctx, "Completion", "messages_ready", map[string]interface{}{
|
||||
"message_count": len(chatManage.History) + 2,
|
||||
})
|
||||
chatMessages := prepareMessagesWithHistory(chatManage)
|
||||
|
||||
// Call the chat model to generate response
|
||||
logger.Info(ctx, "Calling chat model")
|
||||
pipelineInfo(ctx, "Completion", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
chatResponse, err := chatModel.Chat(ctx, chatMessages, opt)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to call chat model: %v", err)
|
||||
pipelineError(ctx, "Completion", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrModelCall.WithError(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Chat completion successful")
|
||||
pipelineInfo(ctx, "Completion", "output", map[string]interface{}{
|
||||
"answer_preview": truncateForLog(chatResponse.Content),
|
||||
"finish_reason": chatResponse.FinishReason,
|
||||
"completion_tokens": chatResponse.Usage.CompletionTokens,
|
||||
"prompt_tokens": chatResponse.Usage.PromptTokens,
|
||||
})
|
||||
chatManage.ChatResponse = chatResponse
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -40,7 +40,12 @@ func (p *PluginChatCompletionStream) ActivationEvents() []types.EventType {
|
||||
func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting chat completion stream")
|
||||
pipelineInfo(ctx, "Stream", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"user_question": chatManage.UserContent,
|
||||
"history_rounds": len(chatManage.History),
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
|
||||
// Prepare chat model and options
|
||||
chatModel, opt, err := prepareChatModel(ctx, p.modelService, chatManage)
|
||||
@@ -49,31 +54,49 @@ func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
|
||||
}
|
||||
|
||||
// Prepare base messages without history
|
||||
logger.Info(ctx, "Preparing chat messages")
|
||||
chatMessages := prepareMessagesWithHistory(chatManage)
|
||||
|
||||
chatMessages := prepareMessagesWithHistory(chatManage)
|
||||
pipelineInfo(ctx, "Stream", "messages_ready", map[string]interface{}{
|
||||
"message_count": len(chatMessages),
|
||||
"system_prompt": chatMessages[0].Content,
|
||||
"user_content": chatMessages[len(chatMessages)-1].Content,
|
||||
})
|
||||
// EventBus is required for event-driven streaming
|
||||
if chatManage.EventBus == nil {
|
||||
logger.Error(ctx, "EventBus is required but not available")
|
||||
pipelineError(ctx, "Stream", "eventbus_missing", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return ErrModelCall.WithError(errors.New("EventBus is required for streaming"))
|
||||
}
|
||||
eventBus := chatManage.EventBus
|
||||
|
||||
logger.Info(ctx, "EventBus detected, enabling event-driven streaming mode")
|
||||
pipelineInfo(ctx, "Stream", "eventbus_ready", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
|
||||
// Initiate streaming chat model call with independent context
|
||||
logger.Info(ctx, "Calling chat stream model")
|
||||
pipelineInfo(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
})
|
||||
responseChan, err := chatModel.ChatStream(ctx, chatMessages, opt)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to call chat stream model: %v", err)
|
||||
pipelineError(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrModelCall.WithError(err)
|
||||
}
|
||||
if responseChan == nil {
|
||||
logger.Error(ctx, "Chat stream returned nil channel")
|
||||
pipelineError(ctx, "Stream", "model_call", map[string]interface{}{
|
||||
"chat_model": chatManage.ChatModelID,
|
||||
"error": "nil_channel",
|
||||
})
|
||||
return ErrModelCall.WithError(errors.New("chat stream returned nil channel"))
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Chat stream initiated successfully")
|
||||
pipelineInfo(ctx, "Stream", "model_started", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
|
||||
// Start goroutine to consume channel and emit events directly
|
||||
go func() {
|
||||
@@ -98,24 +121,9 @@ func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Chat stream completed, emitting completion event")
|
||||
|
||||
// Emit completion event when stream finishes
|
||||
// This allows other components to detect stream completion
|
||||
if err := eventBus.Emit(ctx, types.Event{
|
||||
ID: fmt.Sprintf("%s-complete", uuid.New().String()[:8]),
|
||||
Type: types.EventType(event.EventAgentComplete),
|
||||
SessionID: chatManage.SessionID,
|
||||
Data: event.AgentCompleteData{
|
||||
SessionID: chatManage.SessionID,
|
||||
MessageID: chatManage.MessageID,
|
||||
FinalAnswer: finalContent,
|
||||
},
|
||||
}); err != nil {
|
||||
logger.Errorf(ctx, "Failed to emit completion event: %v", err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Chat stream completed and completion event emitted")
|
||||
pipelineInfo(ctx, "Stream", "channel_close", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
}()
|
||||
|
||||
return next()
|
||||
|
||||
@@ -2,6 +2,10 @@ package chatpipline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
@@ -9,6 +13,78 @@ import (
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
logValueMaxRune = 300
|
||||
defaultStageName = "PIPELINE"
|
||||
defaultActionName = "info"
|
||||
pipelineLogPrefix = "[PIPELINE]"
|
||||
pipelineTruncateEll = "..."
|
||||
)
|
||||
|
||||
func pipelineLog(stage, action string, fields map[string]interface{}) string {
|
||||
if stage == "" {
|
||||
stage = defaultStageName
|
||||
}
|
||||
if action == "" {
|
||||
action = defaultActionName
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
builder.Grow(128)
|
||||
builder.WriteString(pipelineLogPrefix)
|
||||
builder.WriteString(" stage=")
|
||||
builder.WriteString(stage)
|
||||
builder.WriteString(" action=")
|
||||
builder.WriteString(action)
|
||||
|
||||
if len(fields) > 0 {
|
||||
keys := make([]string, 0, len(fields))
|
||||
for k := range fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(key)
|
||||
builder.WriteString("=")
|
||||
builder.WriteString(formatLogValue(fields[key]))
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func pipelineInfo(ctx context.Context, stage, action string, fields map[string]interface{}) {
|
||||
logger.GetLogger(ctx).Info(pipelineLog(stage, action, fields))
|
||||
}
|
||||
|
||||
func pipelineWarn(ctx context.Context, stage, action string, fields map[string]interface{}) {
|
||||
logger.GetLogger(ctx).Warn(pipelineLog(stage, action, fields))
|
||||
}
|
||||
|
||||
func pipelineError(ctx context.Context, stage, action string, fields map[string]interface{}) {
|
||||
logger.GetLogger(ctx).Error(pipelineLog(stage, action, fields))
|
||||
}
|
||||
|
||||
func formatLogValue(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strconv.Quote(truncateForLog(v))
|
||||
case fmt.Stringer:
|
||||
return strconv.Quote(truncateForLog(v.String()))
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func truncateForLog(content string) string {
|
||||
content = strings.ReplaceAll(content, "\n", "\\n")
|
||||
runes := []rune(content)
|
||||
if len(runes) <= logValueMaxRune {
|
||||
return content
|
||||
}
|
||||
return string(runes[:logValueMaxRune]) + pipelineTruncateEll
|
||||
}
|
||||
|
||||
// prepareChatModel shared logic to prepare chat model and options
|
||||
func prepareChatModel(ctx context.Context, modelService interfaces.ModelService,
|
||||
chatManage *types.ChatManage,
|
||||
@@ -35,14 +111,6 @@ func prepareChatModel(ctx context.Context, modelService interfaces.ModelService,
|
||||
return chatModel, opt, nil
|
||||
}
|
||||
|
||||
// prepareBaseMessages prepare basic messages (system prompt and current user content)
|
||||
func prepareBaseMessages(chatManage *types.ChatManage) []chat.Message {
|
||||
var chatMessages []chat.Message
|
||||
chatMessages = append(chatMessages, chat.Message{Role: "system", Content: chatManage.SummaryConfig.Prompt})
|
||||
chatMessages = append(chatMessages, chat.Message{Role: "user", Content: chatManage.UserContent})
|
||||
return chatMessages
|
||||
}
|
||||
|
||||
// prepareMessagesWithHistory prepare complete messages including history
|
||||
func prepareMessagesWithHistory(chatManage *types.ChatManage) []chat.Message {
|
||||
chatMessages := []chat.Message{
|
||||
|
||||
@@ -3,7 +3,6 @@ package chatpipline
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
@@ -27,12 +26,20 @@ func (p *PluginFilterTopK) ActivationEvents() []types.EventType {
|
||||
func (p *PluginFilterTopK) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting filter top-K process")
|
||||
logger.Infof(ctx, "Filter configuration: top-K = %d", chatManage.RerankTopK)
|
||||
pipelineInfo(ctx, "FilterTopK", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"top_k": chatManage.RerankTopK,
|
||||
"merge_cnt": len(chatManage.MergeResult),
|
||||
"rerank_cnt": len(chatManage.RerankResult),
|
||||
"search_cnt": len(chatManage.SearchResult),
|
||||
})
|
||||
|
||||
filterTopK := func(searchResult []*types.SearchResult, topK int) []*types.SearchResult {
|
||||
if topK > 0 && len(searchResult) > topK {
|
||||
logger.Infof(ctx, "Filtering results: before=%d, after=%d", len(searchResult), topK)
|
||||
pipelineInfo(ctx, "FilterTopK", "filter", map[string]interface{}{
|
||||
"before": len(searchResult),
|
||||
"after": topK,
|
||||
})
|
||||
searchResult = searchResult[:topK]
|
||||
}
|
||||
return searchResult
|
||||
@@ -45,9 +52,15 @@ func (p *PluginFilterTopK) OnEvent(ctx context.Context,
|
||||
} else if len(chatManage.SearchResult) > 0 {
|
||||
chatManage.SearchResult = filterTopK(chatManage.SearchResult, chatManage.RerankTopK)
|
||||
} else {
|
||||
logger.Info(ctx, "No results to filter")
|
||||
pipelineWarn(ctx, "FilterTopK", "skip", map[string]interface{}{
|
||||
"reason": "no_results",
|
||||
})
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Filter top-K process completed")
|
||||
pipelineInfo(ctx, "FilterTopK", "output", map[string]interface{}{
|
||||
"merge_cnt": len(chatManage.MergeResult),
|
||||
"rerank_cnt": len(chatManage.RerankResult),
|
||||
"search_cnt": len(chatManage.SearchResult),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
secutils "github.com/Tencent/WeKnora/internal/utils"
|
||||
)
|
||||
@@ -34,6 +33,12 @@ func (p *PluginIntoChatMessage) ActivationEvents() []types.EventType {
|
||||
func (p *PluginIntoChatMessage) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
pipelineInfo(ctx, "IntoChatMessage", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"merge_result_cnt": len(chatManage.MergeResult),
|
||||
"template_len": len(chatManage.SummaryConfig.ContextTemplate),
|
||||
})
|
||||
|
||||
// Extract content from merge results
|
||||
passages := make([]string, len(chatManage.MergeResult))
|
||||
for i, result := range chatManage.MergeResult {
|
||||
@@ -44,6 +49,10 @@ func (p *PluginIntoChatMessage) OnEvent(ctx context.Context,
|
||||
// Parse the context template
|
||||
tmpl, err := template.New("searchContent").Parse(chatManage.SummaryConfig.ContextTemplate)
|
||||
if err != nil {
|
||||
pipelineError(ctx, "IntoChatMessage", "parse_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrTemplateParse.WithError(err)
|
||||
}
|
||||
|
||||
@@ -54,7 +63,9 @@ func (p *PluginIntoChatMessage) OnEvent(ctx context.Context,
|
||||
// 验证用户查询的安全性
|
||||
safeQuery, isValid := secutils.ValidateInput(chatManage.Query)
|
||||
if !isValid {
|
||||
logger.Errorf(ctx, "Invalid user query: %s", chatManage.Query)
|
||||
pipelineWarn(ctx, "IntoChatMessage", "invalid_query", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return ErrTemplateExecute.WithError(fmt.Errorf("用户查询包含非法内容"))
|
||||
}
|
||||
|
||||
@@ -66,11 +77,19 @@ func (p *PluginIntoChatMessage) OnEvent(ctx context.Context,
|
||||
"CurrentWeek": weekdayName[time.Now().Weekday()], // Current weekday in Chinese
|
||||
})
|
||||
if err != nil {
|
||||
pipelineError(ctx, "IntoChatMessage", "render_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrTemplateExecute.WithError(err)
|
||||
}
|
||||
|
||||
// Set formatted content back to chat management
|
||||
chatManage.UserContent = userContent.String()
|
||||
pipelineInfo(ctx, "IntoChatMessage", "output", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"user_content_len": len(chatManage.UserContent),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -99,7 +118,9 @@ func enrichContentWithImageInfo(ctx context.Context, content string, imageInfoJS
|
||||
var imageInfos []types.ImageInfo
|
||||
err := json.Unmarshal([]byte(imageInfoJSON), &imageInfos)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to parse ImageInfo: %v, using content only", err)
|
||||
pipelineWarn(ctx, "IntoChatMessage", "image_parse_error", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -125,7 +146,9 @@ func enrichContentWithImageInfo(ctx context.Context, content string, imageInfoJS
|
||||
// 用于存储已处理的图片URL
|
||||
processedURLs := make(map[string]bool)
|
||||
|
||||
logger.Infof(ctx, "Found %d Markdown image links in content", len(matches))
|
||||
pipelineInfo(ctx, "IntoChatMessage", "image_markdown_links", map[string]interface{}{
|
||||
"match_count": len(matches),
|
||||
})
|
||||
|
||||
// 替换每个图片链接,添加描述和OCR文本
|
||||
for _, match := range matches {
|
||||
@@ -184,8 +207,10 @@ func enrichContentWithImageInfo(ctx context.Context, content string, imageInfoJS
|
||||
content += "附加图片信息:\n" + strings.Join(additionalImageTexts, "\n")
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "Enhanced content with image info: found %d Markdown images, added %d additional images",
|
||||
len(matches), len(additionalImageTexts))
|
||||
pipelineInfo(ctx, "IntoChatMessage", "image_enrich_summary", map[string]interface{}{
|
||||
"markdown_images": len(matches),
|
||||
"additional_imgs": len(additionalImageTexts),
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -5,16 +5,20 @@ import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// PluginMerge handles merging of search result chunks
|
||||
type PluginMerge struct{}
|
||||
type PluginMerge struct {
|
||||
chunkRepo interfaces.ChunkRepository
|
||||
}
|
||||
|
||||
// NewPluginMerge creates and registers a new PluginMerge instance
|
||||
func NewPluginMerge(eventManager *EventManager) *PluginMerge {
|
||||
res := &PluginMerge{}
|
||||
func NewPluginMerge(eventManager *EventManager, chunkRepo interfaces.ChunkRepository) *PluginMerge {
|
||||
res := &PluginMerge{
|
||||
chunkRepo: chunkRepo,
|
||||
}
|
||||
eventManager.Register(res)
|
||||
return res
|
||||
}
|
||||
@@ -28,19 +32,29 @@ func (p *PluginMerge) ActivationEvents() []types.EventType {
|
||||
func (p *PluginMerge) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting chunk merge process")
|
||||
pipelineInfo(ctx, "Merge", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"candidate_cnt": len(chatManage.RerankResult),
|
||||
})
|
||||
|
||||
// Use rerank results if available, fallback to search results
|
||||
searchResult := chatManage.RerankResult
|
||||
if len(searchResult) == 0 {
|
||||
logger.Info(ctx, "No rerank results available, using search results")
|
||||
pipelineWarn(ctx, "Merge", "fallback", map[string]interface{}{
|
||||
"reason": "empty_rerank_result",
|
||||
})
|
||||
searchResult = chatManage.SearchResult
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Processing %d chunks for merging", len(searchResult))
|
||||
pipelineInfo(ctx, "Merge", "candidate_ready", map[string]interface{}{
|
||||
"chunk_cnt": len(searchResult),
|
||||
})
|
||||
|
||||
if len(searchResult) == 0 {
|
||||
logger.Info(ctx, "No chunks available for merging")
|
||||
pipelineWarn(ctx, "Merge", "output", map[string]interface{}{
|
||||
"chunk_cnt": 0,
|
||||
"reason": "no_candidates",
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -50,12 +64,17 @@ func (p *PluginMerge) OnEvent(ctx context.Context,
|
||||
knowledgeGroup[chunk.KnowledgeID] = append(knowledgeGroup[chunk.KnowledgeID], chunk)
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Grouped chunks by knowledge ID, %d knowledge sources", len(knowledgeGroup))
|
||||
pipelineInfo(ctx, "Merge", "group_summary", map[string]interface{}{
|
||||
"knowledge_cnt": len(knowledgeGroup),
|
||||
})
|
||||
|
||||
mergedChunks := []*types.SearchResult{}
|
||||
// Process each knowledge source separately
|
||||
for knowledgeID, chunks := range knowledgeGroup {
|
||||
logger.Infof(ctx, "Processing knowledge ID: %s with %d chunks", knowledgeID, len(chunks))
|
||||
pipelineInfo(ctx, "Merge", "group_process", map[string]interface{}{
|
||||
"knowledge_id": knowledgeID,
|
||||
"chunk_cnt": len(chunks),
|
||||
})
|
||||
|
||||
// Sort chunks by their start position in the original document
|
||||
sort.Slice(chunks, func(i, j int) bool {
|
||||
@@ -92,7 +111,10 @@ func (p *PluginMerge) OnEvent(ctx context.Context,
|
||||
|
||||
// 合并 ImageInfo
|
||||
if err := mergeImageInfo(ctx, lastChunk, chunks[i]); err != nil {
|
||||
logger.Warnf(ctx, "Failed to merge ImageInfo: %v", err)
|
||||
pipelineWarn(ctx, "Merge", "image_merge", map[string]interface{}{
|
||||
"knowledge_id": knowledgeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if chunks[i].Score > lastChunk.Score {
|
||||
@@ -100,8 +122,10 @@ func (p *PluginMerge) OnEvent(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Merged %d chunks into %d chunks for knowledge ID: %s",
|
||||
len(chunks), len(knowledgeMergedChunks), knowledgeID)
|
||||
pipelineInfo(ctx, "Merge", "group_output", map[string]interface{}{
|
||||
"knowledge_id": knowledgeID,
|
||||
"merged_chunks": len(knowledgeMergedChunks),
|
||||
})
|
||||
|
||||
mergedChunks = append(mergedChunks, knowledgeMergedChunks...)
|
||||
}
|
||||
@@ -111,7 +135,11 @@ func (p *PluginMerge) OnEvent(ctx context.Context,
|
||||
return mergedChunks[i].Score > mergedChunks[j].Score
|
||||
})
|
||||
|
||||
logger.Infof(ctx, "Final merged result: %d chunks, sorted by score", len(mergedChunks))
|
||||
pipelineInfo(ctx, "Merge", "output", map[string]interface{}{
|
||||
"merged_total": len(mergedChunks),
|
||||
})
|
||||
|
||||
mergedChunks = p.expandShortContextWithNeighbors(ctx, chatManage, mergedChunks)
|
||||
|
||||
chatManage.MergeResult = mergedChunks
|
||||
return next()
|
||||
@@ -126,7 +154,9 @@ func mergeImageInfo(ctx context.Context, target *types.SearchResult, source *typ
|
||||
|
||||
var sourceImageInfos []types.ImageInfo
|
||||
if err := json.Unmarshal([]byte(source.ImageInfo), &sourceImageInfos); err != nil {
|
||||
logger.Warnf(ctx, "Failed to unmarshal source ImageInfo: %v", err)
|
||||
pipelineWarn(ctx, "Merge", "image_unmarshal_source", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -139,7 +169,9 @@ func mergeImageInfo(ctx context.Context, target *types.SearchResult, source *typ
|
||||
var targetImageInfos []types.ImageInfo
|
||||
if target.ImageInfo != "" {
|
||||
if err := json.Unmarshal([]byte(target.ImageInfo), &targetImageInfos); err != nil {
|
||||
logger.Warnf(ctx, "Failed to unmarshal target ImageInfo: %v", err)
|
||||
pipelineWarn(ctx, "Merge", "image_unmarshal_target", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// 如果目标解析失败,直接使用源数据
|
||||
target.ImageInfo = source.ImageInfo
|
||||
return nil
|
||||
@@ -164,12 +196,345 @@ func mergeImageInfo(ctx context.Context, target *types.SearchResult, source *typ
|
||||
// 序列化合并后的ImageInfo
|
||||
mergedImageInfoJSON, err := json.Marshal(uniqueImageInfos)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to marshal merged ImageInfo: %v", err)
|
||||
pipelineWarn(ctx, "Merge", "image_marshal", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新目标chunk的ImageInfo
|
||||
target.ImageInfo = string(mergedImageInfoJSON)
|
||||
logger.Infof(ctx, "Successfully merged ImageInfo, total count: %d", len(uniqueImageInfos))
|
||||
pipelineInfo(ctx, "Merge", "image_merged", map[string]interface{}{
|
||||
"image_refs": len(uniqueImageInfos),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PluginMerge) expandShortContextWithNeighbors(ctx context.Context, chatManage *types.ChatManage, results []*types.SearchResult) []*types.SearchResult {
|
||||
const (
|
||||
minLen = 350
|
||||
maxLen = 850
|
||||
)
|
||||
|
||||
if len(results) == 0 || p.chunkRepo == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
tenantID, _ := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
if tenantID == 0 && chatManage != nil {
|
||||
tenantID = chatManage.TenantID
|
||||
}
|
||||
if tenantID == 0 {
|
||||
pipelineWarn(ctx, "Merge", "expand_skip", map[string]interface{}{
|
||||
"reason": "missing_tenant",
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
type targetInfo struct {
|
||||
result *types.SearchResult
|
||||
}
|
||||
|
||||
targets := make([]targetInfo, 0)
|
||||
baseIDsSet := make(map[string]struct{})
|
||||
|
||||
for _, r := range results {
|
||||
if r == nil || r.ID == "" || r.Content == "" {
|
||||
continue
|
||||
}
|
||||
if r.ChunkType != string(types.ChunkTypeText) {
|
||||
continue
|
||||
}
|
||||
if runeLen(r.Content) >= minLen {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, targetInfo{result: r})
|
||||
baseIDsSet[r.ID] = struct{}{}
|
||||
pipelineInfo(ctx, "Merge", "need_expand", map[string]interface{}{
|
||||
"chunk_id": r.ID,
|
||||
"content": r.Content,
|
||||
"chunk_type": r.ChunkType,
|
||||
"len": runeLen(r.Content),
|
||||
})
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
return results
|
||||
}
|
||||
|
||||
baseIDs := make([]string, 0, len(baseIDsSet))
|
||||
for id := range baseIDsSet {
|
||||
baseIDs = append(baseIDs, id)
|
||||
}
|
||||
|
||||
chunkMap := make(map[string]*types.Chunk, len(baseIDs))
|
||||
chunks, err := p.chunkRepo.ListChunksByID(ctx, tenantID, baseIDs)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "Merge", "expand_list_base_failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return results
|
||||
}
|
||||
for _, chunk := range chunks {
|
||||
chunkMap[chunk.ID] = chunk
|
||||
}
|
||||
|
||||
neighborIDsSet := make(map[string]struct{})
|
||||
for _, chunk := range chunkMap {
|
||||
if chunk == nil {
|
||||
continue
|
||||
}
|
||||
if chunk.PreChunkID != "" {
|
||||
if _, exists := chunkMap[chunk.PreChunkID]; !exists {
|
||||
neighborIDsSet[chunk.PreChunkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if chunk.NextChunkID != "" {
|
||||
if _, exists := chunkMap[chunk.NextChunkID]; !exists {
|
||||
neighborIDsSet[chunk.NextChunkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(neighborIDsSet) > 0 {
|
||||
neighborIDs := make([]string, 0, len(neighborIDsSet))
|
||||
for id := range neighborIDsSet {
|
||||
neighborIDs = append(neighborIDs, id)
|
||||
}
|
||||
neighbors, err := p.chunkRepo.ListChunksByID(ctx, tenantID, neighborIDs)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "Merge", "expand_list_neighbor_failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
for _, chunk := range neighbors {
|
||||
chunkMap[chunk.ID] = chunk
|
||||
pipelineInfo(ctx, "Merge", "expand_list_neighbor_success", map[string]interface{}{
|
||||
"neighbor_chunk_id": chunk.ID,
|
||||
"neighbor_content": chunk.Content,
|
||||
"neighbor_chunk_type": chunk.ChunkType,
|
||||
"neighbor_len": runeLen(chunk.Content),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
res := target.result
|
||||
p.fetchChunksIfMissing(ctx, tenantID, chunkMap, res.ID)
|
||||
baseChunk := chunkMap[res.ID]
|
||||
if baseChunk == nil || baseChunk.Content == "" || baseChunk.ChunkType != types.ChunkTypeText {
|
||||
continue
|
||||
}
|
||||
|
||||
prevContent := ""
|
||||
nextContent := ""
|
||||
prevIDs := []string{}
|
||||
nextIDs := []string{}
|
||||
|
||||
prevCursor := baseChunk.PreChunkID
|
||||
nextCursor := baseChunk.NextChunkID
|
||||
|
||||
p.fetchChunksIfMissing(ctx, tenantID, chunkMap, prevCursor, nextCursor)
|
||||
|
||||
if prevCursor != "" {
|
||||
if prevChunk := chunkMap[prevCursor]; prevChunk != nil && prevChunk.KnowledgeID == baseChunk.KnowledgeID {
|
||||
prevContent = prevChunk.Content
|
||||
prevIDs = append(prevIDs, prevChunk.ID)
|
||||
prevCursor = prevChunk.PreChunkID
|
||||
} else {
|
||||
prevCursor = ""
|
||||
}
|
||||
}
|
||||
|
||||
if nextCursor != "" {
|
||||
if nextChunk := chunkMap[nextCursor]; nextChunk != nil && nextChunk.KnowledgeID == baseChunk.KnowledgeID {
|
||||
nextContent = nextChunk.Content
|
||||
nextIDs = append(nextIDs, nextChunk.ID)
|
||||
nextCursor = nextChunk.NextChunkID
|
||||
} else {
|
||||
nextCursor = ""
|
||||
}
|
||||
}
|
||||
|
||||
var merged string
|
||||
for {
|
||||
merged = mergeOrderedContent(prevContent, baseChunk.Content, nextContent, maxLen)
|
||||
if merged == "" {
|
||||
break
|
||||
}
|
||||
if runeLen(merged) >= minLen {
|
||||
break
|
||||
}
|
||||
if prevCursor == "" && nextCursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
expanded := false
|
||||
if prevCursor != "" {
|
||||
p.fetchChunksIfMissing(ctx, tenantID, chunkMap, prevCursor)
|
||||
if prevChunk := chunkMap[prevCursor]; prevChunk != nil && prevChunk.KnowledgeID == baseChunk.KnowledgeID {
|
||||
prevContent = concatNoOverlap(prevChunk.Content, prevContent)
|
||||
prevIDs = append([]string{prevChunk.ID}, prevIDs...)
|
||||
prevCursor = prevChunk.PreChunkID
|
||||
expanded = true
|
||||
} else {
|
||||
prevCursor = ""
|
||||
}
|
||||
}
|
||||
|
||||
merged = mergeOrderedContent(prevContent, baseChunk.Content, nextContent, maxLen)
|
||||
if runeLen(merged) >= minLen {
|
||||
break
|
||||
}
|
||||
|
||||
if nextCursor != "" {
|
||||
p.fetchChunksIfMissing(ctx, tenantID, chunkMap, nextCursor)
|
||||
if nextChunk := chunkMap[nextCursor]; nextChunk != nil && nextChunk.KnowledgeID == baseChunk.KnowledgeID {
|
||||
nextContent = concatNoOverlap(nextContent, nextChunk.Content)
|
||||
nextIDs = append(nextIDs, nextChunk.ID)
|
||||
nextCursor = nextChunk.NextChunkID
|
||||
expanded = true
|
||||
} else {
|
||||
nextCursor = ""
|
||||
}
|
||||
}
|
||||
|
||||
if !expanded {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if merged == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
beforeLen := runeLen(res.Content)
|
||||
res.Content = merged
|
||||
|
||||
for _, id := range prevIDs {
|
||||
if id != "" && !containsID(res.SubChunkID, id) {
|
||||
res.SubChunkID = append(res.SubChunkID, id)
|
||||
}
|
||||
}
|
||||
for _, id := range nextIDs {
|
||||
if id != "" && !containsID(res.SubChunkID, id) {
|
||||
res.SubChunkID = append(res.SubChunkID, id)
|
||||
}
|
||||
}
|
||||
|
||||
if prevContent != "" {
|
||||
res.StartAt = baseChunk.StartAt - runeLen(prevContent)
|
||||
if res.StartAt < 0 {
|
||||
res.StartAt = 0
|
||||
}
|
||||
}
|
||||
res.EndAt = res.StartAt + runeLen(res.Content)
|
||||
|
||||
pipelineInfo(ctx, "Merge", "expand_short_chunk", map[string]interface{}{
|
||||
"chunk_id": res.ID,
|
||||
"prev_ids": prevIDs,
|
||||
"next_ids": nextIDs,
|
||||
"before_len": beforeLen,
|
||||
"after_len": runeLen(res.Content),
|
||||
"base_content": baseChunk.Content,
|
||||
"after_content": res.Content,
|
||||
"chunk_type": res.ChunkType,
|
||||
"remaining_prev": prevCursor,
|
||||
"remaining_next": nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func runeLen(s string) int {
|
||||
return len([]rune(s))
|
||||
}
|
||||
|
||||
func mergeOrderedContent(prev, base, next string, maxLen int) string {
|
||||
content := base
|
||||
if prev != "" {
|
||||
content = concatNoOverlap(prev, content)
|
||||
}
|
||||
if next != "" {
|
||||
content = concatNoOverlap(content, next)
|
||||
}
|
||||
runes := []rune(content)
|
||||
if len(runes) > maxLen {
|
||||
return string(runes[:maxLen])
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// concatNoOverlap concatenates a and b, removing potential overlapping prefix/suffix
|
||||
func concatNoOverlap(a, b string) string {
|
||||
if a == "" {
|
||||
return b
|
||||
}
|
||||
if b == "" {
|
||||
return a
|
||||
}
|
||||
|
||||
ar := []rune(a)
|
||||
br := []rune(b)
|
||||
maxOverlap := minInt(len(ar), len(br))
|
||||
for k := maxOverlap; k > 0; k-- {
|
||||
if string(ar[len(ar)-k:]) == string(br[:k]) {
|
||||
return string(ar) + string(br[k:])
|
||||
}
|
||||
}
|
||||
return string(ar) + string(br)
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func containsID(ids []string, target string) bool {
|
||||
for _, id := range ids {
|
||||
if id == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PluginMerge) fetchChunksIfMissing(ctx context.Context, tenantID uint, chunkMap map[string]*types.Chunk, chunkIDs ...string) {
|
||||
missing := make([]string, 0, len(chunkIDs))
|
||||
for _, id := range chunkIDs {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := chunkMap[id]; !exists {
|
||||
missing = append(missing, id)
|
||||
}
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
chunks, err := p.chunkRepo.ListChunksByID(ctx, tenantID, missing)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "Merge", "expand_fetch_missing_failed", map[string]interface{}{
|
||||
"missing_cnt": len(missing),
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
found := make(map[string]struct{}, len(chunks))
|
||||
for _, chunk := range chunks {
|
||||
chunkMap[chunk.ID] = chunk
|
||||
found[chunk.ID] = struct{}{}
|
||||
}
|
||||
|
||||
for _, id := range missing {
|
||||
if _, ok := found[id]; !ok {
|
||||
chunkMap[id] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package chatpipline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/yanyiwu/gojieba"
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/yanyiwu/gojieba"
|
||||
)
|
||||
|
||||
// PluginPreprocess Query preprocessing plugin
|
||||
type PluginPreprocess struct {
|
||||
config *config.Config
|
||||
jieba *gojieba.Jieba
|
||||
stopwords map[string]struct{}
|
||||
config *config.Config
|
||||
jieba *gojieba.Jieba
|
||||
stopwords map[string]struct{}
|
||||
modelService interfaces.ModelService
|
||||
}
|
||||
|
||||
// Regular expressions for text cleaning
|
||||
@@ -28,11 +31,14 @@ var (
|
||||
punctRegex = regexp.MustCompile(`[^\p{L}\p{N}\s]`) // Punctuation marks
|
||||
)
|
||||
|
||||
const maxProcessedTokens = 12
|
||||
|
||||
// NewPluginPreprocess Creates a new query preprocessing plugin
|
||||
func NewPluginPreprocess(
|
||||
eventManager *EventManager,
|
||||
config *config.Config,
|
||||
cleaner interfaces.ResourceCleaner,
|
||||
eventManager *EventManager,
|
||||
config *config.Config,
|
||||
cleaner interfaces.ResourceCleaner,
|
||||
modelService interfaces.ModelService,
|
||||
) *PluginPreprocess {
|
||||
// Use default dictionary for Jieba tokenizer
|
||||
jieba := gojieba.NewJieba()
|
||||
@@ -40,11 +46,12 @@ func NewPluginPreprocess(
|
||||
// Load stopwords from built-in stopword library
|
||||
stopwords := loadStopwords()
|
||||
|
||||
res := &PluginPreprocess{
|
||||
config: config,
|
||||
jieba: jieba,
|
||||
stopwords: stopwords,
|
||||
}
|
||||
res := &PluginPreprocess{
|
||||
config: config,
|
||||
jieba: jieba,
|
||||
stopwords: stopwords,
|
||||
modelService: modelService,
|
||||
}
|
||||
|
||||
// Register resource cleanup function
|
||||
if cleaner != nil {
|
||||
@@ -85,25 +92,66 @@ func (p *PluginPreprocess) ActivationEvents() []types.EventType {
|
||||
|
||||
// OnEvent Process events
|
||||
func (p *PluginPreprocess) OnEvent(ctx context.Context, eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError) *PluginError {
|
||||
if chatManage.RewriteQuery == "" {
|
||||
return next()
|
||||
rawQuery := strings.TrimSpace(chatManage.RewriteQuery)
|
||||
if rawQuery == "" {
|
||||
return next()
|
||||
}
|
||||
|
||||
pipelineInfo(ctx, "Preprocess", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"rewrite_query": rawQuery,
|
||||
})
|
||||
|
||||
normalized := normalizeWhitespace(rawQuery)
|
||||
sanitized := strings.TrimSpace(p.cleanText(normalized))
|
||||
if sanitized == "" {
|
||||
sanitized = normalized
|
||||
}
|
||||
|
||||
logger.GetLogger(ctx).Infof("Starting query preprocessing, original query: %s", chatManage.RewriteQuery)
|
||||
var (
|
||||
processed = sanitized
|
||||
strategy = "original"
|
||||
tokenPreview string
|
||||
tokenCount int
|
||||
)
|
||||
|
||||
// 1. Basic text cleaning
|
||||
processed := p.cleanText(chatManage.RewriteQuery)
|
||||
switch {
|
||||
case containsChineseCharacters(sanitized):
|
||||
segments := p.segmentText(sanitized)
|
||||
tokens := p.selectMeaningfulTokens(segments)
|
||||
tokenCount = len(tokens)
|
||||
if len(tokens) >= 2 {
|
||||
processed = strings.Join(tokens, " ")
|
||||
strategy = "zh_tokens"
|
||||
tokenPreview = strings.Join(tokens, ",")
|
||||
} else {
|
||||
strategy = "fallback_original"
|
||||
}
|
||||
case containsLatinLetters(sanitized):
|
||||
processed = normalizeLatinQuery(sanitized)
|
||||
if processed != sanitized {
|
||||
strategy = "latin_normalize"
|
||||
}
|
||||
default:
|
||||
strategy = "original"
|
||||
}
|
||||
|
||||
// 2. Tokenization
|
||||
segments := p.segmentText(processed)
|
||||
if strings.TrimSpace(processed) == "" {
|
||||
processed = rawQuery
|
||||
strategy = "fallback_original"
|
||||
}
|
||||
|
||||
// 3. Stopword filtering and reconstruction
|
||||
filteredSegments := p.filterStopwords(segments)
|
||||
chatManage.ProcessedQuery = processed
|
||||
chatManage.QueryIntent = p.detectIntentLLM(ctx, chatManage, sanitized)
|
||||
|
||||
// Update preprocessed query
|
||||
chatManage.ProcessedQuery = strings.Join(filteredSegments, " ")
|
||||
|
||||
logger.GetLogger(ctx).Infof("Query preprocessing complete, processed query: %s", chatManage.ProcessedQuery)
|
||||
pipelineInfo(ctx, "Preprocess", "output", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"processed_query": processed,
|
||||
"strategy": strategy,
|
||||
"token_count": tokenCount,
|
||||
"token_preview": truncateForLog(tokenPreview),
|
||||
"query_intent": chatManage.QueryIntent,
|
||||
})
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -136,32 +184,137 @@ func (p *PluginPreprocess) segmentText(text string) []string {
|
||||
}
|
||||
|
||||
// filterStopwords Filter stopwords
|
||||
func (p *PluginPreprocess) filterStopwords(segments []string) []string {
|
||||
var filtered []string
|
||||
func (p *PluginPreprocess) selectMeaningfulTokens(segments []string) []string {
|
||||
var tokens []string
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, word := range segments {
|
||||
// If not a stopword and not blank, keep it
|
||||
if _, isStopword := p.stopwords[word]; !isStopword && !isBlank(word) {
|
||||
filtered = append(filtered, word)
|
||||
word = strings.TrimSpace(word)
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
if _, stop := p.stopwords[word]; stop {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[word]; exists {
|
||||
continue
|
||||
}
|
||||
if !isInformativeToken(word) {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[word] = struct{}{}
|
||||
tokens = append(tokens, word)
|
||||
if len(tokens) >= maxProcessedTokens {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If filtering results in empty list, return original tokenization results
|
||||
if len(filtered) == 0 {
|
||||
return segments
|
||||
}
|
||||
|
||||
return filtered
|
||||
return tokens
|
||||
}
|
||||
|
||||
// isBlank Check if a string is blank
|
||||
func isBlank(str string) bool {
|
||||
for _, r := range str {
|
||||
if !unicode.IsSpace(r) {
|
||||
return false
|
||||
func isInformativeToken(token string) bool {
|
||||
if token == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
runeCount := utf8.RuneCountInString(token)
|
||||
if runeCount == 1 {
|
||||
r, _ := utf8.DecodeRuneInString(token)
|
||||
if unicode.IsDigit(r) {
|
||||
return true
|
||||
}
|
||||
if r <= unicode.MaxASCII && unicode.IsLetter(r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func containsChineseCharacters(text string) bool {
|
||||
for _, r := range text {
|
||||
if unicode.Is(unicode.Han, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
func containsLatinLetters(text string) bool {
|
||||
for _, r := range text {
|
||||
if r <= unicode.MaxASCII && unicode.IsLetter(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeWhitespace(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
return multiSpaceRegex.ReplaceAllString(text, " ")
|
||||
}
|
||||
|
||||
func normalizeLatinQuery(text string) string {
|
||||
text = strings.ToLower(text)
|
||||
text = multiSpaceRegex.ReplaceAllString(text, " ")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
type intentResp struct {
|
||||
Intent string `json:"intent"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
func (p *PluginPreprocess) detectIntentLLM(ctx context.Context, chatManage *types.ChatManage, text string) string {
|
||||
if p.modelService == nil || chatManage.ChatModelID == "" {
|
||||
pipelineWarn(ctx, "IntentDetect", "skip", map[string]interface{}{ "reason": "no_model", "session_id": chatManage.SessionID })
|
||||
return "general"
|
||||
}
|
||||
chatModel, err := p.modelService.GetChatModel(ctx, chatManage.ChatModelID)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "IntentDetect", "get_model_failed", map[string]interface{}{ "error": err.Error(), "model_id": chatManage.ChatModelID })
|
||||
return "general"
|
||||
}
|
||||
pipelineInfo(ctx, "IntentDetect", "start", map[string]interface{}{ "session_id": chatManage.SessionID, "model_id": chatManage.ChatModelID })
|
||||
sys := "You are a query intent classifier. Classify the user's query into one of: definition, howto, compare, qa, general. Respond ONLY with a JSON object {\"intent\": \"...\", \"confidence\": 0.0 } inside a markdown fenced block."
|
||||
usr := text
|
||||
think := false
|
||||
resp, err := chatModel.Chat(ctx, []chat.Message{
|
||||
{Role: "system", Content: sys},
|
||||
{Role: "user", Content: usr},
|
||||
}, &chat.ChatOptions{Temperature: 0.0, MaxCompletionTokens: 64, Thinking: &think})
|
||||
if err != nil || resp.Content == "" {
|
||||
pipelineWarn(ctx, "IntentDetect", "model_call_failed", map[string]interface{}{ "error": err })
|
||||
return "general"
|
||||
}
|
||||
body := extractJSONBody(resp.Content)
|
||||
var ir intentResp
|
||||
if err := json.Unmarshal([]byte(body), &ir); err != nil {
|
||||
pipelineWarn(ctx, "IntentDetect", "parse_failed", map[string]interface{}{ "body": truncateForLog(body), "error": err.Error() })
|
||||
return "general"
|
||||
}
|
||||
pipelineInfo(ctx, "IntentDetect", "result", map[string]interface{}{ "intent": ir.Intent, "confidence": ir.Confidence })
|
||||
switch strings.ToLower(strings.TrimSpace(ir.Intent)) {
|
||||
case "definition", "howto", "compare", "qa", "general":
|
||||
return strings.ToLower(ir.Intent)
|
||||
default:
|
||||
return "general"
|
||||
}
|
||||
}
|
||||
|
||||
func extractJSONBody(text string) string {
|
||||
t := strings.TrimSpace(text)
|
||||
// Try fenced block first
|
||||
if i := strings.Index(t, "{"); i >= 0 {
|
||||
j := strings.LastIndex(t, "}")
|
||||
if j > i {
|
||||
return t[i : j+1]
|
||||
}
|
||||
}
|
||||
return "{}"
|
||||
}
|
||||
|
||||
// Ensure resources are properly released
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package chatpipline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/models/rerank"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/Tencent/WeKnora/internal/models/rerank"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// PluginRerank implements reranking functionality for chat pipeline
|
||||
@@ -34,28 +33,43 @@ func (p *PluginRerank) ActivationEvents() []types.EventType {
|
||||
|
||||
// OnEvent handles reranking events in the chat pipeline
|
||||
func (p *PluginRerank) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting reranking process")
|
||||
logger.Infof(ctx, "Getting rerank model, model ID: %s", chatManage.RerankModelID)
|
||||
pipelineInfo(ctx, "Rerank", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"candidate_cnt": len(chatManage.SearchResult),
|
||||
"rerank_model": chatManage.RerankModelID,
|
||||
"rerank_thresh": chatManage.RerankThreshold,
|
||||
"rewrite_query": chatManage.RewriteQuery,
|
||||
"processed_query": chatManage.ProcessedQuery,
|
||||
})
|
||||
if len(chatManage.SearchResult) == 0 {
|
||||
logger.Infof(ctx, "No search result, skip reranking")
|
||||
pipelineInfo(ctx, "Rerank", "skip", map[string]interface{}{
|
||||
"reason": "empty_search_result",
|
||||
})
|
||||
return next()
|
||||
}
|
||||
if chatManage.RerankModelID == "" {
|
||||
logger.Warn(ctx, "Rerank model ID is empty, skipping reranking")
|
||||
pipelineWarn(ctx, "Rerank", "skip", map[string]interface{}{
|
||||
"reason": "empty_model_id",
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
// Get rerank model from service
|
||||
rerankModel, err := p.modelService.GetRerankModel(ctx, chatManage.RerankModelID)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to get rerank model: %v, rerank model ID: %s", err, chatManage.RerankModelID)
|
||||
pipelineError(ctx, "Rerank", "get_model", map[string]interface{}{
|
||||
"model_id": chatManage.RerankModelID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return ErrGetRerankModel.WithError(err)
|
||||
}
|
||||
|
||||
// Prepare passages for reranking
|
||||
logger.Infof(ctx, "Preparing passages for reranking, search result count: %d", len(chatManage.SearchResult))
|
||||
pipelineInfo(ctx, "Rerank", "build_passages", map[string]interface{}{
|
||||
"candidate_cnt": len(chatManage.SearchResult),
|
||||
})
|
||||
var passages []string
|
||||
for _, result := range chatManage.SearchResult {
|
||||
// 合并Content和ImageInfo的文本内容
|
||||
@@ -72,41 +86,79 @@ func (p *PluginRerank) OnEvent(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// Update search results with reranked scores
|
||||
logger.Infof(ctx, "Filtered rerank results, original: %d, filtered: %d", len(rerankResp), len(rerankResp))
|
||||
result := []*types.SearchResult{}
|
||||
for _, rr := range rerankResp {
|
||||
chatManage.SearchResult[rr.Index].Score = rr.RelevanceScore
|
||||
result = append(result, chatManage.SearchResult[rr.Index])
|
||||
}
|
||||
chatManage.RerankResult = result
|
||||
pipelineInfo(ctx, "Rerank", "model_response", map[string]interface{}{
|
||||
"result_cnt": len(rerankResp),
|
||||
})
|
||||
for i := range chatManage.SearchResult {
|
||||
chatManage.SearchResult[i].Metadata = ensureMetadata(chatManage.SearchResult[i].Metadata)
|
||||
}
|
||||
reranked := make([]*types.SearchResult, 0, len(rerankResp))
|
||||
for _, rr := range rerankResp {
|
||||
sr := chatManage.SearchResult[rr.Index]
|
||||
base := sr.Score
|
||||
sr.Metadata["base_score"] = fmt.Sprintf("%.4f", base)
|
||||
sr.Score = rr.RelevanceScore
|
||||
sr.Score = compositeScore(sr, rr.RelevanceScore, base, chatManage)
|
||||
reranked = append(reranked, sr)
|
||||
}
|
||||
final := applyMMR(ctx, reranked, chatManage, min(len(reranked), max(1, chatManage.RerankTopK)), 0.7)
|
||||
chatManage.RerankResult = final
|
||||
|
||||
if len(chatManage.RerankResult) == 0 {
|
||||
logger.Warn(ctx, "Reranking produced no results above threshold")
|
||||
return ErrSearchNothing
|
||||
}
|
||||
// Log composite top scores and MMR selection summary
|
||||
topN := min(3, len(reranked))
|
||||
for i := 0; i < topN; i++ {
|
||||
pipelineInfo(ctx, "Rerank", "composite_top", map[string]interface{}{
|
||||
"rank": i + 1,
|
||||
"chunk_id": reranked[i].ID,
|
||||
"base_score": reranked[i].Metadata["base_score"],
|
||||
"final_score": fmt.Sprintf("%.4f", reranked[i].Score),
|
||||
"intent": chatManage.QueryIntent,
|
||||
})
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Reranking process completed successfully, result count: %d", len(chatManage.RerankResult))
|
||||
return next()
|
||||
if len(chatManage.RerankResult) == 0 {
|
||||
pipelineWarn(ctx, "Rerank", "output", map[string]interface{}{
|
||||
"filtered_cnt": 0,
|
||||
})
|
||||
return ErrSearchNothing
|
||||
}
|
||||
|
||||
pipelineInfo(ctx, "Rerank", "output", map[string]interface{}{
|
||||
"filtered_cnt": len(chatManage.RerankResult),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
// rerank performs the actual reranking operation with given query and passages
|
||||
func (p *PluginRerank) rerank(ctx context.Context,
|
||||
chatManage *types.ChatManage, rerankModel rerank.Reranker, query string, passages []string,
|
||||
chatManage *types.ChatManage, rerankModel rerank.Reranker, query string, passages []string,
|
||||
) []rerank.RankResult {
|
||||
logger.Infof(ctx, "Executing reranking with query: %s, passage count: %d", query, len(passages))
|
||||
pipelineInfo(ctx, "Rerank", "model_call", map[string]interface{}{
|
||||
"query_variant": query,
|
||||
"passages": len(passages),
|
||||
})
|
||||
rerankResp, err := rerankModel.Rerank(ctx, query, passages)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Reranking failed: %v", err)
|
||||
pipelineError(ctx, "Rerank", "model_call", map[string]interface{}{
|
||||
"query_variant": query,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log top scores for debugging
|
||||
logger.Infof(ctx, "Reranking completed, filtering results with threshold: %f", chatManage.RerankThreshold)
|
||||
for i := range min(3, len(rerankResp)) {
|
||||
logger.Infof(ctx, "Top %d score of rerankResp: %f, passages: %s, index: %d",
|
||||
i+1, rerankResp[i].RelevanceScore, rerankResp[i].Document.Text, rerankResp[i].Index,
|
||||
)
|
||||
pipelineInfo(ctx, "Rerank", "threshold", map[string]interface{}{
|
||||
"threshold": chatManage.RerankThreshold,
|
||||
})
|
||||
for i := range min(5, len(rerankResp)) {
|
||||
pipelineInfo(ctx, "Rerank", "top_score", map[string]interface{}{
|
||||
"rank": i + 1,
|
||||
"score": rerankResp[i].RelevanceScore,
|
||||
"chunk_id": chatManage.SearchResult[rerankResp[i].Index].ID,
|
||||
"match_type": chatManage.SearchResult[rerankResp[i].Index].MatchType,
|
||||
"chunk_type": chatManage.SearchResult[rerankResp[i].Index].ChunkType,
|
||||
"content": chatManage.SearchResult[rerankResp[i].Index].Content,
|
||||
})
|
||||
}
|
||||
|
||||
// Filter results based on threshold with special handling for history matches
|
||||
@@ -121,7 +173,149 @@ func (p *PluginRerank) rerank(ctx context.Context,
|
||||
rankFilter = append(rankFilter, result)
|
||||
}
|
||||
}
|
||||
return rankFilter
|
||||
return rankFilter
|
||||
}
|
||||
|
||||
func ensureMetadata(m map[string]string) map[string]string {
|
||||
if m == nil {
|
||||
return make(map[string]string)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func compositeScore(sr *types.SearchResult, modelScore, baseScore float64, chatManage *types.ChatManage) float64 {
|
||||
sourceWeight := 1.0
|
||||
switch strings.ToLower(sr.KnowledgeSource) {
|
||||
case "web_search":
|
||||
sourceWeight = 0.95
|
||||
default:
|
||||
sourceWeight = 1.0
|
||||
}
|
||||
intentBoost := 1.0
|
||||
if chatManage.QueryIntent != "" {
|
||||
switch chatManage.QueryIntent {
|
||||
case "definition":
|
||||
if sr.ChunkType == string(types.ChunkTypeSummary) {
|
||||
intentBoost = 1.05
|
||||
}
|
||||
case "howto":
|
||||
if sr.EndAt-sr.StartAt > 300 {
|
||||
intentBoost = 1.03
|
||||
}
|
||||
case "compare":
|
||||
intentBoost = 1.0
|
||||
}
|
||||
}
|
||||
positionPrior := 1.0
|
||||
if sr.StartAt >= 0 {
|
||||
positionPrior += clampFloat(1.0-float64(sr.StartAt)/float64(sr.EndAt+1), -0.05, 0.05)
|
||||
}
|
||||
composite := 0.6*modelScore + 0.3*baseScore + 0.1*sourceWeight
|
||||
composite *= intentBoost
|
||||
composite *= positionPrior
|
||||
if composite < 0 {
|
||||
composite = 0
|
||||
}
|
||||
if composite > 1 {
|
||||
composite = 1
|
||||
}
|
||||
return composite
|
||||
}
|
||||
|
||||
func applyMMR(ctx context.Context, results []*types.SearchResult, chatManage *types.ChatManage, k int, lambda float64) []*types.SearchResult {
|
||||
if k <= 0 || len(results) == 0 {
|
||||
return nil
|
||||
}
|
||||
pipelineInfo(ctx, "Rerank", "mmr_start", map[string]interface{}{
|
||||
"lambda": lambda,
|
||||
"k": k,
|
||||
"candidates": len(results),
|
||||
})
|
||||
selected := make([]*types.SearchResult, 0, k)
|
||||
candidates := make([]*types.SearchResult, len(results))
|
||||
copy(candidates, results)
|
||||
tokenSets := make([]map[string]struct{}, len(candidates))
|
||||
for i, r := range candidates {
|
||||
tokenSets[i] = tokenizeSimple(getEnrichedPassage(ctx, r))
|
||||
}
|
||||
for len(selected) < k && len(candidates) > 0 {
|
||||
bestIdx := 0
|
||||
bestScore := -1.0
|
||||
for i, r := range candidates {
|
||||
relevance := r.Score
|
||||
redundancy := 0.0
|
||||
for _, s := range selected {
|
||||
redundancy = math.Max(redundancy, jaccard(tokenSets[i], tokenizeSimple(getEnrichedPassage(ctx, s))))
|
||||
}
|
||||
mmr := lambda*relevance - (1.0-lambda)*redundancy
|
||||
if mmr > bestScore {
|
||||
bestScore = mmr
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
selected = append(selected, candidates[bestIdx])
|
||||
candidates = append(candidates[:bestIdx], candidates[bestIdx+1:]...)
|
||||
}
|
||||
// Compute average redundancy among selected
|
||||
avgRed := 0.0
|
||||
if len(selected) > 1 {
|
||||
pairs := 0
|
||||
for i := 0; i < len(selected); i++ {
|
||||
for j := i + 1; j < len(selected); j++ {
|
||||
si := tokenizeSimple(getEnrichedPassage(ctx, selected[i]))
|
||||
sj := tokenizeSimple(getEnrichedPassage(ctx, selected[j]))
|
||||
avgRed += jaccard(si, sj)
|
||||
pairs++
|
||||
}
|
||||
}
|
||||
if pairs > 0 {
|
||||
avgRed /= float64(pairs)
|
||||
}
|
||||
}
|
||||
pipelineInfo(ctx, "Rerank", "mmr_done", map[string]interface{}{
|
||||
"selected": len(selected),
|
||||
"avg_redundancy": fmt.Sprintf("%.4f", avgRed),
|
||||
})
|
||||
return selected
|
||||
}
|
||||
|
||||
func tokenizeSimple(text string) map[string]struct{} {
|
||||
text = strings.ToLower(text)
|
||||
fields := strings.Fields(text)
|
||||
set := make(map[string]struct{}, len(fields))
|
||||
for _, f := range fields {
|
||||
if len(f) > 1 {
|
||||
set[f] = struct{}{}
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func jaccard(a, b map[string]struct{}) float64 {
|
||||
if len(a) == 0 && len(b) == 0 {
|
||||
return 0
|
||||
}
|
||||
inter := 0
|
||||
for k := range a {
|
||||
if _, ok := b[k]; ok {
|
||||
inter++
|
||||
}
|
||||
}
|
||||
union := len(a) + len(b) - inter
|
||||
if union == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(inter) / float64(union)
|
||||
}
|
||||
|
||||
func clampFloat(v, minV, maxV float64) float64 {
|
||||
if v < minV {
|
||||
return minV
|
||||
}
|
||||
if v > maxV {
|
||||
return maxV
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// getEnrichedPassage 合并Content和ImageInfo的文本内容
|
||||
@@ -134,7 +328,9 @@ func getEnrichedPassage(ctx context.Context, result *types.SearchResult) string
|
||||
var imageInfos []types.ImageInfo
|
||||
err := json.Unmarshal([]byte(result.ImageInfo), &imageInfos)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to parse ImageInfo: %v, using content only", err)
|
||||
pipelineWarn(ctx, "Rerank", "image_info_parse", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return result.Content
|
||||
}
|
||||
|
||||
@@ -164,8 +360,10 @@ func getEnrichedPassage(ctx context.Context, result *types.SearchResult) string
|
||||
}
|
||||
combinedText += strings.Join(imageTexts, "\n")
|
||||
|
||||
logger.Debugf(ctx, "Enhanced passage with image info: content length %d, image texts length %d",
|
||||
len(result.Content), len(strings.Join(imageTexts, "\n")))
|
||||
pipelineInfo(ctx, "Rerank", "image_info_merge", map[string]interface{}{
|
||||
"content_len": len(result.Content),
|
||||
"image_len": len(strings.Join(imageTexts, "\n")),
|
||||
})
|
||||
|
||||
return combinedText
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
@@ -58,10 +57,28 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
// Initialize rewritten query as original query
|
||||
chatManage.RewriteQuery = chatManage.Query
|
||||
|
||||
if !chatManage.EnableRewrite {
|
||||
pipelineInfo(ctx, "Rewrite", "skip", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"reason": "rewrite_disabled",
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
pipelineInfo(ctx, "Rewrite", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"tenant_id": chatManage.TenantID,
|
||||
"user_query": chatManage.Query,
|
||||
"enable_rewrite": chatManage.EnableRewrite,
|
||||
})
|
||||
|
||||
// Get conversation history
|
||||
history, err := p.messageService.GetRecentMessagesBySession(ctx, chatManage.SessionID, 20)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to get conversation history, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineWarn(ctx, "Rewrite", "history_fetch", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Convert historical messages to conversation history structure
|
||||
@@ -99,22 +116,52 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
})
|
||||
|
||||
// Limit the number of historical records
|
||||
if len(historyList) > p.config.Conversation.MaxRounds {
|
||||
historyList = historyList[:p.config.Conversation.MaxRounds]
|
||||
maxRounds := p.config.Conversation.MaxRounds
|
||||
if chatManage.MaxRounds > 0 {
|
||||
maxRounds = chatManage.MaxRounds
|
||||
}
|
||||
if len(historyList) > maxRounds {
|
||||
historyList = historyList[:maxRounds]
|
||||
}
|
||||
|
||||
// Reverse to chronological order
|
||||
slices.Reverse(historyList)
|
||||
chatManage.History = historyList
|
||||
|
||||
userTmpl, err := template.New("rewriteContent").Parse(p.config.Conversation.RewritePromptUser)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to execute template, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
if len(historyList) == 0 {
|
||||
pipelineInfo(ctx, "Rewrite", "skip", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"reason": "empty_history",
|
||||
})
|
||||
return next()
|
||||
}
|
||||
systemTmpl, err := template.New("rewriteContent").Parse(p.config.Conversation.RewritePromptSystem)
|
||||
pipelineInfo(ctx, "Rewrite", "history_ready", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"history_rounds": len(historyList),
|
||||
"max_rounds": maxRounds,
|
||||
})
|
||||
|
||||
userPrompt := p.config.Conversation.RewritePromptUser
|
||||
if chatManage.RewritePromptUser != "" {
|
||||
userPrompt = chatManage.RewritePromptUser
|
||||
}
|
||||
userTmpl, err := template.New("rewriteContent").Parse(userPrompt)
|
||||
if err != nil {
|
||||
logger.GetLogger(ctx).Errorf("Failed to execute template, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineError(ctx, "Rewrite", "parse_user_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
systemPrompt := p.config.Conversation.RewritePromptSystem
|
||||
if chatManage.RewritePromptSystem != "" {
|
||||
systemPrompt = chatManage.RewritePromptSystem
|
||||
}
|
||||
systemTmpl, err := template.New("rewriteContent").Parse(systemPrompt)
|
||||
if err != nil {
|
||||
pipelineError(ctx, "Rewrite", "parse_system_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||
@@ -126,7 +173,10 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
"Conversation": historyList,
|
||||
})
|
||||
if err != nil {
|
||||
logger.GetLogger(ctx).Errorf("Failed to execute template, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineError(ctx, "Rewrite", "render_user_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
err = systemTmpl.Execute(&systemContent, map[string]interface{}{
|
||||
@@ -136,12 +186,19 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
"Conversation": historyList,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to execute template, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineError(ctx, "Rewrite", "render_system_template", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
rewriteModel, err := p.modelService.GetChatModel(ctx, chatManage.ChatModelID)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to get model, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineError(ctx, "Rewrite", "get_model", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"chat_model_id": chatManage.ChatModelID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -162,7 +219,10 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
Thinking: &thinking,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to execute model, session_id: %s, error: %v", chatManage.SessionID, err)
|
||||
pipelineError(ctx, "Rewrite", "model_call", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -170,7 +230,9 @@ func (p *PluginRewrite) OnEvent(ctx context.Context,
|
||||
// Update rewritten query
|
||||
chatManage.RewriteQuery = response.Content
|
||||
}
|
||||
logger.GetLogger(ctx).Infof("Rewritten query, session_id: %s, rewrite_query: %s",
|
||||
chatManage.SessionID, chatManage.RewriteQuery)
|
||||
pipelineInfo(ctx, "Rewrite", "output", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"rewrite_query": chatManage.RewriteQuery,
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package chatpipline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// PluginSearch implements search functionality for chat pipeline
|
||||
@@ -53,23 +54,42 @@ func (p *PluginSearch) ActivationEvents() []types.EventType {
|
||||
|
||||
// OnEvent handles search events in the chat pipeline
|
||||
func (p *PluginSearch) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
// Get knowledge base IDs list
|
||||
knowledgeBaseIDs := chatManage.KnowledgeBaseIDs
|
||||
if len(knowledgeBaseIDs) == 0 && chatManage.KnowledgeBaseID != "" {
|
||||
// Fall back to single knowledge base
|
||||
knowledgeBaseIDs = []string{chatManage.KnowledgeBaseID}
|
||||
logger.Infof(ctx, "No KnowledgeBaseIDs provided, falling back to single KB: %s", chatManage.KnowledgeBaseID)
|
||||
pipelineInfo(ctx, "Search", "fallback_kb", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"kb_id": chatManage.KnowledgeBaseID,
|
||||
})
|
||||
}
|
||||
|
||||
if len(knowledgeBaseIDs) == 0 {
|
||||
logger.Errorf(ctx, "No knowledge base IDs available for search")
|
||||
pipelineError(ctx, "Search", "kb_not_found", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return ErrSearch.WithError(nil)
|
||||
}
|
||||
|
||||
pipelineInfo(ctx, "Search", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"rewrite_query": chatManage.RewriteQuery,
|
||||
"processed_query": chatManage.ProcessedQuery,
|
||||
"kb_ids": strings.Join(knowledgeBaseIDs, ","),
|
||||
"tenant_id": chatManage.TenantID,
|
||||
"web_enabled": chatManage.WebSearchEnabled,
|
||||
})
|
||||
|
||||
// Run KB search and web search concurrently
|
||||
logger.Infof(ctx, "Searching across %d knowledge base(s): %v", len(knowledgeBaseIDs), knowledgeBaseIDs)
|
||||
pipelineInfo(ctx, "Search", "plan", map[string]interface{}{
|
||||
"kb_count": len(knowledgeBaseIDs),
|
||||
"embedding_top_k": chatManage.EmbeddingTopK,
|
||||
"vector_threshold": chatManage.VectorThreshold,
|
||||
"keyword_threshold": chatManage.KeywordThreshold,
|
||||
})
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
allResults := make([]*types.SearchResult, 0)
|
||||
@@ -97,30 +117,116 @@ func (p *PluginSearch) OnEvent(ctx context.Context,
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
wg.Wait()
|
||||
|
||||
chatManage.SearchResult = allResults
|
||||
chatManage.SearchResult = allResults
|
||||
|
||||
// If recall is low, attempt query expansion with keyword-focused search
|
||||
if len(chatManage.SearchResult) < max(1, chatManage.EmbeddingTopK/2) {
|
||||
pipelineInfo(ctx, "Search", "recall_low", map[string]interface{}{
|
||||
"current": len(chatManage.SearchResult),
|
||||
"threshold": chatManage.EmbeddingTopK / 2,
|
||||
})
|
||||
expansions := p.expandQueries(ctx, chatManage)
|
||||
if len(expansions) > 0 {
|
||||
pipelineInfo(ctx, "Search", "expansion_start", map[string]interface{}{
|
||||
"variants": len(expansions),
|
||||
})
|
||||
expTopK := max(chatManage.EmbeddingTopK*2, chatManage.RerankTopK*2)
|
||||
expKwTh := chatManage.KeywordThreshold * 0.8
|
||||
// Concurrent expansion retrieval across queries and KBs
|
||||
expResults := make([]*types.SearchResult, 0, expTopK*len(expansions))
|
||||
var muExp sync.Mutex
|
||||
var wgExp sync.WaitGroup
|
||||
jobs := len(expansions) * len(knowledgeBaseIDs)
|
||||
capSem := 16
|
||||
if jobs < capSem {
|
||||
capSem = jobs
|
||||
}
|
||||
if capSem <= 0 {
|
||||
capSem = 1
|
||||
}
|
||||
sem := make(chan struct{}, capSem)
|
||||
pipelineInfo(ctx, "Search", "expansion_concurrency", map[string]interface{}{
|
||||
"jobs": jobs,
|
||||
"cap": capSem,
|
||||
})
|
||||
for _, q := range expansions {
|
||||
for _, kbID := range knowledgeBaseIDs {
|
||||
wgExp.Add(1)
|
||||
go func(q string, kbID string) {
|
||||
defer wgExp.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
paramsExp := types.SearchParams{
|
||||
QueryText: q,
|
||||
VectorThreshold: chatManage.VectorThreshold,
|
||||
KeywordThreshold: expKwTh,
|
||||
MatchCount: expTopK,
|
||||
DisableVectorMatch: true,
|
||||
DisableKeywordsMatch: false,
|
||||
}
|
||||
res, err := p.knowledgeBaseService.HybridSearch(ctx, kbID, paramsExp)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "Search", "expansion_error", map[string]interface{}{
|
||||
"kb_id": kbID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(res) > 0 {
|
||||
pipelineInfo(ctx, "Search", "expansion_hits", map[string]interface{}{
|
||||
"kb_id": kbID,
|
||||
"query": truncateForLog(q),
|
||||
"hits": len(res),
|
||||
})
|
||||
muExp.Lock()
|
||||
expResults = append(expResults, res...)
|
||||
muExp.Unlock()
|
||||
}
|
||||
}(q, kbID)
|
||||
}
|
||||
}
|
||||
wgExp.Wait()
|
||||
if len(expResults) > 0 {
|
||||
pipelineInfo(ctx, "Search", "expansion_done", map[string]interface{}{
|
||||
"added": len(expResults),
|
||||
})
|
||||
chatManage.SearchResult = append(chatManage.SearchResult, expResults...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add relevant results from chat history
|
||||
historyResult := p.getSearchResultFromHistory(chatManage)
|
||||
if historyResult != nil {
|
||||
logger.Infof(ctx, "Add history result, result count: %d", len(historyResult))
|
||||
pipelineInfo(ctx, "Search", "history_hits", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"history_hits": len(historyResult),
|
||||
})
|
||||
chatManage.SearchResult = append(chatManage.SearchResult, historyResult...)
|
||||
}
|
||||
|
||||
// Remove duplicate results
|
||||
chatManage.SearchResult = removeDuplicateResults(chatManage.SearchResult)
|
||||
// Remove duplicate results
|
||||
before := len(chatManage.SearchResult)
|
||||
chatManage.SearchResult = removeDuplicateResults(chatManage.SearchResult)
|
||||
pipelineInfo(ctx, "Search", "dedup_summary", map[string]interface{}{
|
||||
"before": before,
|
||||
"after": len(chatManage.SearchResult),
|
||||
})
|
||||
|
||||
// Return if we have results
|
||||
if len(chatManage.SearchResult) != 0 {
|
||||
logger.Infof(
|
||||
ctx,
|
||||
"Get search results, count: %d, session_id: %s",
|
||||
len(chatManage.SearchResult), chatManage.SessionID,
|
||||
)
|
||||
pipelineInfo(ctx, "Search", "output", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"result_count": len(chatManage.SearchResult),
|
||||
})
|
||||
return next()
|
||||
}
|
||||
logger.Infof(ctx, "No search result, session_id: %s", chatManage.SessionID)
|
||||
pipelineWarn(ctx, "Search", "output", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"result_count": 0,
|
||||
})
|
||||
return ErrSearchNothing
|
||||
}
|
||||
|
||||
@@ -143,15 +249,52 @@ func (p *PluginSearch) getSearchResultFromHistory(chatManage *types.ChatManage)
|
||||
}
|
||||
|
||||
func removeDuplicateResults(results []*types.SearchResult) []*types.SearchResult {
|
||||
seen := make(map[string]bool)
|
||||
var uniqueResults []*types.SearchResult
|
||||
for _, result := range results {
|
||||
if !seen[result.ID] {
|
||||
seen[result.ID] = true
|
||||
uniqueResults = append(uniqueResults, result)
|
||||
}
|
||||
}
|
||||
return uniqueResults
|
||||
seen := make(map[string]bool)
|
||||
contentSig := make(map[string]bool)
|
||||
var uniqueResults []*types.SearchResult
|
||||
for _, r := range results {
|
||||
keys := []string{r.ID}
|
||||
if r.ParentChunkID != "" {
|
||||
keys = append(keys, "parent:"+r.ParentChunkID)
|
||||
}
|
||||
if r.KnowledgeID != "" {
|
||||
keys = append(keys, fmt.Sprintf("kb:%s#%d", r.KnowledgeID, r.ChunkIndex))
|
||||
}
|
||||
dup := false
|
||||
for _, k := range keys {
|
||||
if seen[k] {
|
||||
dup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if dup {
|
||||
continue
|
||||
}
|
||||
sig := buildContentSignature(r.Content)
|
||||
if sig != "" {
|
||||
if contentSig[sig] {
|
||||
continue
|
||||
}
|
||||
contentSig[sig] = true
|
||||
}
|
||||
for _, k := range keys {
|
||||
seen[k] = true
|
||||
}
|
||||
uniqueResults = append(uniqueResults, r)
|
||||
}
|
||||
return uniqueResults
|
||||
}
|
||||
|
||||
func buildContentSignature(content string) string {
|
||||
c := strings.ToLower(strings.TrimSpace(content))
|
||||
if c == "" {
|
||||
return ""
|
||||
}
|
||||
c = strings.Join(strings.Fields(c), " ")
|
||||
if len(c) > 128 {
|
||||
c = c[:128]
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// searchKnowledgeBases performs KB searches for rewrite and processed queries across KB IDs
|
||||
@@ -175,10 +318,19 @@ func (p *PluginSearch) searchKnowledgeBases(ctx context.Context, knowledgeBaseID
|
||||
defer wg.Done()
|
||||
res, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, baseParams)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to search KB %s: %v", knowledgeBaseID, err)
|
||||
pipelineWarn(ctx, "Search", "kb_search_error", map[string]interface{}{
|
||||
"kb_id": knowledgeBaseID,
|
||||
"query": baseParams.QueryText,
|
||||
"error": err.Error(),
|
||||
"query_ty": "rewrite",
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx, "KB %s search results count: %d", knowledgeBaseID, len(res))
|
||||
pipelineInfo(ctx, "Search", "kb_result", map[string]interface{}{
|
||||
"kb_id": knowledgeBaseID,
|
||||
"query_ty": "rewrite",
|
||||
"hit_count": len(res),
|
||||
})
|
||||
mu.Lock()
|
||||
results = append(results, res...)
|
||||
mu.Unlock()
|
||||
@@ -191,7 +343,9 @@ func (p *PluginSearch) searchKnowledgeBases(ctx context.Context, knowledgeBaseID
|
||||
if chatManage.RewriteQuery != chatManage.ProcessedQuery {
|
||||
paramsProcessed := baseParams
|
||||
paramsProcessed.QueryText = strings.TrimSpace(chatManage.ProcessedQuery)
|
||||
logger.Infof(ctx, "Searching with processed query: %s", paramsProcessed.QueryText)
|
||||
pipelineInfo(ctx, "Search", "processed_query_search", map[string]interface{}{
|
||||
"query": paramsProcessed.QueryText,
|
||||
})
|
||||
|
||||
wg = sync.WaitGroup{}
|
||||
for _, kbID := range knowledgeBaseIDs {
|
||||
@@ -200,10 +354,19 @@ func (p *PluginSearch) searchKnowledgeBases(ctx context.Context, knowledgeBaseID
|
||||
defer wg.Done()
|
||||
res, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, paramsProcessed)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to search KB %s with processed query: %v", knowledgeBaseID, err)
|
||||
pipelineWarn(ctx, "Search", "kb_search_error", map[string]interface{}{
|
||||
"kb_id": knowledgeBaseID,
|
||||
"query": paramsProcessed.QueryText,
|
||||
"error": err.Error(),
|
||||
"query_ty": "processed",
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx, "KB %s processed query results count: %d", knowledgeBaseID, len(res))
|
||||
pipelineInfo(ctx, "Search", "kb_result", map[string]interface{}{
|
||||
"kb_id": knowledgeBaseID,
|
||||
"query_ty": "processed",
|
||||
"hit_count": len(res),
|
||||
})
|
||||
mu.Lock()
|
||||
results = append(results, res...)
|
||||
mu.Unlock()
|
||||
@@ -212,7 +375,9 @@ func (p *PluginSearch) searchKnowledgeBases(ctx context.Context, knowledgeBaseID
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Total KB results (rewrite + processed): %d", len(results))
|
||||
pipelineInfo(ctx, "Search", "kb_result_summary", map[string]interface{}{
|
||||
"total_hits": len(results),
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -223,14 +388,22 @@ func (p *PluginSearch) searchWebIfEnabled(ctx context.Context, chatManage *types
|
||||
}
|
||||
tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
if tenant == nil || tenant.WebSearchConfig == nil || tenant.WebSearchConfig.Provider == "" {
|
||||
logger.Warnf(ctx, "Web search enabled but no valid configuration found for tenant %d", chatManage.TenantID)
|
||||
pipelineWarn(ctx, "Search", "web_config_missing", map[string]interface{}{
|
||||
"tenant_id": chatManage.TenantID,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Performing web search with provider: %s", tenant.WebSearchConfig.Provider)
|
||||
pipelineInfo(ctx, "Search", "web_request", map[string]interface{}{
|
||||
"tenant_id": chatManage.TenantID,
|
||||
"provider": tenant.WebSearchConfig.Provider,
|
||||
})
|
||||
webResults, err := p.webSearchService.Search(ctx, tenant.WebSearchConfig, chatManage.RewriteQuery)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Web search failed: %v", err)
|
||||
pipelineWarn(ctx, "Search", "web_search_error", map[string]interface{}{
|
||||
"tenant_id": chatManage.TenantID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
// Build questions (rewrite + processed if different)
|
||||
@@ -245,14 +418,18 @@ func (p *PluginSearch) searchWebIfEnabled(ctx context.Context, chatManage *types
|
||||
p.knowledgeBaseService, p.knowledgeService, seen, ids,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "RAG compression failed, falling back to raw: %v", err)
|
||||
pipelineWarn(ctx, "Search", "web_compress_error", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
webResults = compressed
|
||||
// Persist temp KB state back into Redis using SessionService
|
||||
p.sessionService.SaveWebSearchTempKBState(ctx, chatManage.SessionID, kbID, newSeen, newIDs)
|
||||
}
|
||||
res := convertWebSearchResults(webResults)
|
||||
logger.Infof(ctx, "Web search returned %d results", len(res))
|
||||
pipelineInfo(ctx, "Search", "web_hits", map[string]interface{}{
|
||||
"hit_count": len(res),
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -323,3 +500,79 @@ func convertWebSearchResults(webResults []*types.WebSearchResult) []*types.Searc
|
||||
|
||||
return results
|
||||
}
|
||||
// expandQueries generates paraphrases and synonyms using chat model to improve keyword recall
|
||||
func (p *PluginSearch) expandQueries(ctx context.Context, chatManage *types.ChatManage) []string {
|
||||
if p.modelService == nil || chatManage.ChatModelID == "" {
|
||||
pipelineWarn(ctx, "Search", "expansion_skip", map[string]interface{}{
|
||||
"reason": "no_model",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
model, err := p.modelService.GetChatModel(ctx, chatManage.ChatModelID)
|
||||
if err != nil {
|
||||
pipelineWarn(ctx, "Search", "expansion_get_model_failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
sys := "Generate up to 5 diverse paraphrases or keyword variants for the user query to improve keyword-based search recall. Respond ONLY with a JSON array of strings inside a fenced code block."
|
||||
usr := chatManage.RewriteQuery
|
||||
think := false
|
||||
resp, err := model.Chat(ctx, []chat.Message{{Role: "system", Content: sys}, {Role: "user", Content: usr}}, &chat.ChatOptions{Temperature: 0.2, MaxCompletionTokens: 80, Thinking: &think})
|
||||
if err != nil || resp.Content == "" {
|
||||
pipelineWarn(ctx, "Search", "expansion_model_call_failed", map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
body := extractJSONBlock(resp.Content)
|
||||
var arr []string
|
||||
if err := json.Unmarshal([]byte(body), &arr); err != nil || len(arr) == 0 {
|
||||
// Fallback: split lines
|
||||
lines := strings.Split(resp.Content, "\n")
|
||||
for _, l := range lines {
|
||||
l = strings.TrimSpace(l)
|
||||
if l != "" {
|
||||
arr = append(arr, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
uniq := make(map[string]struct{})
|
||||
base := []string{chatManage.Query, chatManage.RewriteQuery, chatManage.ProcessedQuery}
|
||||
for _, b := range base {
|
||||
if s := strings.TrimSpace(b); s != "" {
|
||||
uniq[strings.ToLower(s)] = struct{}{}
|
||||
}
|
||||
}
|
||||
expansions := make([]string, 0, len(arr))
|
||||
for _, a := range arr {
|
||||
s := strings.TrimSpace(a)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(s)
|
||||
if _, ok := uniq[key]; ok {
|
||||
continue
|
||||
}
|
||||
uniq[key] = struct{}{}
|
||||
expansions = append(expansions, s)
|
||||
if len(expansions) >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
pipelineInfo(ctx, "Search", "expansion_result", map[string]interface{}{
|
||||
"variants": len(expansions),
|
||||
})
|
||||
return expansions
|
||||
}
|
||||
|
||||
func extractJSONBlock(text string) string {
|
||||
t := strings.TrimSpace(text)
|
||||
if i := strings.Index(t, "["); i >= 0 {
|
||||
j := strings.LastIndex(t, "]")
|
||||
if j > i {
|
||||
return t[i : j+1]
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/event"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -31,11 +30,17 @@ func (p *PluginStreamFilter) ActivationEvents() []types.EventType {
|
||||
func (p *PluginStreamFilter) OnEvent(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Starting stream filter")
|
||||
pipelineInfo(ctx, "StreamFilter", "input", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
"has_event_bus": chatManage.EventBus != nil,
|
||||
"no_match_prefix": chatManage.SummaryConfig.NoMatchPrefix,
|
||||
})
|
||||
|
||||
// EventBus is required
|
||||
if chatManage.EventBus == nil {
|
||||
logger.Error(ctx, "EventBus is required but not available")
|
||||
pipelineError(ctx, "StreamFilter", "eventbus_missing", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return ErrModelCall.WithError(errors.New("EventBus is required for stream filtering"))
|
||||
}
|
||||
eventBus := chatManage.EventBus
|
||||
@@ -44,13 +49,17 @@ func (p *PluginStreamFilter) OnEvent(ctx context.Context,
|
||||
matchNoMatchBuilderPrefix := chatManage.SummaryConfig.NoMatchPrefix != ""
|
||||
|
||||
if matchNoMatchBuilderPrefix {
|
||||
logger.Infof(ctx, "Using no match prefix filter: %s", chatManage.SummaryConfig.NoMatchPrefix)
|
||||
pipelineInfo(ctx, "StreamFilter", "enable_prefix_filter", map[string]interface{}{
|
||||
"prefix": chatManage.SummaryConfig.NoMatchPrefix,
|
||||
})
|
||||
// Create an event interceptor for prefix filtering
|
||||
return p.filterEventsWithPrefix(ctx, chatManage, eventBus, next)
|
||||
}
|
||||
|
||||
// No filtering needed, just pass through
|
||||
logger.Info(ctx, "No prefix filtering required, passing through")
|
||||
pipelineInfo(ctx, "StreamFilter", "passthrough", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -61,7 +70,9 @@ func (p *PluginStreamFilter) filterEventsWithPrefix(
|
||||
originalEventBus types.EventBusInterface,
|
||||
next func() *PluginError,
|
||||
) *PluginError {
|
||||
logger.Info(ctx, "Setting up event-based stream filtering with NoMatchPrefix")
|
||||
pipelineInfo(ctx, "StreamFilter", "setup_temp_bus", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
|
||||
// Create a temporary EventBus to intercept events
|
||||
tempEventBus := event.NewEventBus()
|
||||
@@ -81,7 +92,9 @@ func (p *PluginStreamFilter) filterEventsWithPrefix(
|
||||
|
||||
// Check if content does NOT match the no-match prefix (meaning it's valid content)
|
||||
if !strings.HasPrefix(chatManage.SummaryConfig.NoMatchPrefix, responseBuilder.String()) {
|
||||
logger.Infof(ctx, "Content does not match no-match prefix, emitting valid content: %s", responseBuilder.String())
|
||||
pipelineInfo(ctx, "StreamFilter", "emit_valid_chunk", map[string]interface{}{
|
||||
"chunk_len": len(responseBuilder.String()),
|
||||
})
|
||||
|
||||
// Emit the accumulated content as valid answer
|
||||
originalEventBus.Emit(ctx, types.Event{
|
||||
@@ -104,7 +117,9 @@ func (p *PluginStreamFilter) filterEventsWithPrefix(
|
||||
|
||||
// After pipeline completes, check if we need fallback
|
||||
if !matchFound && responseBuilder.Len() > 0 {
|
||||
logger.Info(ctx, "Content matches no-match prefix, emitting fallback response")
|
||||
pipelineInfo(ctx, "StreamFilter", "emit_fallback", map[string]interface{}{
|
||||
"session_id": chatManage.SessionID,
|
||||
})
|
||||
fallbackID := fmt.Sprintf("%s-fallback", uuid.New().String()[:8])
|
||||
originalEventBus.Emit(ctx, types.Event{
|
||||
ID: fallbackID,
|
||||
|
||||
@@ -77,7 +77,7 @@ func (p *PluginTracing) OnEvent(ctx context.Context,
|
||||
|
||||
// Search traces search operations in the chat pipeline
|
||||
func (p *PluginTracing) Search(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
_, span := tracing.ContextWithSpan(ctx, "PluginTracing.Search")
|
||||
defer span.End()
|
||||
@@ -87,18 +87,23 @@ func (p *PluginTracing) Search(ctx context.Context,
|
||||
attribute.Float64("keyword_threshold", chatManage.KeywordThreshold),
|
||||
attribute.Int("match_count", chatManage.EmbeddingTopK),
|
||||
)
|
||||
err := next()
|
||||
searchResultJson, _ := json.Marshal(chatManage.SearchResult)
|
||||
span.SetAttributes(
|
||||
attribute.String("hybrid_search", string(searchResultJson)),
|
||||
attribute.String("processed_query", chatManage.ProcessedQuery),
|
||||
)
|
||||
return err
|
||||
err := next()
|
||||
searchResultJson, _ := json.Marshal(chatManage.SearchResult)
|
||||
unique := make(map[string]struct{})
|
||||
for _, r := range chatManage.SearchResult {
|
||||
unique[r.ID] = struct{}{}
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.String("hybrid_search", string(searchResultJson)),
|
||||
attribute.String("processed_query", chatManage.ProcessedQuery),
|
||||
attribute.Int("search_unique_count", len(unique)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Rerank traces rerank operations in the chat pipeline
|
||||
func (p *PluginTracing) Rerank(ctx context.Context,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
eventType types.EventType, chatManage *types.ChatManage, next func() *PluginError,
|
||||
) *PluginError {
|
||||
_, span := tracing.ContextWithSpan(ctx, "PluginTracing.Rerank")
|
||||
defer span.End()
|
||||
@@ -109,13 +114,14 @@ func (p *PluginTracing) Rerank(ctx context.Context,
|
||||
attribute.Float64("rerank_filter_threshold", chatManage.RerankThreshold),
|
||||
attribute.Int("rerank_filter_topk", chatManage.RerankTopK),
|
||||
)
|
||||
err := next()
|
||||
resultJson, _ := json.Marshal(chatManage.RerankResult)
|
||||
span.SetAttributes(
|
||||
attribute.Int("rerank_resp_count", len(chatManage.RerankResult)),
|
||||
attribute.String("rerank_resp_results", string(resultJson)),
|
||||
)
|
||||
return err
|
||||
err := next()
|
||||
resultJson, _ := json.Marshal(chatManage.RerankResult)
|
||||
span.SetAttributes(
|
||||
attribute.Int("rerank_resp_count", len(chatManage.RerankResult)),
|
||||
attribute.String("rerank_resp_results", string(resultJson)),
|
||||
attribute.String("query_intent", chatManage.QueryIntent),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Merge traces merge operations in the chat pipeline
|
||||
|
||||
@@ -261,6 +261,7 @@ func (e *EvaluationService) Evaluation(ctx context.Context,
|
||||
VectorThreshold: e.config.Conversation.VectorThreshold,
|
||||
KeywordThreshold: e.config.Conversation.KeywordThreshold,
|
||||
EmbeddingTopK: e.config.Conversation.EmbeddingTopK,
|
||||
MaxRounds: e.config.Conversation.MaxRounds,
|
||||
RerankModelID: rerankModelID,
|
||||
RerankTopK: e.config.Conversation.RerankTopK,
|
||||
RerankThreshold: e.config.Conversation.RerankThreshold,
|
||||
@@ -279,7 +280,9 @@ func (e *EvaluationService) Evaluation(ctx context.Context,
|
||||
Seed: e.config.Conversation.Summary.Seed,
|
||||
MaxCompletionTokens: e.config.Conversation.Summary.MaxCompletionTokens,
|
||||
},
|
||||
FallbackResponse: e.config.Conversation.FallbackResponse,
|
||||
FallbackResponse: e.config.Conversation.FallbackResponse,
|
||||
RewritePromptSystem: e.config.Conversation.RewritePromptSystem,
|
||||
RewritePromptUser: e.config.Conversation.RewritePromptUser,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -372,6 +372,41 @@ func (s *knowledgeBaseService) HybridSearch(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Normalize keyword retriever scores into [0,1] per-engine batch
|
||||
for i := range retrieveResults {
|
||||
rr := retrieveResults[i]
|
||||
if rr.Error != nil || rr.RetrieverType != types.KeywordsRetrieverType || len(rr.Results) == 0 {
|
||||
continue
|
||||
}
|
||||
minS := rr.Results[0].Score
|
||||
maxS := rr.Results[0].Score
|
||||
for _, r := range rr.Results {
|
||||
if r.Score < minS {
|
||||
minS = r.Score
|
||||
}
|
||||
if r.Score > maxS {
|
||||
maxS = r.Score
|
||||
}
|
||||
}
|
||||
if maxS > minS {
|
||||
for _, r := range rr.Results {
|
||||
ns := (r.Score - minS) / (maxS - minS)
|
||||
if ns < 0 {
|
||||
ns = 0
|
||||
} else if ns > 1 {
|
||||
ns = 1
|
||||
}
|
||||
r.Score = ns
|
||||
}
|
||||
logger.Infof(ctx, "Normalized keyword scores for engine %s: min=%f, max=%f", rr.RetrieverEngineType, minS, maxS)
|
||||
} else {
|
||||
for _, r := range rr.Results {
|
||||
r.Score = 1.0
|
||||
}
|
||||
logger.Infof(ctx, "Keyword scores have no variance for engine %s, set to 1.0", rr.RetrieverEngineType)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all results from different retrievers and deduplicate by chunk ID
|
||||
logger.Infof(ctx, "Processing retrieval results")
|
||||
matchResults := []*types.IndexWithScore{}
|
||||
|
||||
@@ -436,40 +436,100 @@ func (s *sessionService) KnowledgeQA(ctx context.Context, session *types.Session
|
||||
return err
|
||||
}
|
||||
|
||||
rewritePromptSystem := s.cfg.Conversation.RewritePromptSystem
|
||||
rewritePromptUser := s.cfg.Conversation.RewritePromptUser
|
||||
var tenantConv *types.ConversationConfig
|
||||
if tc, err := getTenantConversationConfig(ctx); err == nil {
|
||||
tenantConv = tc
|
||||
} else {
|
||||
logger.Warnf(ctx, "Failed to load tenant conversation config, tenant ID: %d, error: %v", session.TenantID, err)
|
||||
}
|
||||
|
||||
vectorThreshold := session.VectorThreshold
|
||||
keywordThreshold := session.KeywordThreshold
|
||||
embeddingTopK := session.EmbeddingTopK
|
||||
rerankModelID := session.RerankModelID
|
||||
rerankTopK := session.RerankTopK
|
||||
rerankThreshold := session.RerankThreshold
|
||||
maxRounds := session.MaxRounds
|
||||
fallbackResponse := session.FallbackResponse
|
||||
enableRewrite := session.EnableRewrite
|
||||
|
||||
summaryParams := session.SummaryParameters
|
||||
if summaryParams == nil {
|
||||
summaryParams = &types.SummaryConfig{}
|
||||
}
|
||||
summaryConfig := types.SummaryConfig{
|
||||
MaxTokens: summaryParams.MaxTokens,
|
||||
RepeatPenalty: summaryParams.RepeatPenalty,
|
||||
TopK: summaryParams.TopK,
|
||||
TopP: summaryParams.TopP,
|
||||
FrequencyPenalty: summaryParams.FrequencyPenalty,
|
||||
PresencePenalty: summaryParams.PresencePenalty,
|
||||
Prompt: summaryParams.Prompt,
|
||||
ContextTemplate: summaryParams.ContextTemplate,
|
||||
Temperature: summaryParams.Temperature,
|
||||
Seed: summaryParams.Seed,
|
||||
NoMatchPrefix: summaryParams.NoMatchPrefix,
|
||||
MaxCompletionTokens: summaryParams.MaxCompletionTokens,
|
||||
}
|
||||
|
||||
if tenantConv != nil {
|
||||
vectorThreshold = tenantConv.VectorThreshold
|
||||
keywordThreshold = tenantConv.KeywordThreshold
|
||||
embeddingTopK = tenantConv.EmbeddingTopK
|
||||
rerankModelID = tenantConv.RerankModelID
|
||||
rerankTopK = tenantConv.RerankTopK
|
||||
rerankThreshold = tenantConv.RerankThreshold
|
||||
maxRounds = tenantConv.MaxRounds
|
||||
fallbackResponse = tenantConv.FallbackResponse
|
||||
enableRewrite = tenantConv.EnableRewrite
|
||||
|
||||
if tenantConv.MaxTokens != 0 {
|
||||
summaryConfig.MaxTokens = tenantConv.MaxTokens
|
||||
}
|
||||
if tenantConv.Prompt != "" {
|
||||
summaryConfig.Prompt = tenantConv.Prompt
|
||||
}
|
||||
if tenantConv.ContextTemplate != "" {
|
||||
summaryConfig.ContextTemplate = tenantConv.ContextTemplate
|
||||
}
|
||||
if tenantConv.Temperature != 0 {
|
||||
summaryConfig.Temperature = tenantConv.Temperature
|
||||
}
|
||||
if tenantConv.RewritePromptSystem != "" {
|
||||
rewritePromptSystem = tenantConv.RewritePromptSystem
|
||||
}
|
||||
if tenantConv.RewritePromptUser != "" {
|
||||
rewritePromptUser = tenantConv.RewritePromptUser
|
||||
}
|
||||
}
|
||||
|
||||
// Create chat management object with session settings
|
||||
logger.Infof(ctx, "Creating chat manage object, knowledge base IDs: %v, chat model ID: %s", knowledgeBaseIDs, chatModelID)
|
||||
chatManage := &types.ChatManage{
|
||||
Query: query,
|
||||
RewriteQuery: query,
|
||||
SessionID: session.ID,
|
||||
MessageID: assistantMessageID, // NEW: For event emission in pipeline
|
||||
KnowledgeBaseID: knowledgeBaseIDs[0], // For backward compatibility, use first KB ID
|
||||
KnowledgeBaseIDs: knowledgeBaseIDs, // Multi-KB support
|
||||
VectorThreshold: session.VectorThreshold,
|
||||
KeywordThreshold: session.KeywordThreshold,
|
||||
EmbeddingTopK: session.EmbeddingTopK,
|
||||
RerankModelID: session.RerankModelID,
|
||||
RerankTopK: session.RerankTopK,
|
||||
RerankThreshold: session.RerankThreshold,
|
||||
ChatModelID: chatModelID,
|
||||
SummaryConfig: types.SummaryConfig{
|
||||
MaxTokens: session.SummaryParameters.MaxTokens,
|
||||
RepeatPenalty: session.SummaryParameters.RepeatPenalty,
|
||||
TopK: session.SummaryParameters.TopK,
|
||||
TopP: session.SummaryParameters.TopP,
|
||||
FrequencyPenalty: session.SummaryParameters.FrequencyPenalty,
|
||||
PresencePenalty: session.SummaryParameters.PresencePenalty,
|
||||
Prompt: session.SummaryParameters.Prompt,
|
||||
ContextTemplate: session.SummaryParameters.ContextTemplate,
|
||||
Temperature: session.SummaryParameters.Temperature,
|
||||
Seed: session.SummaryParameters.Seed,
|
||||
NoMatchPrefix: session.SummaryParameters.NoMatchPrefix,
|
||||
MaxCompletionTokens: session.SummaryParameters.MaxCompletionTokens,
|
||||
},
|
||||
FallbackResponse: session.FallbackResponse,
|
||||
EventBus: eventBus.AsEventBusInterface(), // NEW: For pipeline to emit events directly
|
||||
WebSearchEnabled: webSearchEnabled,
|
||||
TenantID: session.TenantID,
|
||||
Query: query,
|
||||
RewriteQuery: query,
|
||||
SessionID: session.ID,
|
||||
MessageID: assistantMessageID, // NEW: For event emission in pipeline
|
||||
KnowledgeBaseID: knowledgeBaseIDs[0], // For backward compatibility, use first KB ID
|
||||
KnowledgeBaseIDs: knowledgeBaseIDs, // Multi-KB support
|
||||
VectorThreshold: vectorThreshold,
|
||||
KeywordThreshold: keywordThreshold,
|
||||
EmbeddingTopK: embeddingTopK,
|
||||
RerankModelID: rerankModelID,
|
||||
RerankTopK: rerankTopK,
|
||||
RerankThreshold: rerankThreshold,
|
||||
MaxRounds: maxRounds,
|
||||
ChatModelID: chatModelID,
|
||||
SummaryConfig: summaryConfig,
|
||||
FallbackResponse: fallbackResponse,
|
||||
EventBus: eventBus.AsEventBusInterface(), // NEW: For pipeline to emit events directly
|
||||
WebSearchEnabled: webSearchEnabled,
|
||||
TenantID: session.TenantID,
|
||||
RewritePromptSystem: rewritePromptSystem,
|
||||
RewritePromptUser: rewritePromptUser,
|
||||
EnableRewrite: enableRewrite,
|
||||
}
|
||||
|
||||
// Start knowledge QA event processing
|
||||
@@ -676,6 +736,17 @@ func (s *sessionService) KnowledgeQAByEvent(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTenantConversationConfig(ctx context.Context) (*types.ConversationConfig, error) {
|
||||
tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
if tenant == nil {
|
||||
return nil, errors.New("tenant is empty")
|
||||
}
|
||||
if tenant.ConversationConfig == nil {
|
||||
return nil, errors.New("tenant has no conversation config")
|
||||
}
|
||||
return tenant.ConversationConfig, nil
|
||||
}
|
||||
|
||||
// SearchKnowledge performs knowledge base search without LLM summarization
|
||||
func (s *sessionService) SearchKnowledge(ctx context.Context,
|
||||
knowledgeBaseID, query string,
|
||||
@@ -685,14 +756,17 @@ func (s *sessionService) SearchKnowledge(ctx context.Context,
|
||||
|
||||
// Create default retrieval parameters
|
||||
chatManage := &types.ChatManage{
|
||||
Query: query,
|
||||
RewriteQuery: query,
|
||||
KnowledgeBaseID: knowledgeBaseID,
|
||||
VectorThreshold: s.cfg.Conversation.VectorThreshold, // Use default configuration
|
||||
KeywordThreshold: s.cfg.Conversation.KeywordThreshold, // Use default configuration
|
||||
EmbeddingTopK: s.cfg.Conversation.EmbeddingTopK, // Use default configuration
|
||||
RerankTopK: s.cfg.Conversation.RerankTopK, // Use default configuration
|
||||
RerankThreshold: s.cfg.Conversation.RerankThreshold, // Use default configuration
|
||||
Query: query,
|
||||
RewriteQuery: query,
|
||||
KnowledgeBaseID: knowledgeBaseID,
|
||||
VectorThreshold: s.cfg.Conversation.VectorThreshold, // Use default configuration
|
||||
KeywordThreshold: s.cfg.Conversation.KeywordThreshold, // Use default configuration
|
||||
EmbeddingTopK: s.cfg.Conversation.EmbeddingTopK, // Use default configuration
|
||||
RerankTopK: s.cfg.Conversation.RerankTopK, // Use default configuration
|
||||
RerankThreshold: s.cfg.Conversation.RerankThreshold, // Use default configuration
|
||||
MaxRounds: s.cfg.Conversation.MaxRounds,
|
||||
RewritePromptSystem: s.cfg.Conversation.RewritePromptSystem,
|
||||
RewritePromptUser: s.cfg.Conversation.RewritePromptUser,
|
||||
}
|
||||
|
||||
// Get default models
|
||||
|
||||
@@ -124,7 +124,7 @@ func BuildContainer(container *dig.Container) *dig.Container {
|
||||
|
||||
// Session service (depends on agent service)
|
||||
// SessionService is created after AgentService and passes itself to AgentService.CreateAgentEngine when needed
|
||||
must(container.Provide(service.NewSessionService))
|
||||
must(container.Provide(service.NewSessionService))
|
||||
|
||||
must(container.Provide(router.NewAsyncqClient))
|
||||
must(container.Provide(router.NewAsynqServer))
|
||||
|
||||
@@ -444,7 +444,7 @@ func (h *AgentStreamHandler) handleComplete(ctx context.Context, evt event.Event
|
||||
"total_duration_ms": data.TotalDurationMs,
|
||||
},
|
||||
}); err != nil {
|
||||
logger.GetLogger(h.ctx).Error("Append complete event to stream failed", "error", err)
|
||||
logger.GetLogger(h.ctx).Errorf("Append complete event to stream failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
@@ -103,18 +104,8 @@ func (h *Handler) CreateSession(c *gin.Context) {
|
||||
|
||||
logger.Debug(ctx, "Custom session strategy set")
|
||||
} else {
|
||||
// Use default configuration from global config
|
||||
createdSession.MaxRounds = h.config.Conversation.MaxRounds
|
||||
createdSession.EnableRewrite = h.config.Conversation.EnableRewrite
|
||||
createdSession.FallbackStrategy = types.FallbackStrategy(h.config.Conversation.FallbackStrategy)
|
||||
createdSession.FallbackResponse = h.config.Conversation.FallbackResponse
|
||||
createdSession.EmbeddingTopK = h.config.Conversation.EmbeddingTopK
|
||||
createdSession.KeywordThreshold = h.config.Conversation.KeywordThreshold
|
||||
createdSession.VectorThreshold = h.config.Conversation.VectorThreshold
|
||||
createdSession.RerankThreshold = h.config.Conversation.RerankThreshold
|
||||
createdSession.RerankTopK = h.config.Conversation.RerankTopK
|
||||
createdSession.SummaryParameters = h.createDefaultSummaryConfig(ctx)
|
||||
|
||||
tenantInfo, _ := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
||||
h.applyConversationDefaults(ctx, createdSession, tenantInfo)
|
||||
logger.Debug(ctx, "Using default session strategy")
|
||||
}
|
||||
|
||||
@@ -159,6 +150,45 @@ func (h *Handler) CreateSession(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) applyConversationDefaults(ctx context.Context, session *types.Session, tenant *types.Tenant) {
|
||||
session.MaxRounds = h.config.Conversation.MaxRounds
|
||||
session.EnableRewrite = h.config.Conversation.EnableRewrite
|
||||
session.FallbackStrategy = types.FallbackStrategy(h.config.Conversation.FallbackStrategy)
|
||||
session.FallbackResponse = h.config.Conversation.FallbackResponse
|
||||
session.EmbeddingTopK = h.config.Conversation.EmbeddingTopK
|
||||
session.KeywordThreshold = h.config.Conversation.KeywordThreshold
|
||||
session.VectorThreshold = h.config.Conversation.VectorThreshold
|
||||
session.RerankThreshold = h.config.Conversation.RerankThreshold
|
||||
session.RerankTopK = h.config.Conversation.RerankTopK
|
||||
session.RerankModelID = ""
|
||||
session.SummaryModelID = ""
|
||||
|
||||
if tenant != nil && tenant.ConversationConfig != nil {
|
||||
tc := tenant.ConversationConfig
|
||||
session.MaxRounds = tc.MaxRounds
|
||||
session.EnableRewrite = tc.EnableRewrite
|
||||
if tc.FallbackStrategy != "" {
|
||||
session.FallbackStrategy = types.FallbackStrategy(tc.FallbackStrategy)
|
||||
}
|
||||
if tc.FallbackResponse != "" {
|
||||
session.FallbackResponse = tc.FallbackResponse
|
||||
}
|
||||
session.EmbeddingTopK = tc.EmbeddingTopK
|
||||
session.KeywordThreshold = tc.KeywordThreshold
|
||||
session.VectorThreshold = tc.VectorThreshold
|
||||
session.RerankThreshold = tc.RerankThreshold
|
||||
session.RerankTopK = tc.RerankTopK
|
||||
if tc.RerankModelID != "" {
|
||||
session.RerankModelID = tc.RerankModelID
|
||||
}
|
||||
if tc.SummaryModelID != "" {
|
||||
session.SummaryModelID = tc.SummaryModelID
|
||||
}
|
||||
}
|
||||
|
||||
session.SummaryParameters = h.createDefaultSummaryConfig(ctx)
|
||||
}
|
||||
|
||||
// GetSession retrieves a session by its ID
|
||||
func (h *Handler) GetSession(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
@@ -185,10 +185,20 @@ func (h *Handler) createDefaultSummaryConfig(ctx context.Context) *types.Summary
|
||||
|
||||
// Override with tenant-level conversation config if available
|
||||
if tenant != nil && tenant.ConversationConfig != nil {
|
||||
if tenant.ConversationConfig.Prompt != "" {
|
||||
useSystemPrompt := tenant.ConversationConfig.UseCustomSystemPrompt
|
||||
if !useSystemPrompt && tenant.ConversationConfig.Prompt != "" {
|
||||
// Backward compatibility: treat legacy configs without flag as custom
|
||||
useSystemPrompt = true
|
||||
}
|
||||
if useSystemPrompt && tenant.ConversationConfig.Prompt != "" {
|
||||
cfg.Prompt = tenant.ConversationConfig.Prompt
|
||||
}
|
||||
if tenant.ConversationConfig.ContextTemplate != "" {
|
||||
|
||||
useContextTemplate := tenant.ConversationConfig.UseCustomContextTemplate
|
||||
if !useContextTemplate && tenant.ConversationConfig.ContextTemplate != "" {
|
||||
useContextTemplate = true
|
||||
}
|
||||
if useContextTemplate && tenant.ConversationConfig.ContextTemplate != "" {
|
||||
cfg.ContextTemplate = tenant.ConversationConfig.ContextTemplate
|
||||
}
|
||||
if tenant.ConversationConfig.Temperature > 0 {
|
||||
@@ -214,9 +224,15 @@ func (h *Handler) fillSummaryConfigDefaults(ctx context.Context, config *types.S
|
||||
var defaultMaxTokens int
|
||||
|
||||
if tenant != nil && tenant.ConversationConfig != nil {
|
||||
// Use tenant-level config as defaults
|
||||
defaultPrompt = tenant.ConversationConfig.Prompt
|
||||
defaultContextTemplate = tenant.ConversationConfig.ContextTemplate
|
||||
useSystemPrompt := tenant.ConversationConfig.UseCustomSystemPrompt
|
||||
if useSystemPrompt && tenant.ConversationConfig.Prompt != "" {
|
||||
defaultPrompt = tenant.ConversationConfig.Prompt
|
||||
}
|
||||
|
||||
useContextTemplate := tenant.ConversationConfig.UseCustomContextTemplate
|
||||
if useContextTemplate && tenant.ConversationConfig.ContextTemplate != "" {
|
||||
defaultContextTemplate = tenant.ConversationConfig.ContextTemplate
|
||||
}
|
||||
defaultTemperature = tenant.ConversationConfig.Temperature
|
||||
defaultMaxTokens = tenant.ConversationConfig.MaxTokens
|
||||
}
|
||||
|
||||
@@ -443,6 +443,16 @@ func (h *Handler) handleKnowledgeQARequest(
|
||||
if data.Done {
|
||||
logger.Infof(asyncCtx, "Knowledge QA service completed for session: %s", sessionID)
|
||||
h.completeAssistantMessage(asyncCtx, assistantMessage)
|
||||
// Emit completion event when stream finishes
|
||||
if err := eventBus.Emit(asyncCtx, event.Event{
|
||||
Type: event.EventAgentComplete,
|
||||
SessionID: sessionID,
|
||||
Data: event.AgentCompleteData{
|
||||
FinalAnswer: assistantMessage.Content,
|
||||
},
|
||||
}); err != nil {
|
||||
logger.Errorf(asyncCtx, "Failed to emit completion event: %v", err)
|
||||
}
|
||||
cancel() // Clean up context
|
||||
return nil
|
||||
}
|
||||
|
||||
+141
-29
@@ -510,6 +510,62 @@ func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) buildDefaultConversationConfig() *types.ConversationConfig {
|
||||
return &types.ConversationConfig{
|
||||
Prompt: h.config.Conversation.Summary.Prompt,
|
||||
ContextTemplate: h.config.Conversation.Summary.ContextTemplate,
|
||||
UseCustomContextTemplate: true,
|
||||
UseCustomSystemPrompt: true,
|
||||
Temperature: h.config.Conversation.Summary.Temperature,
|
||||
MaxTokens: h.config.Conversation.Summary.MaxTokens,
|
||||
MaxRounds: h.config.Conversation.MaxRounds,
|
||||
EmbeddingTopK: h.config.Conversation.EmbeddingTopK,
|
||||
KeywordThreshold: h.config.Conversation.KeywordThreshold,
|
||||
VectorThreshold: h.config.Conversation.VectorThreshold,
|
||||
RerankTopK: h.config.Conversation.RerankTopK,
|
||||
RerankThreshold: h.config.Conversation.RerankThreshold,
|
||||
EnableRewrite: h.config.Conversation.EnableRewrite,
|
||||
FallbackStrategy: h.config.Conversation.FallbackStrategy,
|
||||
FallbackResponse: h.config.Conversation.FallbackResponse,
|
||||
FallbackPrompt: h.config.Conversation.FallbackPrompt,
|
||||
RewritePromptUser: h.config.Conversation.RewritePromptUser,
|
||||
RewritePromptSystem: h.config.Conversation.RewritePromptSystem,
|
||||
}
|
||||
}
|
||||
|
||||
func validateConversationConfig(req *types.ConversationConfig) error {
|
||||
if req.MaxRounds <= 0 {
|
||||
return errors.NewBadRequestError("max_rounds must be greater than 0")
|
||||
}
|
||||
if req.EmbeddingTopK <= 0 {
|
||||
return errors.NewBadRequestError("embedding_top_k must be greater than 0")
|
||||
}
|
||||
if req.KeywordThreshold < 0 || req.KeywordThreshold > 1 {
|
||||
return errors.NewBadRequestError("keyword_threshold must be between 0 and 1")
|
||||
}
|
||||
if req.VectorThreshold < 0 || req.VectorThreshold > 1 {
|
||||
return errors.NewBadRequestError("vector_threshold must be between 0 and 1")
|
||||
}
|
||||
if req.RerankTopK <= 0 {
|
||||
return errors.NewBadRequestError("rerank_top_k must be greater than 0")
|
||||
}
|
||||
if req.RerankThreshold < 0 || req.RerankThreshold > 1 {
|
||||
return errors.NewBadRequestError("rerank_threshold must be between 0 and 1")
|
||||
}
|
||||
if req.Temperature < 0 || req.Temperature > 2 {
|
||||
return errors.NewBadRequestError("temperature must be between 0 and 2")
|
||||
}
|
||||
if req.MaxTokens <= 0 || req.MaxTokens > 100000 {
|
||||
return errors.NewBadRequestError("max_tokens must be between 1 and 100000")
|
||||
}
|
||||
if req.FallbackStrategy != "" &&
|
||||
req.FallbackStrategy != string(types.FallbackStrategyFixed) &&
|
||||
req.FallbackStrategy != string(types.FallbackStrategyModel) {
|
||||
return errors.NewBadRequestError("fallback_strategy is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTenantConversationConfig retrieves the conversation configuration for a tenant
|
||||
// This is the global conversation configuration that applies to normal mode sessions by default
|
||||
func (h *TenantHandler) GetTenantConversationConfig(c *gin.Context) {
|
||||
@@ -524,29 +580,94 @@ func (h *TenantHandler) GetTenantConversationConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// If tenant has no conversation config, return defaults from config.yaml
|
||||
var response *types.ConversationConfig
|
||||
if tenant.ConversationConfig == nil {
|
||||
logger.Info(ctx, "Tenant has no conversation config, returning defaults")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"prompt": h.config.Conversation.Summary.Prompt,
|
||||
"context_template": h.config.Conversation.Summary.ContextTemplate,
|
||||
"temperature": h.config.Conversation.Summary.Temperature,
|
||||
"max_tokens": h.config.Conversation.Summary.MaxTokens,
|
||||
},
|
||||
})
|
||||
return
|
||||
response = h.buildDefaultConversationConfig()
|
||||
} else {
|
||||
logger.Infof(ctx, "Tenant has conversation config, merging with defaults, Tenant ID: %d", tenant.ID)
|
||||
// Merge tenant config with defaults, so that newly added fields always have valid values
|
||||
defaultCfg := h.buildDefaultConversationConfig()
|
||||
tc := tenant.ConversationConfig
|
||||
|
||||
// Prompt related
|
||||
defaultCfg.UseCustomSystemPrompt = tc.UseCustomSystemPrompt
|
||||
if !defaultCfg.UseCustomSystemPrompt && tc.Prompt != "" {
|
||||
// Legacy configs without explicit flag
|
||||
defaultCfg.UseCustomSystemPrompt = true
|
||||
}
|
||||
defaultCfg.UseCustomContextTemplate = tc.UseCustomContextTemplate
|
||||
if !defaultCfg.UseCustomContextTemplate && tc.ContextTemplate != "" {
|
||||
defaultCfg.UseCustomContextTemplate = true
|
||||
}
|
||||
if tc.Prompt != "" {
|
||||
defaultCfg.Prompt = tc.Prompt
|
||||
}
|
||||
if tc.ContextTemplate != "" {
|
||||
defaultCfg.ContextTemplate = tc.ContextTemplate
|
||||
}
|
||||
if tc.Temperature > 0 {
|
||||
defaultCfg.Temperature = tc.Temperature
|
||||
}
|
||||
if tc.MaxTokens > 0 {
|
||||
defaultCfg.MaxTokens = tc.MaxTokens
|
||||
}
|
||||
|
||||
// Retrieval parameters
|
||||
if tc.MaxRounds > 0 {
|
||||
defaultCfg.MaxRounds = tc.MaxRounds
|
||||
}
|
||||
if tc.EmbeddingTopK > 0 {
|
||||
defaultCfg.EmbeddingTopK = tc.EmbeddingTopK
|
||||
}
|
||||
if tc.KeywordThreshold > 0 {
|
||||
defaultCfg.KeywordThreshold = tc.KeywordThreshold
|
||||
}
|
||||
if tc.VectorThreshold > 0 {
|
||||
defaultCfg.VectorThreshold = tc.VectorThreshold
|
||||
}
|
||||
if tc.RerankTopK > 0 {
|
||||
defaultCfg.RerankTopK = tc.RerankTopK
|
||||
}
|
||||
if tc.RerankThreshold > 0 {
|
||||
defaultCfg.RerankThreshold = tc.RerankThreshold
|
||||
}
|
||||
// EnableRewrite 需要允许显式关闭,因此直接覆盖
|
||||
defaultCfg.EnableRewrite = tc.EnableRewrite
|
||||
|
||||
// Model IDs
|
||||
if tc.SummaryModelID != "" {
|
||||
defaultCfg.SummaryModelID = tc.SummaryModelID
|
||||
}
|
||||
if tc.RerankModelID != "" {
|
||||
defaultCfg.RerankModelID = tc.RerankModelID
|
||||
}
|
||||
|
||||
// Fallback settings
|
||||
if tc.FallbackStrategy != "" {
|
||||
defaultCfg.FallbackStrategy = tc.FallbackStrategy
|
||||
}
|
||||
if tc.FallbackResponse != "" {
|
||||
defaultCfg.FallbackResponse = tc.FallbackResponse
|
||||
}
|
||||
if tc.FallbackPrompt != "" {
|
||||
defaultCfg.FallbackPrompt = tc.FallbackPrompt
|
||||
}
|
||||
|
||||
// Rewrite prompts
|
||||
if tc.RewritePromptSystem != "" {
|
||||
defaultCfg.RewritePromptSystem = tc.RewritePromptSystem
|
||||
}
|
||||
if tc.RewritePromptUser != "" {
|
||||
defaultCfg.RewritePromptUser = tc.RewritePromptUser
|
||||
}
|
||||
|
||||
response = defaultCfg
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieved tenant conversation config successfully, Tenant ID: %d", tenant.ID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"prompt": tenant.ConversationConfig.Prompt,
|
||||
"context_template": tenant.ConversationConfig.ContextTemplate,
|
||||
"temperature": tenant.ConversationConfig.Temperature,
|
||||
"max_tokens": tenant.ConversationConfig.MaxTokens,
|
||||
},
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -564,12 +685,8 @@ func (h *TenantHandler) updateTenantConversationConfigInternal(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if req.Temperature < 0 || req.Temperature > 2 {
|
||||
c.Error(errors.NewBadRequestError("temperature must be between 0 and 2"))
|
||||
return
|
||||
}
|
||||
if req.MaxTokens <= 0 || req.MaxTokens > 100000 {
|
||||
c.Error(errors.NewBadRequestError("max_tokens must be between 1 and 100000"))
|
||||
if err := validateConversationConfig(&req); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -599,12 +716,7 @@ func (h *TenantHandler) updateTenantConversationConfigInternal(c *gin.Context) {
|
||||
logger.Infof(ctx, "Tenant conversation config updated successfully, Tenant ID: %d", tenant.ID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"prompt": updatedTenant.ConversationConfig.Prompt,
|
||||
"context_template": updatedTenant.ConversationConfig.ContextTemplate,
|
||||
"temperature": updatedTenant.ConversationConfig.Temperature,
|
||||
"max_tokens": updatedTenant.ConversationConfig.MaxTokens,
|
||||
},
|
||||
"data": updatedTenant.ConversationConfig,
|
||||
"message": "Conversation configuration updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ type ChatManage struct {
|
||||
SessionID string `json:"session_id"` // Unique identifier for the chat session
|
||||
Query string `json:"query,omitempty"` // Original user query
|
||||
ProcessedQuery string `json:"processed_query,omitempty"` // Query after preprocessing
|
||||
RewriteQuery string `json:"rewrite_query,omitempty"` // Query after rewriting for better retrieval
|
||||
RewriteQuery string `json:"rewrite_query,omitempty"` // Query after rewriting for better retrieval
|
||||
QueryIntent string `json:"query_intent,omitempty"` // Parsed intent: definition/howto/compare/qa/general
|
||||
History []*History `json:"history,omitempty"` // Chat history for context
|
||||
|
||||
KnowledgeBaseID string `json:"knowledge_base_id"` // ID of the knowledge base to search against (deprecated, use KnowledgeBaseIDs)
|
||||
@@ -20,11 +21,17 @@ type ChatManage struct {
|
||||
RerankTopK int `json:"rerank_top_k"` // Number of top results after reranking
|
||||
RerankThreshold float64 `json:"rerank_threshold"` // Minimum score threshold for reranked results
|
||||
|
||||
MaxRounds int `json:"max_rounds"` // Maximum history rounds used for rewrite/context
|
||||
|
||||
ChatModelID string `json:"chat_model_id"` // ID of the chat model to use
|
||||
SummaryConfig SummaryConfig `json:"summary_config"` // Configuration for summary generation
|
||||
FallbackStrategy FallbackStrategy `json:"fallback_strategy"` // Strategy when no relevant results are found
|
||||
FallbackResponse string `json:"fallback_response"` // Default response when fallback occurs
|
||||
|
||||
EnableRewrite bool `json:"enable_rewrite"` // Whether to enable rewrite
|
||||
RewritePromptSystem string `json:"rewrite_prompt_system"` // Custom system prompt for rewrite stage
|
||||
RewritePromptUser string `json:"rewrite_prompt_user"` // Custom user prompt for rewrite stage
|
||||
|
||||
// Internal fields for pipeline data processing
|
||||
SearchResult []*SearchResult `json:"-"` // Results from search phase
|
||||
RerankResult []*SearchResult `json:"-"` // Results after reranking
|
||||
@@ -52,13 +59,15 @@ func (c *ChatManage) Clone() *ChatManage {
|
||||
return &ChatManage{
|
||||
Query: c.Query,
|
||||
ProcessedQuery: c.ProcessedQuery,
|
||||
RewriteQuery: c.RewriteQuery,
|
||||
RewriteQuery: c.RewriteQuery,
|
||||
QueryIntent: c.QueryIntent,
|
||||
SessionID: c.SessionID,
|
||||
KnowledgeBaseID: c.KnowledgeBaseID,
|
||||
KnowledgeBaseIDs: knowledgeBaseIDs,
|
||||
VectorThreshold: c.VectorThreshold,
|
||||
KeywordThreshold: c.KeywordThreshold,
|
||||
EmbeddingTopK: c.EmbeddingTopK,
|
||||
MaxRounds: c.MaxRounds,
|
||||
VectorDatabase: c.VectorDatabase,
|
||||
RerankModelID: c.RerankModelID,
|
||||
RerankTopK: c.RerankTopK,
|
||||
@@ -78,8 +87,11 @@ func (c *ChatManage) Clone() *ChatManage {
|
||||
Seed: c.SummaryConfig.Seed,
|
||||
MaxCompletionTokens: c.SummaryConfig.MaxCompletionTokens,
|
||||
},
|
||||
FallbackStrategy: c.FallbackStrategy,
|
||||
FallbackResponse: c.FallbackResponse,
|
||||
FallbackStrategy: c.FallbackStrategy,
|
||||
FallbackResponse: c.FallbackResponse,
|
||||
RewritePromptSystem: c.RewritePromptSystem,
|
||||
RewritePromptUser: c.RewritePromptUser,
|
||||
EnableRewrite: c.EnableRewrite,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,14 +74,38 @@ func (c *RetrieverEngines) Scan(value interface{}) error {
|
||||
|
||||
// ConversationConfig represents the conversation configuration for normal mode
|
||||
type ConversationConfig struct {
|
||||
// Prompt is the system prompt
|
||||
Prompt string `json:"prompt"`
|
||||
// Prompt is the system prompt for normal mode
|
||||
UseCustomSystemPrompt bool `json:"use_custom_system_prompt"`
|
||||
Prompt string `json:"prompt"`
|
||||
UseCustomContextTemplate bool `json:"use_custom_context_template"`
|
||||
// ContextTemplate is the prompt template for summarizing retrieval results
|
||||
ContextTemplate string `json:"context_template"`
|
||||
// Temperature controls the randomness of the model output
|
||||
Temperature float64 `json:"temperature"`
|
||||
// MaxTokens is the maximum number of tokens to generate
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
|
||||
// Retrieval & strategy parameters
|
||||
MaxRounds int `json:"max_rounds"`
|
||||
EmbeddingTopK int `json:"embedding_top_k"`
|
||||
KeywordThreshold float64 `json:"keyword_threshold"`
|
||||
VectorThreshold float64 `json:"vector_threshold"`
|
||||
RerankTopK int `json:"rerank_top_k"`
|
||||
RerankThreshold float64 `json:"rerank_threshold"`
|
||||
EnableRewrite bool `json:"enable_rewrite"`
|
||||
|
||||
// Model configuration
|
||||
SummaryModelID string `json:"summary_model_id"`
|
||||
RerankModelID string `json:"rerank_model_id"`
|
||||
|
||||
// Fallback strategy
|
||||
FallbackStrategy string `json:"fallback_strategy"`
|
||||
FallbackResponse string `json:"fallback_response"`
|
||||
FallbackPrompt string `json:"fallback_prompt"`
|
||||
|
||||
// Rewrite prompts
|
||||
RewritePromptSystem string `json:"rewrite_prompt_system"`
|
||||
RewritePromptUser string `json:"rewrite_prompt_user"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface, used to convert ConversationConfig to database value
|
||||
|
||||
Reference in New Issue
Block a user