This commit is contained in:
renzhiyuan 2025-08-27 15:47:08 +08:00
commit f54eb39a45
295 changed files with 57340 additions and 0 deletions

0
.dockerignore Normal file
View File

189
.env.example Normal file
View File

@ -0,0 +1,189 @@
# 使用说明
# 1. 复制此文件为 .env
# 2. 替换所有占位符为实际值
# 3. 确保 .env 文件不会被提交到版本控制系统
# gin mod
# 可选值: debug(开发模式,有详细日志), release(生产模式)
GIN_MODE=debug
# Ollama 服务的基准 URL用于连接本地/其他服务器上运行的 Ollama 服务
OLLAMA_BASE_URL=http://host.docker.internal:11434
# 存储配置
# 主数据库类型(postgres/mysql)
DB_DRIVER=postgres
# 向量存储类型(postgres/elasticsearch_v7/elasticsearch_v8)
RETRIEVE_DRIVER=postgres
# 文件存储类型(local/minio/cos)
STORAGE_TYPE=local
# 流处理后端(memory/redis)
STREAM_MANAGER_TYPE=redis
# 主数据库配置
# 数据库端口默认为5432
DB_PORT=5432
# 数据库用户名
DB_USER=postgres
# 数据库密码
DB_PASSWORD=postgres123!@#
# 数据库名称
DB_NAME=WeKnora
# 如果使用 redis 作为流处理后端,需要配置以下参数
# Redis端口默认为6379
REDIS_PORT=6379
# Redis密码如果没有设置密码可以留空
REDIS_PASSWORD=redis123!@#
# Redis数据库索引默认为0
REDIS_DB=0
# Redis key的前缀用于命名空间隔离
REDIS_PREFIX=stream:
# 当使用本地存储时,文件保存的基础目录路径
LOCAL_STORAGE_BASE_DIR=./data/files
TENANT_AES_KEY=weknorarag-api-key-secret-secret
# 是否开启知识图谱构建和检索(构建阶段需调用大模型,耗时较长)
ENABLE_GRAPH_RAG=false
MINIO_PORT=9000
MINIO_CONSOLE_PORT=9001
# Embedding并发数出现429错误时可调小此参数
CONCURRENCY_POOL_SIZE=5
# 如果使用ElasticSearch作为向量存储需要配置以下参数
# ElasticSearch地址例如 http://localhost:9200
# ELASTICSEARCH_ADDR=your_elasticsearch_addr
# ElasticSearch用户名如果需要身份验证
# ELASTICSEARCH_USERNAME=your_elasticsearch_username
# ElasticSearch密码如果需要身份验证
# ELASTICSEARCH_PASSWORD=your_elasticsearch_password
# ElasticSearch索引名称用于存储向量数据
# ELASTICSEARCH_INDEX=WeKnora
# 如果使用MinIO作为文件存储需要配置以下参数
# MinIO访问密钥
# MINIO_ACCESS_KEY_ID=your_minio_access_key
# MinIO密钥
# MINIO_SECRET_ACCESS_KEY=your_minio_secret_key
# MinIO桶名称用于存储文件
# MINIO_BUCKET_NAME=your_minio_bucket_name
# 如果使用腾讯云COS作为文件存储需要配置以下参数
# 腾讯云COS的访问密钥ID
# COS_SECRET_ID=your_cos_secret_id
# 腾讯云COS的密钥
# COS_SECRET_KEY=your_cos_secret_key
# 腾讯云COS的区域例如 ap-guangzhou
# COS_REGION=your_cos_region
# 腾讯云COS的桶名称
# COS_BUCKET_NAME=your_cos_bucket_name
# 腾讯云COS的应用ID
# COS_APP_ID=your_cos_app_id
# 腾讯云COS的路径前缀用于存储文件
# COS_PATH_PREFIX=your_cos_path_prefix
# COS_ENABLE_OLD_DOMAIN=true 表示启用旧的域名格式,默认为 true
COS_ENABLE_OLD_DOMAIN=true
# 如果解析网络连接使用Web代理需要配置以下参数
# WEB_PROXY=your_web_proxy
##############################################################
###### 注意: 以下配置不再生效已在Web“配置初始化”阶段完成 #########
# # 初始化默认租户与知识库
# # 租户ID通常是一个字符串
# INIT_TEST_TENANT_ID=1
# # 知识库ID通常是一个字符串
# INIT_TEST_KNOWLEDGE_BASE_ID=kb-00000001
# # LLM Model
# # 使用的LLM模型名称
# # 默认使用 Ollama 的 Qwen3 8B 模型ollama 会自动处理模型下载和加载
# # 如果需要使用其他模型,请替换为实际的模型名称
# INIT_LLM_MODEL_NAME=qwen3:8b
# # LLM模型的访问地址
# # 支持第三方模型服务的URL
# # 如果使用 Ollama 的本地服务可以留空ollama 会自动处理
# # INIT_LLM_MODEL_BASE_URL=your_llm_model_base_url
# # LLM模型的API密钥如果需要身份验证可以设置
# # 支持第三方模型服务的API密钥
# # 如果使用 Ollama 的本地服务可以留空ollama 会自动处理
# # INIT_LLM_MODEL_API_KEY=your_llm_model_api_key
# # Embedding Model
# # 使用的Embedding模型名称
# # 默认使用 nomic-embed-text 模型,支持文本嵌入
# # 如果需要使用其他模型,请替换为实际的模型名称
# INIT_EMBEDDING_MODEL_NAME=nomic-embed-text
# # Embedding模型向量维度
# INIT_EMBEDDING_MODEL_DIMENSION=768
# # Embedding模型的ID通常是一个字符串
# INIT_EMBEDDING_MODEL_ID=builtin:nomic-embed-text:768
# # Embedding模型的访问地址
# # 支持第三方模型服务的URL
# # 如果使用 Ollama 的本地服务可以留空ollama 会自动处理
# # INIT_EMBEDDING_MODEL_BASE_URL=your_embedding_model_base_url
# # Embedding模型的API密钥如果需要身份验证可以设置
# # 支持第三方模型服务的API密钥
# # 如果使用 Ollama 的本地服务可以留空ollama 会自动处理
# # INIT_EMBEDDING_MODEL_API_KEY=your_embedding_model_api_key
# # Rerank Model(可选)
# # 对于rag来说使用Rerank模型对提升文档搜索的准确度有着重要作用
# # 目前 ollama 暂不支持运行 Rerank 模型
# # 使用的Rerank模型名称
# # INIT_RERANK_MODEL_NAME=your_rerank_model_name
# # Rerank模型的访问地址
# # 支持第三方模型服务的URL
# # INIT_RERANK_MODEL_BASE_URL=your_rerank_model_base_url
# # Rerank模型的API密钥如果需要身份验证可以设置
# # 支持第三方模型服务的API密钥
# # INIT_RERANK_MODEL_API_KEY=your_rerank_model_api_key
# # VLM_MODEL_NAME 使用的多模态模型名称
# # 用于解析图片数据
# # VLM_MODEL_NAME=your_vlm_model_name
# # VLM_MODEL_BASE_URL 使用的多模态模型访问地址
# # 支持第三方模型服务的URL
# # VLM_MODEL_BASE_URL=your_vlm_model_base_url
# # VLM_MODEL_API_KEY 使用的多模态模型API密钥
# # 支持第三方模型服务的API密钥
# # VLM_MODEL_API_KEY=your_vlm_model_api_key

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sh text eol=lf

48
.github/workflows/docker-image.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
build-app:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- service_name: ui
file: frontend/Dockerfile
context: ./frontend
platform: linux/amd64,linux/arm64
- service_name: app
file: docker/Dockerfile.app
context: .
platform: linux/amd64,linux/arm64
- service_name: docreader
file: docker/Dockerfile.docreader
context: .
platform: linux/amd64,linux/arm64
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Build ${{ matrix.service_name }} Image
uses: docker/build-push-action@v3
with:
push: true
platforms: ${{ matrix.platform }}
file: ${{ matrix.file }}
context: ${{ matrix.context }}
tags: ${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:latest

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# 忽略.env文件和其他包含敏感信息的配置文件
.env
# 但不忽略示例文件
!.env.example
*.pem
*_key
*_secret
*.key
*.crt
# IDE和编辑器文件
.idea/
.vscode/
*.swp
*.swo
# 构建和依赖文件
node_modules/
/dist/
/build/
*.log
# 临时文件
tmp/
temp/
WeKnora
/models/
services/docreader/src/proto/__pycache__
test/data/mswag.txt
data/files/
.python-version
.venv/
### macOS
# General
.DS_Store

2692
LICENSE Normal file

File diff suppressed because it is too large Load Diff

154
Makefile Normal file
View File

@ -0,0 +1,154 @@
.PHONY: help build run test clean docker-build docker-run migrate-up migrate-down docker-restart docker-stop start-all stop-all start-ollama stop-ollama build-images build-images-app build-images-docreader build-images-frontend clean-images
# Show help
help:
@echo "WeKnora Makefile 帮助"
@echo ""
@echo "基础命令:"
@echo " build 构建应用"
@echo " run 运行应用"
@echo " test 运行测试"
@echo " clean 清理构建文件"
@echo ""
@echo "Docker 命令:"
@echo " docker-build 构建 Docker 镜像"
@echo " docker-run 运行 Docker 容器"
@echo " docker-stop 停止 Docker 容器"
@echo " docker-restart 重启 Docker 容器"
@echo ""
@echo "服务管理:"
@echo " start-all 启动所有服务"
@echo " stop-all 停止所有服务"
@echo " start-ollama 仅启动 Ollama 服务"
@echo ""
@echo "镜像构建:"
@echo " build-images 从源码构建所有镜像"
@echo " build-images-app 从源码构建应用镜像"
@echo " build-images-docreader 从源码构建文档读取器镜像"
@echo " build-images-frontend 从源码构建前端镜像"
@echo " clean-images 清理本地镜像"
@echo ""
@echo "数据库:"
@echo " migrate-up 执行数据库迁移"
@echo " migrate-down 回滚数据库迁移"
@echo ""
@echo "开发工具:"
@echo " fmt 格式化代码"
@echo " lint 代码检查"
@echo " deps 安装依赖"
@echo " docs 生成 API 文档"
# Go related variables
BINARY_NAME=WeKnora
MAIN_PATH=./cmd/server
# Docker related variables
DOCKER_IMAGE=WeKnora
DOCKER_TAG=latest
# Build the application
build:
go build -o $(BINARY_NAME) $(MAIN_PATH)
# Run the application
run: build
./$(BINARY_NAME)
# Run tests
test:
go test -v ./...
# Clean build artifacts
clean:
go clean
rm -f $(BINARY_NAME)
# Build Docker image
docker-build:
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
# Run Docker container (传统方式)
docker-run:
docker-compose up
# 使用新脚本启动所有服务
start-all:
./scripts/start_all.sh
# 使用新脚本仅启动Ollama服务
start-ollama:
./scripts/start_all.sh --ollama
# 使用新脚本仅启动Docker容器
start-docker:
./scripts/start_all.sh --docker
# 使用新脚本停止所有服务
stop-all:
./scripts/start_all.sh --stop
# Stop Docker container (传统方式)
docker-stop:
docker-compose down
# 从源码构建镜像相关命令
build-images:
./scripts/build_images.sh
build-images-app:
./scripts/build_images.sh --app
build-images-docreader:
./scripts/build_images.sh --docreader
build-images-frontend:
./scripts/build_images.sh --frontend
clean-images:
./scripts/build_images.sh --clean
# Restart Docker container (stop, rebuild, start)
docker-restart:
docker-compose stop -t 60
docker-compose up --build
# Database migrations
migrate-up:
./scripts/migrate.sh up
migrate-down:
./scripts/migrate.sh down
# Generate API documentation
docs:
swag init -g $(MAIN_PATH)/main.go -o ./docs
# Format code
fmt:
go fmt ./...
# Lint code
lint:
golangci-lint run
# Install dependencies
deps:
go mod download
# Build for production
build-prod:
GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o $(BINARY_NAME) $(MAIN_PATH)
clean-db:
@echo "Cleaning database..."
@if [ $$(docker volume ls -q -f name=weknora_postgres-data) ]; then \
docker volume rm weknora_postgres-data; \
fi
@if [ $$(docker volume ls -q -f name=weknora_minio_data) ]; then \
docker volume rm weknora_minio_data; \
fi
@if [ $$(docker volume ls -q -f name=weknora_redis_data) ]; then \
docker volume rm weknora_redis_data; \
fi

283
README.md Normal file
View File

@ -0,0 +1,283 @@
<p align="center">
<picture>
<img src="./docs/images/logo.png" alt="WeKnora Logo" height="120"/>
</picture>
</p>
<p align="center">
<a href="https://weknora.weixin.qq.com" target="_blank">
<img alt="官方网站" src="https://img.shields.io/badge/官方网站-WeKnora-4e6b99">
</a>
<a href="https://chatbot.weixin.qq.com" target="_blank">
<img alt="微信对话开放平台" src="https://img.shields.io/badge/微信对话开放平台-5ac725">
</a>
<a href="https://github.com/Tencent/WeKnora/blob/main/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="License">
</a>
</p>
<p align="center">
| <a href="./README_EN.md"><b>English</b></a> | <b>简体中文</b> |
</p>
<p align="center">
<h4 align="center">
[项目介绍](#-项目介绍) • [架构设计](#-架构设计) • [核心特性](#-核心特性) • [快速开始](#-快速开始) • [文档](#-文档) • [开发指南](#-开发指南)
</h4>
</p>
# 💡 WeKnora - 基于大模型的文档理解检索框架
## 📌 项目介绍
[**WeKnora维娜拉**](https://weknora.weixin.qq.com) 是一款基于大语言模型LLM的文档理解与语义检索框架专为结构复杂、内容异构的文档场景而打造。
框架采用模块化架构,融合多模态预处理、语义向量索引、智能召回与大模型生成推理,构建起高效、可控的文档问答流程。核心检索流程基于 **RAGRetrieval-Augmented Generation** 机制,将上下文相关片段与语言模型结合,实现更高质量的语义回答。
**官网:** https://weknora.weixin.qq.com
## 🏗️ 架构设计
![weknora-pipelone.png](./docs/images/pipeline.jpg)
WeKnora 采用现代化模块化设计,构建了一条完整的文档理解与检索流水线。系统主要包括文档解析、向量化处理、检索引擎和大模型推理等核心模块,每个组件均可灵活配置与扩展。
## 🎯 核心特性
- **🔍 精准理解**:支持 PDF、Word、图片等文档的结构化内容提取统一构建语义视图
- **🧠 智能推理**:借助大语言模型理解文档上下文与用户意图,支持精准问答与多轮对话
- **🔧 灵活扩展**:从解析、嵌入、召回到生成全流程解耦,便于灵活集成与定制扩展
- **⚡ 高效检索**:混合多种检索策略:关键词、向量、知识图谱
- **🎯 简单易用**直观的Web界面与标准API零技术门槛快速上手
- **🔒 安全可控**:支持本地化与私有云部署,数据完全自主可控
## 📊 适用场景
| 应用场景 | 具体应用 | 核心价值 |
|---------|----------|----------|
| **企业知识管理** | 内部文档检索、规章制度问答、操作手册查询 | 提升知识查找效率,降低培训成本 |
| **科研文献分析** | 论文检索、研究报告分析、学术资料整理 | 加速文献调研,辅助研究决策 |
| **产品技术支持** | 产品手册问答、技术文档检索、故障排查 | 提升客户服务质量,减少技术支持负担 |
| **法律合规审查** | 合同条款检索、法规政策查询、案例分析 | 提高合规效率,降低法律风险 |
| **医疗知识辅助** | 医学文献检索、诊疗指南查询、病例分析 | 辅助临床决策,提升诊疗质量 |
## 🧩 功能模块能力
| 功能模块 | 支持情况 | 说明 |
|---------|---------|------|
| 文档格式支持 | ✅ PDF / Word / Txt / Markdown / 图片(含 OCR / Caption | 支持多种结构化与非结构化文档内容解析,支持图文混排与图像文字提取 |
| 嵌入模型支持 | ✅ 本地模型、BGE / GTE API 等 | 支持自定义 embedding 模型,兼容本地部署与云端向量生成接口 |
| 向量数据库接入 | ✅ PostgreSQLpgvector、Elasticsearch | 支持主流向量索引后端,可灵活切换与扩展,适配不同检索场景 |
| 检索机制 | ✅ BM25 / Dense Retrieve / GraphRAG | 支持稠密/稀疏召回、知识图谱增强检索等多种策略,可自由组合召回-重排-生成流程 |
| 大模型集成 | ✅ 支持 Qwen、DeepSeek 等,思考/非思考模式切换 | 可接入本地大模型(如 Ollama 启动)或调用外部 API 服务,支持推理模式灵活配置 |
| 问答能力 | ✅ 上下文感知、多轮对话、提示词模板 | 支持复杂语义建模、指令控制与链式问答,可配置提示词与上下文窗口 |
| 端到端测试支持 | ✅ 检索+生成过程可视化与指标评估 | 提供一体化链路测试工具支持评估召回命中率、回答覆盖度、BLEU / ROUGE 等主流指标 |
| 部署模式 | ✅ 支持本地部署 / Docker 镜像 | 满足私有化、离线部署与灵活运维的需求 |
| 用户界面 | ✅ Web UI + RESTful API | 提供交互式界面与标准 API 接口,适配开发者与业务用户使用习惯 |
## 🚀 快速开始
### 🛠 环境要求
确保本地已安装以下工具:
* [Docker](https://www.docker.com/)
* [Docker Compose](https://docs.docker.com/compose/)
* [Git](https://git-scm.com/)
### 📦 安装步骤
#### ① 克隆代码仓库
```bash
# 克隆主仓库
git clone https://github.com/Tencent/WeKnora.git
cd WeKnora
```
#### ② 配置环境变量
```bash
# 复制示例配置文件
cp .env.example .env
# 编辑 .env填入对应配置信息
# 所有变量说明详见 .env.example 注释
```
#### ③ 启动服务
```bash
# 启动全部服务(含 Ollama 与后端容器)
./scripts/start_all.sh
# 或
make start-all
```
#### ③ 启动服务备选
```bash
# 启动 ollama 服务 (可选)
ollama serve > /dev/null 2>&1 &
# 启动服务
docker compose up -d
```
#### ④ 停止服务
```bash
./scripts/start_all.sh --stop
# 或
make stop-all
```
### 🌐 服务访问地址
启动成功后,可访问以下地址:
* Web UI`http://localhost`
* 后端 API`http://localhost:8080`
* 链路追踪Jaeger`http://localhost:16686`
### 🔌 使用微信对话开放平台
WeKnora 作为[微信对话开放平台](https://chatbot.weixin.qq.com)的核心技术框架,提供更简便的使用方式:
- **零代码部署**:只需上传知识,即可在微信生态中快速部署智能问答服务,实现"即问即答"的体验
- **高效问题管理**:支持高频问题的独立分类管理,提供丰富的数据工具,确保回答精准可靠且易于维护
- **微信生态覆盖**通过微信对话开放平台WeKnora 的智能问答能力可无缝集成到公众号、小程序等微信场景中,提升用户交互体验
## 🔧 初始化配置引导
为了方便用户快速配置各类模型降低试错成本我们改进了原来的配置文件初始化方式增加了Web UI界面进行各种模型的配置。在使用之前请确保代码更新到最新版本。具体使用步骤如下
如果是第一次使用本项目,可跳过①②步骤,直接进入③④步骤。
### ① 关闭服务
```bash
./scripts/start_all.sh --stop
```
### ② 清空原有数据表(建议在没有重要数据的情况下使用)
```bash
make clean-db
```
### ③ 编译并启动服务
```bash
./scripts/start_all.sh
```
### ④ 访问Web UI
http://localhost
首次访问会自动跳转到初始化配置页面,配置完成后会自动跳转到知识库页面。请按照页面提示信息完成模型的配置。
![配置页面](./docs/images/config.png)
## 📱 功能展示
### Web UI 界面
<table>
<tr>
<td><b>知识上传</b><br/><img src="./docs/images/knowledges.png" alt="知识上传界面"></td>
<td><b>知识问答入口</b><br/><img src="./docs/images/qa.png" alt="知识问答入口"></td>
</tr>
<tr>
<td colspan="2"><b>图文结果回答</b><br/><img src="./docs/images/answer.png" alt="图文结果回答"></td>
</tr>
</table>
**知识库管理:** 支持拖拽上传各类文档,自动识别文档结构并提取核心知识,建立索引。系统清晰展示处理进度和文档状态,实现高效的知识库管理。
### 文档知识图谱
<table>
<tr>
<td><img src="./docs/images/graph2.png" alt="知识图谱展示1"></td>
<td><img src="./docs/images/graph1.png" alt="知识图谱展示2"></td>
</tr>
</table>
WeKnora 支持将文档转化为知识图谱,展示文档中不同段落之间的关联关系。开启知识图谱功能后,系统会分析并构建文档内部的语义关联网络,不仅帮助用户理解文档内容,还为索引和检索提供结构化支撑,提升检索结果的相关性和广度。
## 📘 文档
常见问题排查:[常见问题排查](./docs/QA.md)
详细接口说明请参考:[API 文档](./docs/API.md)
## 🧭 开发指南
### 📁 项目目录结构
```
WeKnora/
├── cmd/ # 应用入口
├── internal/ # 核心业务逻辑
├── config/ # 配置文件
├── migrations/ # 数据库迁移脚本
├── scripts/ # 启动与工具脚本
├── services/ # 各子服务实现
├── frontend/ # 前端项目
└── docs/ # 项目文档
```
### 🔧 常用命令
```bash
# 清空数据库(慎用!)
make clean-db
```
## 🤝 贡献指南
我们欢迎社区用户参与贡献如有建议、Bug 或新功能需求,请通过 [Issue](https://github.com/Tencent/WeKnora/issues) 提出,或直接提交 Pull Request。
### 🎯 贡献方式
- 🐛 **Bug修复**: 发现并修复系统缺陷
- ✨ **新功能**: 提出并实现新特性
- 📚 **文档改进**: 完善项目文档
- 🧪 **测试用例**: 编写单元测试和集成测试
- 🎨 **UI/UX优化**: 改进用户界面和体验
### 📋 贡献流程
1. **Fork项目** 到你的GitHub账户
2. **创建特性分支** `git checkout -b feature/amazing-feature`
3. **提交更改** `git commit -m 'Add amazing feature'`
4. **推送分支** `git push origin feature/amazing-feature`
5. **创建Pull Request** 并详细描述变更内容
### 🎨 代码规范
- 遵循 [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- 使用 `gofmt` 格式化代码
- 添加必要的单元测试
- 更新相关文档
### 📝 提交规范
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
```
feat: 添加文档批量上传功能
fix: 修复向量检索精度问题
docs: 更新API文档
test: 添加检索引擎测试用例
refactor: 重构文档解析模块
```
## 📄 许可证
本项目基于 [MIT](./LICENSE) 协议发布。
你可以自由使用、修改和分发本项目代码,但需保留原始版权声明。

249
README_EN.md Normal file
View File

@ -0,0 +1,249 @@
<p align="center">
<picture>
<img src="./docs/images/logo.png" alt="WeKnora Logo" height="120"/>
</picture>
</p>
<p align="center">
<a href="https://weknora.weixin.qq.com" target="_blank">
<img alt="官方网站" src="https://img.shields.io/badge/官方网站-WeKnora-4e6b99">
</a>
<a href="https://chatbot.weixin.qq.com" target="_blank">
<img alt="微信对话开放平台" src="https://img.shields.io/badge/微信对话开放平台-5ac725">
</a>
<a href="https://github.com/Tencent/WeKnora/blob/main/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="License">
</a>
</p>
<p align="center">
| <b>English</b> | <a href="./README.md"><b>简体中文</b></a> |
</p>
<p align="center">
<h4 align="center">
[Overview](#-overview) • [Architecture](#-architecture) • [Key Features](#-key-features) • [Getting Started](#-getting-started) • [API Reference](#-api-reference) • [Developer Guide](#-developer-guide)
</h4>
</p>
# 💡 WeKnora - LLM-Powered Document Understanding & Retrieval Framework
## 📌 Overview
[**WeKnora**](https://weknora.weixin.qq.com) is an LLM-powered framework designed for deep document understanding and semantic retrieval, especially for handling complex, heterogeneous documents.
It adopts a modular architecture that combines multimodal preprocessing, semantic vector indexing, intelligent retrieval, and large language model inference. At its core, WeKnora follows the **RAG (Retrieval-Augmented Generation)** paradigm, enabling high-quality, context-aware answers by combining relevant document chunks with model reasoning.
**Website:** https://weknora.weixin.qq.com
## 🏗️ Architecture
![weknora-pipeline.png](./docs/images/pipeline.jpg)
WeKnora employs a modern modular design to build a complete document understanding and retrieval pipeline. The system primarily includes document parsing, vector processing, retrieval engine, and large model inference as core modules, with each component being flexibly configurable and extendable.
## 🎯 Key Features
- **🔍 Precise Understanding**: Structured content extraction from PDFs, Word documents, images and more into unified semantic views
- **🧠 Intelligent Reasoning**: Leverages LLMs to understand document context and user intent for accurate Q&A and multi-turn conversations
- **🔧 Flexible Extension**: All components from parsing and embedding to retrieval and generation are decoupled for easy customization
- **⚡ Efficient Retrieval**: Hybrid retrieval strategies combining keywords, vectors, and knowledge graphs
- **🎯 User-Friendly**: Intuitive web interface and standardized APIs for zero technical barriers
- **🔒 Secure & Controlled**: Support for local deployment and private cloud, ensuring complete data sovereignty
## 📊 Application Scenarios
| Scenario | Applications | Core Value |
|---------|----------|----------|
| **Enterprise Knowledge Management** | Internal document retrieval, policy Q&A, operation manual search | Improve knowledge discovery efficiency, reduce training costs |
| **Academic Research Analysis** | Paper retrieval, research report analysis, scholarly material organization | Accelerate literature review, assist research decisions |
| **Product Technical Support** | Product manual Q&A, technical documentation search, troubleshooting | Enhance customer service quality, reduce support burden |
| **Legal & Compliance Review** | Contract clause retrieval, regulatory policy search, case analysis | Improve compliance efficiency, reduce legal risks |
| **Medical Knowledge Assistance** | Medical literature retrieval, treatment guideline search, case analysis | Support clinical decisions, improve diagnosis quality |
## 🧩 Feature Matrix
| Module | Support | Description |
|---------|---------|------|
| Document Formats | ✅ PDF / Word / Txt / Markdown / Images (with OCR / Caption) | Support for structured and unstructured documents with text extraction from images |
| Embedding Models | ✅ Local models, BGE / GTE APIs, etc. | Customizable embedding models, compatible with local deployment and cloud vector generation APIs |
| Vector DB Integration | ✅ PostgreSQL (pgvector), Elasticsearch | Support for mainstream vector index backends, flexible switching for different retrieval scenarios |
| Retrieval Strategies | ✅ BM25 / Dense Retrieval / GraphRAG | Support for sparse/dense recall and knowledge graph-enhanced retrieval with customizable retrieve-rerank-generate pipelines |
| LLM Integration | ✅ Support for Qwen, DeepSeek, etc., with thinking/non-thinking mode switching | Compatible with local models (e.g., via Ollama) or external API services with flexible inference configuration |
| QA Capabilities | ✅ Context-aware, multi-turn dialogue, prompt templates | Support for complex semantic modeling, instruction control and chain-of-thought Q&A with configurable prompts and context windows |
| E2E Testing | ✅ Retrieval+generation process visualization and metric evaluation | End-to-end testing tools for evaluating recall hit rates, answer coverage, BLEU/ROUGE and other metrics |
| Deployment Modes | ✅ Support for local deployment / Docker images | Meets private, offline deployment and flexible operation requirements |
| User Interfaces | ✅ Web UI + RESTful API | Interactive interface and standard API endpoints, suitable for both developers and business users |
## 🚀 Getting Started
### 🛠 Prerequisites
Make sure the following tools are installed on your system:
* [Docker](https://www.docker.com/)
* [Docker Compose](https://docs.docker.com/compose/)
* [Git](https://git-scm.com/)
### 📦 Installation
#### ① Clone the repository
```bash
# Clone the main repository
git clone https://github.com/Tencent/WeKnora.git
cd WeKnora
```
#### ② Configure environment variables
```bash
# Copy example env file
cp .env.example .env
# Edit .env and set required values
# All variables are documented in the .env.example comments
```
#### ③ Start the services
```bash
# Start all services (Ollama + backend containers)
./scripts/start_all.sh
# Or
make start-all
```
#### ③ Start the services (backup)
```bash
# Start ollama services (Optional)
ollama serve > /dev/null 2>&1 &
# Start the service
docker compose up -d
```
#### ④ Stop the services
```bash
./scripts/start_all.sh --stop
# Or
make stop-all
```
### 🌐 Access Services
Once started, services will be available at:
* Web UI: `http://localhost`
* Backend API: `http://localhost:8080`
* Jaeger Tracing: `http://localhost:16686`
### 🔌 Using WeChat Dialog Open Platform
WeKnora serves as the core technology framework for the [WeChat Dialog Open Platform](https://chatbot.weixin.qq.com), providing a more convenient usage approach:
- **Zero-code Deployment**: Simply upload knowledge to quickly deploy intelligent Q&A services within the WeChat ecosystem, achieving an "ask and answer" experience
- **Efficient Question Management**: Support for categorized management of high-frequency questions, with rich data tools to ensure accurate, reliable, and easily maintainable answers
- **WeChat Ecosystem Integration**: Through the WeChat Dialog Open Platform, WeKnora's intelligent Q&A capabilities can be seamlessly integrated into WeChat Official Accounts, Mini Programs, and other WeChat scenarios, enhancing user interaction experiences
## 📱 Interface Showcase
### Web UI Interface
<table>
<tr>
<td><b>Knowledge Upload</b><br/><img src="./docs/images/knowledges.png" alt="Knowledge Upload Interface"></td>
<td><b>Q&A Entry</b><br/><img src="./docs/images/qa.png" alt="Q&A Entry Interface"></td>
</tr>
<tr>
<td colspan="2"><b>Rich Text & Image Responses</b><br/><img src="./docs/images/answer.png" alt="Rich Answer Interface"></td>
</tr>
</table>
**Knowledge Base Management:** Support for dragging and dropping various documents, automatically identifying document structures and extracting core knowledge to establish indexes. The system clearly displays processing progress and document status, achieving efficient knowledge base management.
### Document Knowledge Graph
<table>
<tr>
<td><img src="./docs/images/graph2.png" alt="Knowledge Graph View 1"></td>
<td><img src="./docs/images/graph1.png" alt="Knowledge Graph View 2"></td>
</tr>
</table>
WeKnora supports transforming documents into knowledge graphs, displaying the relationships between different sections of the documents. Once the knowledge graph feature is enabled, the system analyzes and constructs an internal semantic association network that not only helps users understand document content but also provides structured support for indexing and retrieval, enhancing the relevance and breadth of search results.
## 📘 API Reference
Detailed API documentation is available at: [API Docs](./docs/API.md)
## 🧭 Developer Guide
### 📁 Directory Structure
```
WeKnora/
├── cmd/ # Main entry point
├── internal/ # Core business logic
├── config/ # Configuration files
├── migrations/ # DB migration scripts
├── scripts/ # Shell scripts
├── services/ # Microservice logic
├── frontend/ # Frontend app
└── docs/ # Project documentation
```
### 🔧 Common Commands
```bash
# Wipe all data from DB (use with caution)
make clean-db
```
## 🤝 Contributing
We welcome community contributions! For suggestions, bugs, or feature requests, please submit an [Issue](https://github.com/Tencent/WeKnora/issues) or directly create a Pull Request.
### 🎯 How to Contribute
- 🐛 **Bug Fixes**: Discover and fix system defects
- ✨ **New Features**: Propose and implement new capabilities
- 📚 **Documentation**: Improve project documentation
- 🧪 **Test Cases**: Write unit and integration tests
- 🎨 **UI/UX Enhancements**: Improve user interface and experience
### 📋 Contribution Process
1. **Fork the project** to your GitHub account
2. **Create a feature branch** `git checkout -b feature/amazing-feature`
3. **Commit changes** `git commit -m 'Add amazing feature'`
4. **Push branch** `git push origin feature/amazing-feature`
5. **Create a Pull Request** with detailed description of changes
### 🎨 Code Standards
- Follow [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- Format code using `gofmt`
- Add necessary unit tests
- Update relevant documentation
### 📝 Commit Guidelines
Use [Conventional Commits](https://www.conventionalcommits.org/) standard:
```
feat: Add document batch upload functionality
fix: Resolve vector retrieval precision issue
docs: Update API documentation
test: Add retrieval engine test cases
refactor: Restructure document parsing module
```
## 📄 License
This project is licensed under the [MIT License](./LICENSE).
You are free to use, modify, and distribute the code with proper attribution.

183
client/README.md Normal file
View File

@ -0,0 +1,183 @@
# WeKnora HTTP 客户端
这个包提供了与WeKnora服务进行交互的客户端库支持所有基于HTTP的接口调用使其他模块更方便地集成WeKnora服务无需直接编写HTTP请求代码。
## 主要功能
该客户端包含以下主要功能模块:
1. **会话管理**:创建、获取、更新和删除会话
2. **知识库管理**:创建、获取、更新和删除知识库
3. **知识管理**:添加、获取和删除知识内容
4. **租户管理**租户的CRUD操作
5. **知识问答**:支持普通问答和流式问答
6. **分块管理**:查询、更新和删除知识分块
7. **消息管理**:获取和删除会话消息
8. **模型管理**:创建、获取、更新和删除模型
## 使用方法
### 创建客户端实例
```go
import (
"context"
"github.com/Tencent/WeKnora/internal/client"
"time"
)
// 创建客户端实例
apiClient := client.NewClient(
"http://api.example.com",
client.WithToken("your-auth-token"),
client.WithTimeout(30*time.Second),
)
```
### 示例:创建知识库并上传文件
```go
// 创建知识库
kb := &client.KnowledgeBase{
Name: "测试知识库",
Description: "这是一个测试知识库",
ChunkingConfig: client.ChunkingConfig{
ChunkSize: 500,
ChunkOverlap: 50,
Separators: []string{"\n\n", "\n", ". ", "? ", "! "},
},
ImageProcessingConfig: client.ImageProcessingConfig{
ModelID: "image_model_id",
},
EmbeddingModelID: "embedding_model_id",
SummaryModelID: "summary_model_id",
}
kb, err := apiClient.CreateKnowledgeBase(context.Background(), kb)
if err != nil {
// 处理错误
}
// 上传知识文件并添加元数据
metadata := map[string]string{
"source": "local",
"type": "document",
}
knowledge, err := apiClient.CreateKnowledgeFromFile(context.Background(), kb.ID, "path/to/file.pdf", metadata)
if err != nil {
// 处理错误
}
```
### 示例:创建会话并进行问答
```go
// 创建会话
sessionRequest := &client.CreateSessionRequest{
KnowledgeBaseID: knowledgeBaseID,
SessionStrategy: &client.SessionStrategy{
MaxRounds: 10,
EnableRewrite: true,
FallbackStrategy: "fixed_answer",
FallbackResponse: "抱歉,我无法回答这个问题",
EmbeddingTopK: 5,
KeywordThreshold: 0.5,
VectorThreshold: 0.7,
RerankModelID: "rerank_model_id",
RerankTopK: 3,
RerankThreshold: 0.8,
SummaryModelID: "summary_model_id",
},
}
session, err := apiClient.CreateSession(context.Background(), sessionRequest)
if err != nil {
// 处理错误
}
// 普通问答
answer, err := apiClient.KnowledgeQA(context.Background(), session.ID, &client.KnowledgeQARequest{
Query: "什么是人工智能?",
})
if err != nil {
// 处理错误
}
// 流式问答
err = apiClient.KnowledgeQAStream(context.Background(), session.ID, "什么是机器学习?", func(response *client.StreamResponse) error {
// 处理每个响应片段
fmt.Print(response.Content)
return nil
})
if err != nil {
// 处理错误
}
```
### 示例:管理模型
```go
// 创建模型
modelRequest := &client.CreateModelRequest{
Name: "测试模型",
Type: client.ModelTypeChat,
Source: client.ModelSourceInternal,
Description: "这是一个测试模型",
Parameters: client.ModelParameters{
"temperature": 0.7,
"top_p": 0.9,
},
IsDefault: true,
}
model, err := apiClient.CreateModel(context.Background(), modelRequest)
if err != nil {
// 处理错误
}
// 列出所有模型
models, err := apiClient.ListModels(context.Background())
if err != nil {
// 处理错误
}
```
### 示例:管理知识分块
```go
// 列出知识分块
chunks, total, err := apiClient.ListKnowledgeChunks(context.Background(), knowledgeID, 1, 10)
if err != nil {
// 处理错误
}
// 更新分块
updateRequest := &client.UpdateChunkRequest{
Content: "更新后的分块内容",
IsEnabled: true,
}
updatedChunk, err := apiClient.UpdateChunk(context.Background(), knowledgeID, chunkID, updateRequest)
if err != nil {
// 处理错误
}
```
### 示例:获取会话消息
```go
// 获取最近消息
messages, err := apiClient.GetRecentMessages(context.Background(), sessionID, 10)
if err != nil {
// 处理错误
}
// 获取指定时间之前的消息
beforeTime := time.Now().Add(-24 * time.Hour)
olderMessages, err := apiClient.GetMessagesBefore(context.Background(), sessionID, beforeTime, 10)
if err != nil {
// 处理错误
}
```
## 完整示例
请参考 `example.go` 文件中的 `ExampleUsage` 函数,其中展示了客户端的完整使用流程。

184
client/README_EN.md Normal file
View File

@ -0,0 +1,184 @@
# WeKnora HTTP Client
This package provides a client library for interacting with WeKnora services, supporting all HTTP-based interface calls, making it easier for other modules to integrate with WeKnora services without having to write HTTP request code directly.
## Main Features
The client includes the following main functional modules:
1. **Session Management**: Create, retrieve, update, and delete sessions
2. **Knowledge Base Management**: Create, retrieve, update, and delete knowledge bases
3. **Knowledge Management**: Add, retrieve, and delete knowledge content
4. **Tenant Management**: CRUD operations for tenants
5. **Knowledge Q&A**: Supports regular Q&A and streaming Q&A
6. **Chunk Management**: Query, update, and delete knowledge chunks
7. **Message Management**: Retrieve and delete session messages
8. **Model Management**: Create, retrieve, update, and delete models
9. **Evaluation Function**: Start evaluation tasks and get evaluation results
## Usage
### Creating Client Instance
```go
import (
"context"
"github.com/Tencent/WeKnora/internal/client"
"time"
)
// Create client instance
apiClient := client.NewClient(
"http://api.example.com",
client.WithToken("your-auth-token"),
client.WithTimeout(30*time.Second),
)
```
### Example: Create Knowledge Base and Upload File
```go
// Create knowledge base
kb := &client.KnowledgeBase{
Name: "Test Knowledge Base",
Description: "This is a test knowledge base",
ChunkingConfig: client.ChunkingConfig{
ChunkSize: 500,
ChunkOverlap: 50,
Separators: []string{"\n\n", "\n", ". ", "? ", "! "},
},
ImageProcessingConfig: client.ImageProcessingConfig{
ModelID: "image_model_id",
},
EmbeddingModelID: "embedding_model_id",
SummaryModelID: "summary_model_id",
}
kb, err := apiClient.CreateKnowledgeBase(context.Background(), kb)
if err != nil {
// Handle error
}
// Upload knowledge file with metadata
metadata := map[string]string{
"source": "local",
"type": "document",
}
knowledge, err := apiClient.CreateKnowledgeFromFile(context.Background(), kb.ID, "path/to/file.pdf", metadata)
if err != nil {
// Handle error
}
```
### Example: Create Session and Chat
```go
// Create session
sessionRequest := &client.CreateSessionRequest{
KnowledgeBaseID: knowledgeBaseID,
SessionStrategy: &client.SessionStrategy{
MaxRounds: 10,
EnableRewrite: true,
FallbackStrategy: "fixed_answer",
FallbackResponse: "Sorry, I cannot answer this question",
EmbeddingTopK: 5,
KeywordThreshold: 0.5,
VectorThreshold: 0.7,
RerankModelID: "rerank_model_id",
RerankTopK: 3,
RerankThreshold: 0.8,
SummaryModelID: "summary_model_id",
},
}
session, err := apiClient.CreateSession(context.Background(), sessionRequest)
if err != nil {
// Handle error
}
// Regular Q&A
answer, err := apiClient.KnowledgeQA(context.Background(), session.ID, &client.KnowledgeQARequest{
Query: "What is artificial intelligence?",
})
if err != nil {
// Handle error
}
// Streaming Q&A
err = apiClient.KnowledgeQAStream(context.Background(), session.ID, "What is machine learning?", func(response *client.StreamResponse) error {
// Handle each response chunk
fmt.Print(response.Content)
return nil
})
if err != nil {
// Handle error
}
```
### Example: Managing Models
```go
// Create model
modelRequest := &client.CreateModelRequest{
Name: "Test Model",
Type: client.ModelTypeChat,
Source: client.ModelSourceInternal,
Description: "This is a test model",
Parameters: client.ModelParameters{
"temperature": 0.7,
"top_p": 0.9,
},
IsDefault: true,
}
model, err := apiClient.CreateModel(context.Background(), modelRequest)
if err != nil {
// Handle error
}
// List all models
models, err := apiClient.ListModels(context.Background())
if err != nil {
// Handle error
}
```
### Example: Managing Knowledge Chunks
```go
// List knowledge chunks
chunks, total, err := apiClient.ListKnowledgeChunks(context.Background(), knowledgeID, 1, 10)
if err != nil {
// Handle error
}
// Update chunk
updateRequest := &client.UpdateChunkRequest{
Content: "Updated chunk content",
IsEnabled: true,
}
updatedChunk, err := apiClient.UpdateChunk(context.Background(), knowledgeID, chunkID, updateRequest)
if err != nil {
// Handle error
}
```
### Example: Getting Session Messages
```go
// Get recent messages
messages, err := apiClient.GetRecentMessages(context.Background(), sessionID, 10)
if err != nil {
// Handle error
}
// Get messages before a specific time
beforeTime := time.Now().Add(-24 * time.Hour)
olderMessages, err := apiClient.GetMessagesBefore(context.Background(), sessionID, beforeTime, 10)
if err != nil {
// Handle error
}
```
## Complete Example
Please refer to the `ExampleUsage` function in the `example.go` file, which demonstrates the complete usage flow of the client.

171
client/chunk.go Normal file
View File

@ -0,0 +1,171 @@
// Package client provides the implementation for interacting with the WeKnora API
// This package encapsulates CRUD operations for server resources and provides a friendly interface for callers
// The Chunk related interfaces are used to manage document chunks in the knowledge base
package client
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
)
// Chunk represents the information about a document chunk
// Chunks are the basic units of storage and indexing in the knowledge base
type Chunk struct {
ID string `json:"id"` // Unique identifier of the chunk
KnowledgeID string `json:"knowledge_id"` // Identifier of the parent knowledge
TenantID uint `json:"tenant_id"` // Tenant ID
Content string `json:"content"` // Text content of the chunk
Embedding []float32 `json:"embedding"` // Vector embedding representation
ChunkIndex int `json:"chunk_index"` // Index position of chunk in the document
TotalChunks int `json:"total_chunks"` // Total number of chunks in the document
IsEnabled bool `json:"is_enabled"` // Whether this chunk is enabled
StartAt int `json:"start_at"` // Starting position in original text
EndAt int `json:"end_at"` // Ending position in original text
VectorStoreID string `json:"vector_store_id"` // Vector storage ID
KeywordStoreID string `json:"keyword_store_id"` // Keyword storage ID
EmbeddingStatus int `json:"embedding_status"` // Embedding status: 0-unprocessed, 1-processing, 2-completed
ChunkType string `json:"chunk_type"`
ImageInfo string `json:"image_info"`
CreatedAt string `json:"created_at"` // Creation time
UpdatedAt string `json:"updated_at"` // Last update time
}
// ChunkResponse represents the response for a single chunk
// API response structure containing a single chunk information
type ChunkResponse struct {
Success bool `json:"success"` // Whether operation was successful
Data Chunk `json:"data"` // Chunk data
}
// ChunkListResponse represents the response for a list of chunks
// API response structure for returning a list of chunks
type ChunkListResponse struct {
Success bool `json:"success"` // Whether operation was successful
Data []Chunk `json:"data"` // List of chunks
Total int64 `json:"total"` // Total count
Page int `json:"page"` // Current page
PageSize int `json:"page_size"` // Items per page
}
// UpdateChunkRequest represents the request structure for updating a chunk
// Used for requesting chunk information updates
type UpdateChunkRequest struct {
Content string `json:"content"` // Chunk content
Embedding []float32 `json:"embedding"` // Vector embedding
ChunkIndex int `json:"chunk_index"` // Chunk index
IsEnabled bool `json:"is_enabled"` // Whether enabled
StartAt int `json:"start_at"` // Start position
EndAt int `json:"end_at"` // End position
}
// ListKnowledgeChunks lists all chunks under a knowledge document
// Queries all chunks by knowledge ID with pagination support
// Parameters:
// - ctx: Context
// - knowledgeID: Knowledge ID
// - page: Page number, starts from 1
// - pageSize: Number of items per page
//
// Returns:
// - []Chunk: List of chunks
// - int64: Total count
// - error: Error information
func (c *Client) ListKnowledgeChunks(ctx context.Context,
knowledgeID string, page int, pageSize int,
) ([]Chunk, int64, error) {
path := fmt.Sprintf("/api/v1/chunks/%s", knowledgeID)
queryParams := url.Values{}
queryParams.Add("page", strconv.Itoa(page))
queryParams.Add("page_size", strconv.Itoa(pageSize))
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return nil, 0, err
}
var response ChunkListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, 0, err
}
return response.Data, response.Total, nil
}
// UpdateChunk updates a chunk's information
// Updates information for a specific chunk under a knowledge document
// Parameters:
// - ctx: Context
// - knowledgeID: Knowledge ID
// - chunkID: Chunk ID
// - request: Update request
//
// Returns:
// - *Chunk: Updated chunk
// - error: Error information
func (c *Client) UpdateChunk(ctx context.Context,
knowledgeID string, chunkID string, request *UpdateChunkRequest,
) (*Chunk, error) {
path := fmt.Sprintf("/api/v1/chunks/%s/%s", knowledgeID, chunkID)
resp, err := c.doRequest(ctx, http.MethodPut, path, request, nil)
if err != nil {
return nil, err
}
var response ChunkResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// DeleteChunk deletes a specific chunk
// Deletes a specific chunk under a knowledge document
// Parameters:
// - ctx: Context
// - knowledgeID: Knowledge ID
// - chunkID: Chunk ID
//
// Returns:
// - error: Error information
func (c *Client) DeleteChunk(ctx context.Context, knowledgeID string, chunkID string) error {
path := fmt.Sprintf("/api/v1/chunks/%s/%s", knowledgeID, chunkID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// DeleteChunksByKnowledgeID deletes all chunks under a knowledge document
// Batch deletes all chunks under the specified knowledge document
// Parameters:
// - ctx: Context
// - knowledgeID: Knowledge ID
//
// Returns:
// - error: Error information
func (c *Client) DeleteChunksByKnowledgeID(ctx context.Context, knowledgeID string) error {
path := fmt.Sprintf("/api/v1/chunks/%s", knowledgeID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}

104
client/client.go Normal file
View File

@ -0,0 +1,104 @@
// Package client provides the implementation for interacting with the WeKnora API
// This package encapsulates CRUD operations for server resources and provides a friendly interface for callers
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Client is the client for interacting with the WeKnora service
type Client struct {
baseURL string
httpClient *http.Client
token string
}
// ClientOption defines client configuration options
type ClientOption func(*Client)
// WithTimeout sets the HTTP client timeout
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.httpClient.Timeout = timeout
}
}
// WithToken sets the authentication token
func WithToken(token string) ClientOption {
return func(c *Client) {
c.token = token
}
}
// NewClient creates a new client instance
func NewClient(baseURL string, options ...ClientOption) *Client {
client := &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
for _, option := range options {
option(client)
}
return client
}
// doRequest executes an HTTP request
func (c *Client) doRequest(ctx context.Context,
method, path string, body interface{}, query url.Values,
) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to serialize request body: %w", err)
}
reqBody = bytes.NewBuffer(jsonData)
}
url := fmt.Sprintf("%s%s", c.baseURL, path)
if len(query) > 0 {
url = fmt.Sprintf("%s?%s", url, query.Encode())
}
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("X-API-Key", c.token)
}
if requestID := ctx.Value("RequestID"); requestID != nil {
req.Header.Set("X-Request-ID", requestID.(string))
}
return c.httpClient.Do(req)
}
// parseResponse parses an HTTP response
func parseResponse(resp *http.Response, target interface{}) error {
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
}
if target == nil {
return nil
}
return json.NewDecoder(resp.Body).Decode(target)
}

113
client/evaluation.go Normal file
View File

@ -0,0 +1,113 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Evaluation related interfaces are used for starting and retrieving model evaluation task results
// Evaluation tasks can be used to measure model performance and
// compare different embedding models, chat models, and reranking models
package client
import (
"context"
"net/http"
"net/url"
)
// EvaluationTask represents an evaluation task
// Contains basic information about a model evaluation task
type EvaluationTask struct {
ID string `json:"id"` // Task unique identifier
Status string `json:"status"` // Task status: pending, running, completed, failed
Progress int `json:"progress"` // Task progress, integer value 0-100
DatasetID string `json:"dataset_id"` // Evaluation dataset ID
EmbeddingID string `json:"embedding_id"` // Embedding model ID
ChatID string `json:"chat_id"` // Chat model ID
RerankID string `json:"rerank_id"` // Reranking model ID
CreatedAt string `json:"created_at"` // Task creation time
CompleteAt string `json:"complete_at"` // Task completion time
ErrorMsg string `json:"error_msg"` // Error message, has value when task fails
}
// EvaluationResult represents the evaluation results
// Contains detailed evaluation result information
type EvaluationResult struct {
TaskID string `json:"task_id"` // Associated task ID
Status string `json:"status"` // Task status
Progress int `json:"progress"` // Task progress
TotalQueries int `json:"total_queries"` // Total number of queries
TotalSamples int `json:"total_samples"` // Total number of samples
Metrics map[string]float64 `json:"metrics"` // Evaluation metrics collection
QueriesStat []map[string]interface{} `json:"queries_stat"` // Statistics for each query
CreatedAt string `json:"created_at"` // Creation time
CompleteAt string `json:"complete_at"` // Completion time
ErrorMsg string `json:"error_msg"` // Error message
}
// EvaluationRequest represents an evaluation request
// Parameters used to start a new evaluation task
type EvaluationRequest struct {
DatasetID string `json:"dataset_id"` // Dataset ID to evaluate
EmbeddingModelID string `json:"embedding_id"` // Embedding model ID
ChatModelID string `json:"chat_id"` // Chat model ID
RerankModelID string `json:"rerank_id"` // Reranking model ID
}
// EvaluationTaskResponse represents an evaluation task response
// API response structure for evaluation tasks
type EvaluationTaskResponse struct {
Success bool `json:"success"` // Whether operation was successful
Data EvaluationTask `json:"data"` // Evaluation task data
}
// EvaluationResultResponse represents an evaluation result response
// API response structure for evaluation results
type EvaluationResultResponse struct {
Success bool `json:"success"` // Whether operation was successful
Data EvaluationResult `json:"data"` // Evaluation result data
}
// StartEvaluation starts an evaluation task
// Creates and starts a new evaluation task based on provided parameters
// Parameters:
// - ctx: Context, used for passing request context information such as deadline, cancellation signals, etc.
// - request: Evaluation request parameters, including dataset ID and model IDs
//
// Returns:
// - *EvaluationTask: Created evaluation task information
// - error: Error information if the request fails
func (c *Client) StartEvaluation(ctx context.Context, request *EvaluationRequest) (*EvaluationTask, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/evaluation", request, nil)
if err != nil {
return nil, err
}
var response EvaluationTaskResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetEvaluationResult retrieves evaluation results
// Retrieves detailed results for an evaluation task by task ID
// Parameters:
// - ctx: Context, used for passing request context information
// - taskID: Evaluation task ID, used to identify the specific evaluation task to query
//
// Returns:
// - *EvaluationResult: Detailed evaluation task results
// - error: Error information if the request fails
func (c *Client) GetEvaluationResult(ctx context.Context, taskID string) (*EvaluationResult, error) {
queryParams := url.Values{}
queryParams.Add("task_id", taskID)
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/evaluation", nil, queryParams)
if err != nil {
return nil, err
}
var response EvaluationResultResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}

271
client/example.go Normal file
View File

@ -0,0 +1,271 @@
package client
import (
"context"
"fmt"
"os"
"strings"
"time"
)
// ExampleUsage demonstrates the complete usage flow of the WeKnora client, including:
// - Creating a client instance
// - Creating a knowledge base
// - Uploading knowledge files
// - Creating a session
// - Performing question-answering
// - Using streaming question-answering
// - Managing models
// - Managing knowledge chunks
// - Getting session messages
// - Cleaning up resources
func ExampleUsage() {
// Create a client instance
apiClient := NewClient(
"http://localhost:8080",
WithToken("your-auth-token"),
WithTimeout(30*time.Second),
)
// 1. Create a knowledge base
fmt.Println("1. Creating knowledge base...")
kb := &KnowledgeBase{
Name: "Test Knowledge Base",
Description: "This is a test knowledge base",
ChunkingConfig: ChunkingConfig{
ChunkSize: 500,
ChunkOverlap: 50,
Separators: []string{"\n\n", "\n", ". ", "? ", "! "},
},
ImageProcessingConfig: ImageProcessingConfig{
ModelID: "image_model_id",
},
EmbeddingModelID: "embedding_model_id",
SummaryModelID: "summary_model_id",
}
createdKB, err := apiClient.CreateKnowledgeBase(context.Background(), kb)
if err != nil {
fmt.Printf("Failed to create knowledge base: %v\n", err)
return
}
fmt.Printf("Knowledge base created successfully: ID=%s, Name=%s\n", createdKB.ID, createdKB.Name)
// 2. Upload knowledge file
fmt.Println("\n2. Uploading knowledge file...")
filePath := "path/to/sample.pdf" // Sample file path
// Check if file exists before uploading
if _, err := os.Stat(filePath); os.IsNotExist(err) {
fmt.Printf("File does not exist: %s, skipping upload step\n", filePath)
} else {
// Add metadata
metadata := map[string]string{
"source": "local",
"type": "document",
}
knowledge, err := apiClient.CreateKnowledgeFromFile(context.Background(), createdKB.ID, filePath, metadata, nil)
if err != nil {
fmt.Printf("Failed to upload knowledge file: %v\n", err)
} else {
fmt.Printf("File uploaded successfully: Knowledge ID=%s, Title=%s\n", knowledge.ID, knowledge.Title)
}
}
// Create text knowledge (alternative to file upload)
// Note: This is just an example, the client package may not support creating text knowledge directly
// In actual use, refer to the methods provided in client.knowledge.go
fmt.Println("\nCreating text knowledge (example)")
fmt.Println("Title: Test Text Knowledge")
fmt.Println("Description: Test knowledge created from text")
// 3. Create a model
fmt.Println("\n3. Creating model...")
modelRequest := &CreateModelRequest{
Name: "Test Model",
Type: ModelTypeChat,
Source: ModelSourceInternal,
Description: "This is a test model",
Parameters: ModelParameters{
"temperature": 0.7,
"top_p": 0.9,
},
IsDefault: true,
}
model, err := apiClient.CreateModel(context.Background(), modelRequest)
if err != nil {
fmt.Printf("Failed to create model: %v\n", err)
} else {
fmt.Printf("Model created successfully: ID=%s, Name=%s\n", model.ID, model.Name)
}
// List all models
models, err := apiClient.ListModels(context.Background())
if err != nil {
fmt.Printf("Failed to get model list: %v\n", err)
} else {
fmt.Printf("System has %d models\n", len(models))
}
// 4. Create a session
fmt.Println("\n4. Creating session...")
sessionRequest := &CreateSessionRequest{
KnowledgeBaseID: createdKB.ID,
SessionStrategy: &SessionStrategy{
MaxRounds: 10,
EnableRewrite: true,
FallbackStrategy: "fixed_answer",
FallbackResponse: "Sorry, I cannot answer this question",
EmbeddingTopK: 5,
KeywordThreshold: 0.5,
VectorThreshold: 0.7,
RerankModelID: "rerank_model_id",
RerankTopK: 3,
RerankThreshold: 0.8,
SummaryModelID: "summary_model_id",
SummaryParameters: &SummaryConfig{
Temperature: 0.7,
TopP: 0.9,
MaxTokens: 100,
},
},
}
session, err := apiClient.CreateSession(context.Background(), sessionRequest)
if err != nil {
fmt.Printf("Failed to create session: %v\n", err)
return
}
fmt.Printf("Session created successfully: ID=%s\n", session.ID)
// 5. Perform knowledge Q&A (using streaming API)
fmt.Println("\n5. Performing knowledge Q&A...")
question := "What is artificial intelligence?"
fmt.Printf("Question: %s\nAnswer: ", question)
// Use streaming API for Q&A (Note: Client may only provide streaming Q&A API)
var answer strings.Builder
var references []*SearchResult
err = apiClient.KnowledgeQAStream(context.Background(),
session.ID,
question,
func(response *StreamResponse) error {
if response.ResponseType == ResponseTypeAnswer {
answer.WriteString(response.Content)
}
if response.Done && len(response.KnowledgeReferences) > 0 {
references = response.KnowledgeReferences
}
return nil
})
if err != nil {
fmt.Printf("Q&A failed: %v\n", err)
} else {
fmt.Printf("%s\n", answer.String())
if len(references) > 0 {
fmt.Println("References:")
for i, ref := range references {
fmt.Printf("%d. %s\n", i+1, ref.Content[:min(50, len(ref.Content))]+"...")
}
}
}
// 6. Perform another streaming Q&A
fmt.Println("\n6. Performing streaming Q&A...")
streamQuestion := "What is machine learning?"
fmt.Printf("Question: %s\nAnswer: ", streamQuestion)
err = apiClient.KnowledgeQAStream(context.Background(),
session.ID,
streamQuestion,
func(response *StreamResponse) error {
fmt.Print(response.Content)
return nil
},
)
if err != nil {
fmt.Printf("\nStreaming Q&A failed: %v\n", err)
}
fmt.Println() // Line break
// 7. Get session messages
fmt.Println("\n7. Getting session messages...")
messages, err := apiClient.GetRecentMessages(context.Background(), session.ID, 10)
if err != nil {
fmt.Printf("Failed to get session messages: %v\n", err)
} else {
fmt.Printf("Retrieved %d recent messages:\n", len(messages))
for i, msg := range messages {
fmt.Printf("%d. Role: %s, Content: %s\n", i+1, msg.Role, msg.Content[:min(30, len(msg.Content))]+"...")
}
}
// 8. Manage knowledge chunks
// Assume we have uploaded knowledge and have a knowledge ID
knowledgeID := "knowledge_id_example" // In actual use, use a real knowledge ID
fmt.Println("\n8. Managing knowledge chunks...")
chunks, total, err := apiClient.ListKnowledgeChunks(context.Background(), knowledgeID, 1, 10)
if err != nil {
fmt.Printf("Failed to get knowledge chunks: %v\n", err)
} else {
fmt.Printf("Knowledge has %d chunks, retrieved %d chunks\n", total, len(chunks))
if len(chunks) > 0 {
// Update the first chunk
chunkID := chunks[0].ID
updateRequest := &UpdateChunkRequest{
Content: "Updated chunk content - " + chunks[0].Content,
IsEnabled: true,
}
updatedChunk, err := apiClient.UpdateChunk(context.Background(), knowledgeID, chunkID, updateRequest)
if err != nil {
fmt.Printf("Failed to update chunk: %v\n", err)
} else {
fmt.Printf("Chunk updated successfully: ID=%s\n", updatedChunk.ID)
}
}
}
// 10. Clean up resources (optional, in actual use, keep or delete as needed)
fmt.Println("\n10. Cleaning up resources...")
if session != nil {
if err := apiClient.DeleteSession(context.Background(), session.ID); err != nil {
fmt.Printf("Failed to delete session: %v\n", err)
} else {
fmt.Println("Session deleted")
}
}
// Delete knowledge (assuming we have a valid knowledge ID)
if knowledgeID != "" {
if err := apiClient.DeleteKnowledge(context.Background(), knowledgeID); err != nil {
fmt.Printf("Failed to delete knowledge: %v\n", err)
} else {
fmt.Println("Knowledge deleted")
}
}
if createdKB != nil {
if err := apiClient.DeleteKnowledgeBase(context.Background(), createdKB.ID); err != nil {
fmt.Printf("Failed to delete knowledge base: %v\n", err)
} else {
fmt.Println("Knowledge base deleted")
}
}
fmt.Println("\nExample completed")
}
// min returns the smaller of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}

3
client/go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/Tencent/WeKnora/client
go 1.24.2

0
client/go.sum Normal file
View File

348
client/knowledge.go Normal file
View File

@ -0,0 +1,348 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Knowledge related interfaces are used to manage knowledge entries in the knowledge base
// Knowledge entries can be created from local files, web URLs, or directly from text content
// They can also be retrieved, deleted, and downloaded as files
package client
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
// Knowledge represents knowledge information
type Knowledge struct {
ID string `json:"id"`
TenantID uint `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Source string `json:"source"`
ParseStatus string `json:"parse_status"`
EnableStatus string `json:"enable_status"`
EmbeddingModelID string `json:"embedding_model_id"`
FileName string `json:"file_name"`
FileType string `json:"file_type"`
FileSize int64 `json:"file_size"`
FilePath string `json:"file_path"`
Metadata map[string]string `json:"metadata"` // Extensible metadata for storing machine information, paths, etc.
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ProcessedAt *time.Time `json:"processed_at"`
ErrorMessage string `json:"error_message"`
}
// KnowledgeResponse represents the API response containing a single knowledge entry
type KnowledgeResponse struct {
Success bool `json:"success"`
Data Knowledge `json:"data"`
Code string `json:"code"`
Message string `json:"message"`
}
// KnowledgeListResponse represents the API response containing a list of knowledge entries with pagination
type KnowledgeListResponse struct {
Success bool `json:"success"`
Data []Knowledge `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// KnowledgeBatchResponse represents the API response for batch knowledge retrieval
type KnowledgeBatchResponse struct {
Success bool `json:"success"`
Data []Knowledge `json:"data"`
}
// UpdateImageInfoRequest represents the request structure for updating a chunk
// Used for requesting chunk information updates
type UpdateImageInfoRequest struct {
ImageInfo string `json:"image_info"` // Image information in JSON format
}
// ErrDuplicateFile is returned when attempting to create a knowledge entry with a file that already exists
var ErrDuplicateFile = errors.New("file already exists")
// CreateKnowledgeFromFile creates a knowledge entry from a local file path
func (c *Client) CreateKnowledgeFromFile(ctx context.Context,
knowledgeBaseID string, filePath string, metadata map[string]string, enableMultimodel *bool,
) (*Knowledge, error) {
// Open the local file
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// Get file information
fileInfo, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file information: %w", err)
}
// Create the HTTP request
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge/file", knowledgeBaseID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Create a multipart form writer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}
// Copy file contents
_, err = io.Copy(part, file)
if err != nil {
return nil, fmt.Errorf("failed to copy file content: %w", err)
}
// Add enable_multimodel field
if enableMultimodel != nil {
if err := writer.WriteField("enable_multimodel", strconv.FormatBool(*enableMultimodel)); err != nil {
return nil, fmt.Errorf("failed to write enable_multimodel field: %w", err)
}
}
// Add metadata to the request if provided
if metadata != nil {
metadataBytes, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to serialize metadata: %w", err)
}
if err := writer.WriteField("metadata", string(metadataBytes)); err != nil {
return nil, fmt.Errorf("failed to write metadata field: %w", err)
}
}
// Close the multipart writer
err = writer.Close()
if err != nil {
return nil, fmt.Errorf("failed to close writer: %w", err)
}
// Set request headers
req.Header.Set("Content-Type", writer.FormDataContentType())
if c.token != "" {
req.Header.Set("X-API-Key", c.token)
}
if requestID := ctx.Value("RequestID"); requestID != nil {
req.Header.Set("X-Request-ID", requestID.(string))
}
// Set the request body
req.Body = io.NopCloser(body)
// Send the request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Parse the response
var response KnowledgeResponse
if resp.StatusCode == http.StatusConflict {
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &response.Data, ErrDuplicateFile
} else if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// CreateKnowledgeFromURL creates a knowledge entry from a web URL
func (c *Client) CreateKnowledgeFromURL(ctx context.Context, knowledgeBaseID string, url string, enableMultimodel *bool) (*Knowledge, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge/url", knowledgeBaseID)
reqBody := struct {
URL string `json:"url"`
EnableMultimodel *bool `json:"enable_multimodel"`
}{
URL: url,
EnableMultimodel: enableMultimodel,
}
resp, err := c.doRequest(ctx, http.MethodPost, path, reqBody, nil)
if err != nil {
return nil, err
}
var response KnowledgeResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetKnowledge retrieves a knowledge entry by its ID
func (c *Client) GetKnowledge(ctx context.Context, knowledgeID string) (*Knowledge, error) {
path := fmt.Sprintf("/api/v1/knowledge/%s", knowledgeID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response KnowledgeResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetKnowledgeBatch retrieves multiple knowledge entries by their IDs
func (c *Client) GetKnowledgeBatch(ctx context.Context, knowledgeIDs []string) ([]Knowledge, error) {
path := "/api/v1/knowledge/batch"
queryParams := url.Values{}
for _, id := range knowledgeIDs {
queryParams.Add("ids", id)
}
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return nil, err
}
var response KnowledgeBatchResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
// ListKnowledge lists knowledge entries in a knowledge base with pagination
func (c *Client) ListKnowledge(ctx context.Context,
knowledgeBaseID string,
page int,
pageSize int,
) ([]Knowledge, int64, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge", knowledgeBaseID)
queryParams := url.Values{}
queryParams.Add("page", strconv.Itoa(page))
queryParams.Add("page_size", strconv.Itoa(pageSize))
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return nil, 0, err
}
var response KnowledgeListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, 0, err
}
return response.Data, response.Total, nil
}
// DeleteKnowledge deletes a knowledge entry by its ID
func (c *Client) DeleteKnowledge(ctx context.Context, knowledgeID string) error {
path := fmt.Sprintf("/api/v1/knowledge/%s", knowledgeID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// DownloadKnowledgeFile downloads a knowledge file to the specified local path
func (c *Client) DownloadKnowledgeFile(ctx context.Context, knowledgeID string, destPath string) error {
path := fmt.Sprintf("/api/v1/knowledge/%s/download", knowledgeID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
// Check for HTTP errors
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
}
// Create destination file
out, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer out.Close()
// Copy response body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func (c *Client) UpdateKnowledge(ctx context.Context, knowledge *Knowledge) error {
path := fmt.Sprintf("/api/v1/knowledge/%s", knowledge.ID)
resp, err := c.doRequest(ctx, http.MethodPut, path, knowledge, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// UpdateChunk updates a chunk's information
// Updates information for a specific chunk under a knowledge document
// Parameters:
// - ctx: Context
// - knowledgeID: Knowledge ID
// - chunkID: Chunk ID
// - request: Update request
//
// Returns:
// - *Chunk: Updated chunk
// - error: Error information
func (c *Client) UpdateImageInfo(ctx context.Context,
knowledgeID string, chunkID string, request *UpdateImageInfoRequest,
) error {
path := fmt.Sprintf("/api/v1/knowledge/image/%s/%s", knowledgeID, chunkID)
resp, err := c.doRequest(ctx, http.MethodPut, path, request, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}

211
client/knowledgebase.go Normal file
View File

@ -0,0 +1,211 @@
// Package client provides the implementation for interacting with the WeKnora API
// The KnowledgeBase related interfaces are used to manage knowledge bases
// Knowledge bases are collections of knowledge entries that can be used for question-answering
// They can also be searched and queried using hybrid search
package client
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
)
// KnowledgeBase represents a knowledge base
type KnowledgeBase struct {
ID string `json:"id"`
Name string `json:"name"` // Name must be unique within the same tenant
Description string `json:"description"`
TenantID uint `json:"tenant_id"` // Changed to uint type
ChunkingConfig ChunkingConfig `json:"chunking_config"`
ImageProcessingConfig ImageProcessingConfig `json:"image_processing_config"`
EmbeddingModelID string `json:"embedding_model_id"`
SummaryModelID string `json:"summary_model_id"` // Summary model ID
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// KnowledgeBaseConfig represents knowledge base configuration
type KnowledgeBaseConfig struct {
ChunkingConfig ChunkingConfig `json:"chunking_config"`
ImageProcessingConfig ImageProcessingConfig `json:"image_processing_config"`
}
// ChunkingConfig represents document chunking configuration
type ChunkingConfig struct {
ChunkSize int `json:"chunk_size"` // Chunk size
ChunkOverlap int `json:"chunk_overlap"` // Overlap size
Separators []string `json:"separators"` // Separators
EnableMultimodal bool `json:"enable_multimodal"` // Whether to enable multimodal processing
}
// ImageProcessingConfig represents image processing configuration
type ImageProcessingConfig struct {
ModelID string `json:"model_id"` // Multimodal model ID
}
// KnowledgeBaseResponse knowledge base response
type KnowledgeBaseResponse struct {
Success bool `json:"success"`
Data KnowledgeBase `json:"data"`
}
// KnowledgeBaseListResponse knowledge base list response
type KnowledgeBaseListResponse struct {
Success bool `json:"success"`
Data []KnowledgeBase `json:"data"`
}
// SearchResult represents search result
type SearchResult struct {
ID string `json:"id"`
Content string `json:"content"`
KnowledgeID string `json:"knowledge_id"`
ChunkIndex int `json:"chunk_index"`
KnowledgeTitle string `json:"knowledge_title"`
StartAt int `json:"start_at"`
EndAt int `json:"end_at"`
Seq int `json:"seq"`
Score float64 `json:"score"`
ChunkType string `json:"chunk_type"`
ImageInfo string `json:"image_info"`
Metadata map[string]string `json:"metadata"`
KnowledgeFilename string `json:"knowledge_filename"`
KnowledgeSource string `json:"knowledge_source"`
}
// HybridSearchResponse hybrid search response
type HybridSearchResponse struct {
Success bool `json:"success"`
Data []*SearchResult `json:"data"`
}
type CopyKnowledgeBaseRequest struct {
SourceID string `json:"source_id"`
TargetID string `json:"target_id"`
}
// CreateKnowledgeBase creates a knowledge base
func (c *Client) CreateKnowledgeBase(ctx context.Context, knowledgeBase *KnowledgeBase) (*KnowledgeBase, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/knowledge-bases", knowledgeBase, nil)
if err != nil {
return nil, err
}
var response KnowledgeBaseResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetKnowledgeBase gets a knowledge base
func (c *Client) GetKnowledgeBase(ctx context.Context, knowledgeBaseID string) (*KnowledgeBase, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s", knowledgeBaseID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response KnowledgeBaseResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// ListKnowledgeBases lists knowledge bases
func (c *Client) ListKnowledgeBases(ctx context.Context) ([]KnowledgeBase, error) {
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/knowledge-bases", nil, nil)
if err != nil {
return nil, err
}
var response KnowledgeBaseListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
// UpdateKnowledgeBaseRequest update knowledge base request
type UpdateKnowledgeBaseRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Config *KnowledgeBaseConfig `json:"config"`
}
// UpdateKnowledgeBase updates a knowledge base
func (c *Client) UpdateKnowledgeBase(ctx context.Context,
knowledgeBaseID string,
request *UpdateKnowledgeBaseRequest,
) (*KnowledgeBase, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s", knowledgeBaseID)
resp, err := c.doRequest(ctx, http.MethodPut, path, request, nil)
if err != nil {
return nil, err
}
var response KnowledgeBaseResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// DeleteKnowledgeBase deletes a knowledge base
func (c *Client) DeleteKnowledgeBase(ctx context.Context, knowledgeBaseID string) error {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s", knowledgeBaseID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// HybridSearch performs hybrid search
func (c *Client) HybridSearch(ctx context.Context, knowledgeBaseID string, query string) ([]*SearchResult, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/hybrid-search", knowledgeBaseID)
queryParams := url.Values{}
queryParams.Add("query", query)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return nil, err
}
var response HybridSearchResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
func (c *Client) CopyKnowledgeBase(ctx context.Context, request *CopyKnowledgeBaseRequest) error {
path := "/api/v1/knowledge-bases/copy"
resp, err := c.doRequest(ctx, http.MethodPost, path, request, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}

82
client/message.go Normal file
View File

@ -0,0 +1,82 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Message related interfaces are used to manage messages in a session
// Messages can be created, retrieved, deleted, and queried
package client
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
// Message message information
type Message struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
RequestID string `json:"request_id"`
Content string `json:"content"`
Role string `json:"role"`
KnowledgeReferences []*SearchResult `json:"knowledge_references" `
IsCompleted bool `json:"is_completed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MessageListResponse message list response
type MessageListResponse struct {
Success bool `json:"success"`
Data []Message `json:"data"`
}
// LoadMessages loads session messages, supports pagination and time filtering
func (c *Client) LoadMessages(ctx context.Context, sessionID string, limit int, beforeTime *time.Time) ([]Message, error) {
path := fmt.Sprintf("/api/v1/messages/%s/load", sessionID)
queryParams := url.Values{}
queryParams.Add("limit", strconv.Itoa(limit))
if beforeTime != nil {
queryParams.Add("before_time", beforeTime.Format(time.RFC3339Nano))
}
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return nil, err
}
var response MessageListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
// GetRecentMessages gets recent messages from a session
func (c *Client) GetRecentMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) {
return c.LoadMessages(ctx, sessionID, limit, nil)
}
// GetMessagesBefore gets messages before a specified time
func (c *Client) GetMessagesBefore(ctx context.Context, sessionID string, beforeTime time.Time, limit int) ([]Message, error) {
return c.LoadMessages(ctx, sessionID, limit, &beforeTime)
}
// DeleteMessage deletes a message
func (c *Client) DeleteMessage(ctx context.Context, sessionID string, messageID string) error {
path := fmt.Sprintf("/api/v1/messages/%s/%s", sessionID, messageID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}

155
client/model.go Normal file
View File

@ -0,0 +1,155 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Model related interfaces are used to manage models for different tasks
// Models can be created, retrieved, updated, deleted, and queried
package client
import (
"context"
"fmt"
"net/http"
)
// ModelType model type
type ModelType string
// ModelSource model source
type ModelSource string
// ModelParameters model parameters
type ModelParameters map[string]interface{}
// Model model information
type Model struct {
ID string `json:"id"`
TenantID uint `json:"tenant_id"`
Name string `json:"name"`
Type ModelType `json:"type"`
Source ModelSource `json:"source"`
Description string `json:"description"`
Parameters ModelParameters `json:"parameters"`
IsDefault bool `json:"is_default"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// CreateModelRequest model creation request
type CreateModelRequest struct {
Name string `json:"name"`
Type ModelType `json:"type"`
Source ModelSource `json:"source"`
Description string `json:"description"`
Parameters ModelParameters `json:"parameters"`
IsDefault bool `json:"is_default"`
}
// UpdateModelRequest model update request
type UpdateModelRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters ModelParameters `json:"parameters"`
IsDefault bool `json:"is_default"`
}
// ModelResponse model response
type ModelResponse struct {
Success bool `json:"success"`
Data Model `json:"data"`
}
// ModelListResponse model list response
type ModelListResponse struct {
Success bool `json:"success"`
Data []Model `json:"data"`
}
// Model type constants
const (
ModelTypeEmbedding ModelType = "embedding"
ModelTypeChat ModelType = "chat"
ModelTypeRerank ModelType = "rerank"
ModelTypeSummary ModelType = "summary"
)
// Model source constants
const (
ModelSourceInternal ModelSource = "internal"
ModelSourceExternal ModelSource = "external"
)
// CreateModel creates a model
func (c *Client) CreateModel(ctx context.Context, request *CreateModelRequest) (*Model, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/models", request, nil)
if err != nil {
return nil, err
}
var response ModelResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetModel gets a model
func (c *Client) GetModel(ctx context.Context, modelID string) (*Model, error) {
path := fmt.Sprintf("/api/v1/models/%s", modelID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response ModelResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// ListModels lists all models
func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/models", nil, nil)
if err != nil {
return nil, err
}
var response ModelListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
// UpdateModel updates a model
func (c *Client) UpdateModel(ctx context.Context, modelID string, request *UpdateModelRequest) (*Model, error) {
path := fmt.Sprintf("/api/v1/models/%s", modelID)
resp, err := c.doRequest(ctx, http.MethodPut, path, request, nil)
if err != nil {
return nil, err
}
var response ModelResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// DeleteModel deletes a model
func (c *Client) DeleteModel(ctx context.Context, modelID string) error {
path := fmt.Sprintf("/api/v1/models/%s", modelID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}

403
client/session.go Normal file
View File

@ -0,0 +1,403 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Session related interfaces are used to manage sessions for question-answering
// Sessions can be created, retrieved, updated, deleted, and queried
// They can also be used to generate titles for sessions
package client
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// SessionStrategy defines session strategy
type SessionStrategy struct {
MaxRounds int `json:"max_rounds"` // Maximum number of rounds to maintain
EnableRewrite bool `json:"enable_rewrite"` // Enable query rewrite
FallbackStrategy string `json:"fallback_strategy"` // Fallback strategy
FallbackResponse string `json:"fallback_response"` // Fixed fallback response content
EmbeddingTopK int `json:"embedding_top_k"` // Top K for vector retrieval
KeywordThreshold float64 `json:"keyword_threshold"` // Keyword retrieval threshold
VectorThreshold float64 `json:"vector_threshold"` // Vector retrieval threshold
RerankModelID string `json:"rerank_model_id"` // Rerank model ID
RerankTopK int `json:"rerank_top_k"` // Top K for reranking
RerankThreshold float64 `json:"reranking_threshold"` // Reranking threshold
SummaryModelID string `json:"summary_model_id"` // Summary model ID
SummaryParameters *SummaryConfig `json:"summary_parameters"` // Summary model parameters
NoMatchPrefix string `json:"no_match_prefix"` // Fallback response prefix
}
// SummaryConfig defines summary configuration
type SummaryConfig struct {
MaxTokens int `json:"max_tokens"`
TopP float64 `json:"top_p"`
TopK int `json:"top_k"`
FrequencyPenalty float64 `json:"frequency_penalty"`
PresencePenalty float64 `json:"presence_penalty"`
RepeatPenalty float64 `json:"repeat_penalty"`
Prompt string `json:"prompt"`
ContextTemplate string `json:"context_template"`
NoMatchPrefix string `json:"no_match_prefix"`
Temperature float64 `json:"temperature"`
Seed int `json:"seed"`
MaxCompletionTokens int `json:"max_completion_tokens"`
}
// CreateSessionRequest session creation request
type CreateSessionRequest struct {
KnowledgeBaseID string `json:"knowledge_base_id"` // Associated knowledge base ID
SessionStrategy *SessionStrategy `json:"session_strategy"` // Session strategy
}
// Session session information
type Session struct {
ID string `json:"id"`
TenantID uint `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
Title string `json:"title"`
MaxRounds int `json:"max_rounds"`
EnableRewrite bool `json:"enable_rewrite"`
FallbackStrategy string `json:"fallback_strategy"`
FallbackResponse string `json:"fallback_response"`
EmbeddingTopK int `json:"embedding_top_k"`
KeywordThreshold float64 `json:"keyword_threshold"`
VectorThreshold float64 `json:"vector_threshold"`
RerankModelID string `json:"rerank_model_id"`
RerankTopK int `json:"rerank_top_k"`
RerankThreshold float64 `json:"reranking_threshold"` // Reranking threshold
SummaryModelID string `json:"summary_model_id"`
SummaryParameters *SummaryConfig `json:"summary_parameters"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// SessionResponse session response
type SessionResponse struct {
Success bool `json:"success"`
Data Session `json:"data"`
}
// SessionListResponse session list response
type SessionListResponse struct {
Success bool `json:"success"`
Data []Session `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// CreateSession creates a session
func (c *Client) CreateSession(ctx context.Context, request *CreateSessionRequest) (*Session, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/sessions", request, nil)
if err != nil {
return nil, err
}
var response SessionResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetSession gets a session
func (c *Client) GetSession(ctx context.Context, sessionID string) (*Session, error) {
path := fmt.Sprintf("/api/v1/sessions/%s", sessionID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response SessionResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetSessionsByTenant gets all sessions for a tenant
func (c *Client) GetSessionsByTenant(ctx context.Context, page int, pageSize int) ([]Session, int, error) {
queryParams := url.Values{}
queryParams.Add("page", strconv.Itoa(page))
queryParams.Add("page_size", strconv.Itoa(pageSize))
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/sessions", nil, queryParams)
if err != nil {
return nil, 0, err
}
var response SessionListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, 0, err
}
return response.Data, response.Total, nil
}
// UpdateSession updates a session
func (c *Client) UpdateSession(ctx context.Context, sessionID string, request *CreateSessionRequest) (*Session, error) {
path := fmt.Sprintf("/api/v1/sessions/%s", sessionID)
resp, err := c.doRequest(ctx, http.MethodPut, path, request, nil)
if err != nil {
return nil, err
}
var response SessionResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// DeleteSession deletes a session
func (c *Client) DeleteSession(ctx context.Context, sessionID string) error {
path := fmt.Sprintf("/api/v1/sessions/%s", sessionID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// GenerateTitleRequest title generation request
type GenerateTitleRequest struct {
Messages []Message `json:"messages"`
}
// GenerateTitleResponse title generation response
type GenerateTitleResponse struct {
Success bool `json:"success"`
Data string `json:"data"`
}
// GenerateTitle generates a session title
func (c *Client) GenerateTitle(ctx context.Context, sessionID string, request *GenerateTitleRequest) (string, error) {
path := fmt.Sprintf("/api/v1/sessions/%s/generate_title", sessionID)
resp, err := c.doRequest(ctx, http.MethodPost, path, request, nil)
if err != nil {
return "", err
}
var response GenerateTitleResponse
if err := parseResponse(resp, &response); err != nil {
return "", err
}
return response.Data, nil
}
// KnowledgeQARequest knowledge Q&A request
type KnowledgeQARequest struct {
Query string `json:"query"`
}
type ResponseType string
const (
ResponseTypeAnswer ResponseType = "answer"
ResponseTypeReferences ResponseType = "references"
)
// StreamResponse streaming response
type StreamResponse struct {
ID string `json:"id"` // Unique identifier
ResponseType ResponseType `json:"response_type"` // Response type
Content string `json:"content"` // Current content fragment
Done bool `json:"done"` // Whether completed
KnowledgeReferences []*SearchResult `json:"knowledge_references"` // Knowledge references
}
// KnowledgeQAStream knowledge Q&A streaming API
func (c *Client) KnowledgeQAStream(ctx context.Context, sessionID string, query string, callback func(*StreamResponse) error) error {
path := fmt.Sprintf("/api/v1/knowledge-chat/%s", sessionID)
fmt.Printf("Starting KnowledgeQAStream request, session ID: %s, query: %s\n", sessionID, query)
request := &KnowledgeQARequest{
Query: query,
}
resp, err := c.doRequest(ctx, http.MethodPost, path, request, nil)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
err := fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
fmt.Printf("Request returned error status: %v\n", err)
return err
}
fmt.Println("Successfully established SSE connection, processing data stream")
// Use bufio to read SSE data line by line
scanner := bufio.NewScanner(resp.Body)
var dataBuffer string
var eventType string
messageCount := 0
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("Received SSE line: %s\n", line)
// Empty line indicates the end of an event
if line == "" {
if dataBuffer != "" {
fmt.Printf("Processing data: %s, event type: %s\n", dataBuffer, eventType)
var streamResponse StreamResponse
if err := json.Unmarshal([]byte(dataBuffer), &streamResponse); err != nil {
fmt.Printf("Failed to parse SSE data: %v\n", err)
return fmt.Errorf("failed to parse SSE data: %w", err)
}
messageCount++
fmt.Printf("Parsed message #%d, done status: %v\n", messageCount, streamResponse.Done)
if err := callback(&streamResponse); err != nil {
fmt.Printf("Callback processing failed: %v\n", err)
return err
}
dataBuffer = ""
eventType = ""
}
continue
}
// Process lines with event: prefix
if strings.HasPrefix(line, "event:") {
eventType = line[6:] // Remove "event:" prefix
fmt.Printf("Set event type: %s\n", eventType)
}
// Process lines with data: prefix
if strings.HasPrefix(line, "data:") {
dataBuffer = line[5:] // Remove "data:" prefix
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("Failed to read SSE stream: %v\n", err)
return fmt.Errorf("failed to read SSE stream: %w", err)
}
fmt.Printf("KnowledgeQAStream completed, processed %d messages\n", messageCount)
return nil
}
// ContinueStream continues to receive an active stream for a session
func (c *Client) ContinueStream(ctx context.Context, sessionID string, messageID string, callback func(*StreamResponse) error) error {
path := fmt.Sprintf("/api/v1/sessions/continue-stream/%s", sessionID)
queryParams := url.Values{}
queryParams.Add("message_id", messageID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, queryParams)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
}
// Use bufio to read SSE data line by line
scanner := bufio.NewScanner(resp.Body)
var dataBuffer string
var eventType string
for scanner.Scan() {
line := scanner.Text()
// Empty line indicates the end of an event
if line == "" {
if dataBuffer != "" && eventType == "message" {
var streamResponse StreamResponse
if err := json.Unmarshal([]byte(dataBuffer), &streamResponse); err != nil {
return fmt.Errorf("failed to parse SSE data: %w", err)
}
if err := callback(&streamResponse); err != nil {
return err
}
dataBuffer = ""
eventType = ""
}
continue
}
// Process lines with event: prefix
if strings.HasPrefix(line, "event:") {
eventType = line[6:] // Remove "event:" prefix
}
// Process lines with data: prefix
if strings.HasPrefix(line, "data:") {
dataBuffer = line[5:] // Remove "data:" prefix
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to read SSE stream: %w", err)
}
return nil
}
// SearchKnowledgeRequest knowledge search request
type SearchKnowledgeRequest struct {
Query string `json:"query"` // Query content
KnowledgeBaseID string `json:"knowledge_base_id"` // Knowledge base ID
}
// SearchKnowledgeResponse search results response
type SearchKnowledgeResponse struct {
Success bool `json:"success"`
Data []*SearchResult `json:"data"`
}
// SearchKnowledge performs knowledge base search without LLM summarization
func (c *Client) SearchKnowledge(ctx context.Context, request *SearchKnowledgeRequest) ([]*SearchResult, error) {
fmt.Printf("Starting SearchKnowledge request, knowledge base ID: %s, query: %s\n",
request.KnowledgeBaseID, request.Query)
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/knowledge-search", request, nil)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
err := fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
fmt.Printf("Request returned error status: %v\n", err)
return nil, err
}
var response SearchKnowledgeResponse
if err := parseResponse(resp, &response); err != nil {
fmt.Printf("Failed to parse response: %v\n", err)
return nil, err
}
fmt.Printf("SearchKnowledge completed, found %d results\n", len(response.Data))
return response.Data, nil
}

136
client/tenant.go Normal file
View File

@ -0,0 +1,136 @@
// Package client provides the implementation for interacting with the WeKnora API
// The Tenant related interfaces are used to manage tenants in the system
// Tenants can be created, retrieved, updated, deleted, and queried
// They can also be used to manage retriever engines for different tasks
package client
import (
"context"
"fmt"
"net/http"
"time"
)
// RetrieverEngines defines a collection of retriever engine parameters
type RetrieverEngines struct {
Engines []RetrieverEngineParams `json:"engines"`
}
// RetrieverEngineParams contains configuration for retriever engines
type RetrieverEngineParams struct {
RetrieverType string `json:"retriever_type"` // Type of retriever (e.g., keywords, vector)
RetrieverEngineType string `json:"retriever_engine_type"` // Type of engine implementing the retriever
}
// Tenant represents tenant information in the system
type Tenant struct {
ID uint `yaml:"id" json:"id" gorm:"primaryKey"`
// Tenant name
Name string `yaml:"name" json:"name"`
// Tenant description
Description string `yaml:"description" json:"description"`
// API key for authentication
APIKey string `yaml:"api_key" json:"api_key"`
// Tenant status (active, inactive)
Status string `yaml:"status" json:"status" gorm:"default:'active'"`
// Configured retrieval engines
RetrieverEngines RetrieverEngines `yaml:"retriever_engines" json:"retriever_engines" gorm:"type:json"`
// Business/department information
Business string `yaml:"business" json:"business"`
// Creation timestamp
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
// Last update timestamp
UpdatedAt time.Time `yaml:"updated_at" json:"updated_at"`
}
// TenantResponse represents the API response structure for tenant operations
type TenantResponse struct {
Success bool `json:"success"` // Whether the operation was successful
Data Tenant `json:"data"` // Tenant data
}
// TenantListResponse represents the API response structure for listing tenants
type TenantListResponse struct {
Success bool `json:"success"` // Whether the operation was successful
Data struct {
Items []Tenant `json:"items"` // List of tenant items
} `json:"data"`
}
// CreateTenant creates a new tenant
func (c *Client) CreateTenant(ctx context.Context, tenant *Tenant) (*Tenant, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/api/v1/tenants", tenant, nil)
if err != nil {
return nil, err
}
var response TenantResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// GetTenant retrieves a tenant by ID
func (c *Client) GetTenant(ctx context.Context, tenantID uint) (*Tenant, error) {
path := fmt.Sprintf("/api/v1/tenants/%d", tenantID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response TenantResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// UpdateTenant updates an existing tenant
func (c *Client) UpdateTenant(ctx context.Context, tenant *Tenant) (*Tenant, error) {
path := fmt.Sprintf("/api/v1/tenants/%d", tenant.ID)
resp, err := c.doRequest(ctx, http.MethodPut, path, tenant, nil)
if err != nil {
return nil, err
}
var response TenantResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return &response.Data, nil
}
// DeleteTenant removes a tenant by ID
func (c *Client) DeleteTenant(ctx context.Context, tenantID uint) error {
path := fmt.Sprintf("/api/v1/tenants/%d", tenantID)
resp, err := c.doRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return err
}
var response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
return parseResponse(resp, &response)
}
// ListTenants retrieves all tenants
func (c *Client) ListTenants(ctx context.Context) ([]Tenant, error) {
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/tenants", nil, nil)
if err != nil {
return nil, err
}
var response TenantListResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data.Items, nil
}

108
cmd/server/main.go Normal file
View File

@ -0,0 +1,108 @@
// Package main is the main package for the WeKnora server
// It contains the main function and the entry point for the server
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/Tencent/WeKnora/internal/config"
"github.com/Tencent/WeKnora/internal/container"
"github.com/Tencent/WeKnora/internal/runtime"
"github.com/Tencent/WeKnora/internal/tracing"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
func main() {
// Set log format with request ID
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
log.SetOutput(os.Stdout)
// Set Gin mode
if os.Getenv("GIN_MODE") == "release" {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.DebugMode)
config.SetEnv()
}
// Build dependency injection container
c := container.BuildContainer(runtime.GetContainer())
// Run application
err := c.Invoke(func(
cfg *config.Config,
router *gin.Engine,
tracer *tracing.Tracer,
resourceCleaner interfaces.ResourceCleaner,
) error {
// Create context for resource cleanup
shutdownTimeout := cfg.Server.ShutdownTimeout
if shutdownTimeout == 0 {
shutdownTimeout = 30 * time.Second
}
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cleanupCancel()
// Register tracer cleanup function to resource cleaner
resourceCleaner.RegisterWithName("Tracer", func() error {
return tracer.Cleanup(cleanupCtx)
})
// Create HTTP server
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: router,
}
ctx, done := context.WithCancel(context.Background())
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
go func() {
sig := <-signals
log.Printf("Received signal: %v, starting server shutdown...", sig)
// Create a context with timeout for server shutdown
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
// Clean up all registered resources
log.Println("Cleaning up resources...")
errs := resourceCleaner.Cleanup(cleanupCtx)
if len(errs) > 0 {
log.Printf("Errors occurred during resource cleanup: %v", errs)
}
log.Println("Server has exited")
done()
}()
// Start server
log.Printf("Server is running at %s:%d", cfg.Server.Host, cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start server: %v", err)
}
// Wait for shutdown signal
<-ctx.Done()
return nil
})
if err != nil {
log.Fatalf("Failed to run application: %v", err)
}
}

536
config/config.yaml Normal file
View File

@ -0,0 +1,536 @@
# 服务器配置
server:
port: 8088
host: "0.0.0.0"
# 对话服务配置
conversation:
max_rounds: 5
keyword_threshold: 0.3
embedding_top_k: 10
vector_threshold: 0.5
rerank_threshold: 0.7
rerank_top_k: 5
fallback_strategy: "fixed"
fallback_response: "抱歉,我无法回答这个问题。"
fallback_prompt: |
你是一个专业、友好的AI助手。现在用户提出的问题超出了你的知识库范围你需要生成一个礼貌且有帮助的回复。
## 回复要求
- 诚实承认你无法提供准确答案
- 简洁友好,不要过度道歉
- 可以提供相关的建议或替代方案
- 回复控制在50字以内
- 使用礼貌、专业的语气
## Few-shot示例
用户问题: 今天杭州西湖的游客数量是多少?
回复: 抱歉我无法获取实时的杭州西湖游客数据。您可以通过杭州旅游官网或相关APP查询这一信息。
用户问题: 张教授的新论文发表了吗?
回复: 我没有张教授的最新论文信息。建议您查询学术数据库或直接联系张教授获取最新动态。
用户问题: 我的银行卡号是多少?
回复: 作为AI助手我无法获取您的个人银行信息。请登录您的银行APP或联系银行客服获取相关信息。
## 用户当前的问题是:
{{.Query}}
enable_rewrite: true
enable_rerank: true
rewrite_prompt_system: |
你是一个专注于指代消解和省略补全的智能助手,你的任务是根据历史对话上下文,清晰识别用户问题中的代词并替换为明确的主语,同时补全省略的关键信息。
## 改写目标
请根据历史对话,对当前用户问题进行改写,目标是:
- 进行指代消解,将"它"、"这个"、"那个"、"他"、"她"、"它们"、"他们"、"她们"等代词替换为明确的主语
- 补全省略的关键信息,确保问题语义完整
- 保持问题的原始含义和表达方式不变
- 改写后必须也是一个问题
- 改写后的问题字数控制在30字以内
- 仅输出改写后的问题,不要输出任何解释,更不要尝试回答该问题,后面有其他助手回去解答此问题
## Few-shot示例
示例1:
历史对话:
用户: 微信支付有哪些功能?
助手: 微信支付的主要功能包括转账、付款码、收款、信用卡还款等多种支付服务。
用户问题: 它的安全性
改写后: 微信支付的安全性
示例2:
历史对话:
用户: 苹果手机电池不耐用怎么办?
助手: 您可以通过降低屏幕亮度、关闭后台应用和定期更新系统来延长电池寿命。
用户问题: 这样会影响使用体验吗?
改写后: 降低屏幕亮度和关闭后台应用是否影响使用体验
示例3:
历史对话:
用户: 如何制作红烧肉?
助手: 红烧肉的制作需要先将肉块焯水,然后加入酱油、糖等调料慢炖。
用户问题: 需要炖多久?
改写后: 红烧肉需要炖多久
示例4:
历史对话:
用户: 北京到上海的高铁票价是多少?
助手: 北京到上海的高铁票价根据车次和座位类型不同二等座约为553元一等座约为933元。
用户问题: 时间呢?
改写后: 北京到上海的高铁时长
示例5:
历史对话:
用户: 如何注册微信账号?
助手: 注册微信账号需要下载微信APP输入手机号接收验证码然后设置昵称和密码。
用户问题: 国外手机号可以吗?
改写后: 国外手机号是否可以注册微信账号
rewrite_prompt_user: |
## 历史对话背景
{{range .Conversation}}
------BEGIN------
用户的问题是:{{.Query}}
助手的回答是:{{.Answer}}
------END------
{{end}}
## 需要改写的用户问题
{{.Query}}
## 改写后的问题
keywords_extraction_prompt: |
# 角色
你是一个专业的关键词提取助手,你的任务是根据用户的问题,提取出最重要的关键词/短语。
# 要求
- 总结用户的问题,并给出最重要的关键词/短语,关键词/短语的数量不超过5个
- 使用逗号作为分隔符来分隔关键词/短语
- 关键词/短语必须来自于用户的问题,不得虚构
- 不要输出任何解释,直接输出关键词/短语,不要有任何前缀、解释或标点符号,不要尝试回答该问题,后面有其他助手会去搜索此问题
# 输出格式
keyword1, keyword2, keyword3, keyword4, keyword5
# Examples
## Example 1
USER: 如何提高英语口语水平?
###############
Output: 英语口语, 口语水平, 提高英语口语, 英语口语提升, 英语口语练习
## Example 2
USER: 最近上海有什么好玩的展览活动?
###############
Output: 上海展览, 展览活动, 上海展览推荐, 展览活动推荐, 上海展览活动
## Example 3
USER: 苹果手机电池不耐用怎么解决?
###############
Output: 苹果手机, 电池不耐用, 电池优化, 电池寿命, 电池保养
## Example 4
USER: Python的Logo长啥样
###############
Output: Python Logo
## Example 5
USER: 如何使用iPhone连接WiFi
###############
Output: iPhone, 连接WiFi, 使用iPhone连接WiFi
# Real Data
USER: {{.Query}}
keywords_extraction_prompt_user: |
Output:
generate_summary_prompt: |
你是一个精准的文章总结专家。你的任务是提取并总结用户提供的文章或片段的核心内容。
## 核心要求
- 总结结果长度为50-100个字根据内容复杂度灵活调整
- 完全基于提供的文章内容生成总结,不添加任何未在文章中出现的信息
- 确保总结包含文章的关键信息点和主要结论
- 即使文章内容较复杂或专业,也必须尝试提取核心要点进行总结
- 直接输出总结结果,不包含任何引言、前缀或解释
## 格式与风格
- 使用客观、中立的第三人称陈述语气
- 使用清晰简洁的中文表达
- 保持逻辑连贯性,确保句与句之间有合理过渡
- 避免重复使用相同的表达方式或句式结构
## 注意事项
- 绝对不输出"无法生成"、"无法总结"、"内容不足"等拒绝回应的词语
- 不要照抄或参考示例中的任何内容,确保总结完全基于用户提供的新文章
- 对于任何文本都尽最大努力提取重点并总结,无论长度或复杂度
## 以下是用户给出的文章相关信息:
generate_session_title_prompt: |
你是一个专业的会话标题生成助手,你的任务是为用户提问创建简洁、精准且具描述性的标题。
## 格式要求
- 标题长度必须在10个字以内
- 标题应准确反映用户问题的核心主题
- 使用名词短语结构,避免使用问句
- 保持简洁明了,删除非必要词语
- 不要使用"关于"、"如何"等冗余词语开头
- 直接输出标题文本,不要有任何前缀、解释或标点符号
## Few-shot示例
用户问题: 如何提高英语口语水平?
标题: 英语口语提升
用户问题: 最近上海有什么好玩的展览活动?
标题: 上海展览推荐
用户问题: 苹果手机电池不耐用怎么解决?
标题: 苹果电池优化
## 用户的问题是:
summary:
repeat_penalty: 1.0
temperature: 0.3
max_completion_tokens: 2048
no_match_prefix: |-
<think>
</think>
NO_MATCH
prompt: |
这是用户和助手之间的对话。当用户提出问题时,助手会基于特定的信息进行解答。助手首先在心中思考推理过程,然后向用户提供答案。
推理过程用 <think> </think> 标签包围答案直接输出在think标签后面
<think>
这里是推理过程
</think>
这里是答案
context_template: |
你是一个专业的智能信息检索助手,名为小微,犹如专业的高级秘书,依据检索到的信息回答用户问题。
当用户提出问题时,助手只能基于给定的信息进行解答,不能利用任何先验知识。
## 回答问题规则
- 仅根据检索到的信息中的事实进行回复,不得运用任何先验知识,保持回应的客观性和准确性。
- 复杂问题和答案的按Markdown分结构展示总述部分不需要拆分
- 如果是比较简单的答案,不需要把最终答案拆分的过于细碎
- 结果中使用的图片地址必须来自于检索到的信息,不得虚构
- 检查结果中的文字和图片是否来自于检索到的信息,如果扩展了不在检索到的信息中的内容,必须进行修改,直到得到最终答案
- 如果用户问题无法回答只输出NO_MATCH即可
<think>
</think>
NO_MATCH
## 输出限制
- 以Markdown图文格式输出你的最终结果
- 输出内容要保证简短且全面,条理清晰,信息明确,不重复。
## 当前时间是:
{{.CurrentTime}} {{.CurrentWeek}}
## 检索到的信息如下:
------BEGIN------
{{range .Contexts}}
{{.}}
{{end}}
------END------
## 用户当前的问题是:
{{.Query}}
extract_entities_prompt: |
## 任务
用户提供的文本中,提取所有符合以下实体类型的实体:
EntityTypes: [Person, Organization, Location, Product, Event, Date, Work, Concept, Resource, Category, Operation]
## 要求
1. 提取结果必须以JSON数组格式输出
2. 每个实体必须包含 title 和 type 字段description 字段可选但强烈建议提供
3. 确保 type 字段的值必须严格从 EntityTypes 列表中选择,不得创建新类型
4. 如果无法确定实体类型,不要强行归类,宁可不提取该实体
5. 不要输出任何解释或额外内容只输出JSON数组
6. 所有字段值不能包含HTML标签或其他代码
7. 如果实体有歧义需在description中说明具体指代
8. 若没有找到任何实体,返回空数组 []
## 实体提取规则
- Person: 真实或虚构的人物,包括历史人物、现代人物、文学角色等
- Organization: 公司、政府机构、团队、学校等组织实体
- Location: 地理位置、地标、国家、城市等
- Product: 商品、服务、品牌等商业产品
- Event: 事件、会议、节日、历史事件等
- Date: 日期、时间段、年代等时间相关信息
- Work: 书籍、电影、音乐、艺术作品等创作内容
- Concept: 抽象概念、思想、理论等
- Resource: 自然资源、信息资源、工具等
- Category: 分类、类别、领域等
- Operation: 操作、动作、方法、过程等
## 提取步骤
1. 仔细阅读文本,识别可能的实体
2. 对每个识别到的实体确定其最适合的实体类型必须从EntityTypes中选择
3. 为每个实体创建包含以下字段的JSON对象
- title: 实体的标准名称,不包含修饰词,如引号等
- type: 从EntityTypes中选择的实体类型
- description: 对该实体的简明中文描述,应基于文本内容
4. 验证每个实体的所有字段是否正确且格式化恰当
5. 将所有实体对象合并为一个JSON数组
6. 检查最终JSON是否有效并符合要求
## 示例
[输入]
文本: 《红楼梦》又名《石头记》是清代作家曹雪芹创作的中国古典四大名著之一被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间1743年前后是中国文学史上现实主义的高峰对后世影响深远。
[输出]
[
{
"title": "红楼梦",
"type": "Work",
"description": "红楼梦是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书"
},
{
"title": "石头记",
"type": "Work",
"description": "石头记是红楼梦的别名"
},
{
"title": "曹雪芹",
"type": "Person",
"description": "曹雪芹是清代作家红楼梦的作者创作了前80回"
},
{
"title": "高鹗",
"type": "Person",
"description": "高鹗是红楼梦后40回的续作者"
},
{
"title": "贾宝玉",
"type": "Person",
"description": "贾宝玉是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "林黛玉",
"type": "Person",
"description": "林黛玉是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "薛宝钗",
"type": "Person",
"description": "薛宝钗是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "金陵十二钗",
"type": "Concept",
"description": "金陵十二钗是红楼梦中以贾宝玉为中心的十二位主要女性角色"
},
{
"title": "乾隆年间",
"type": "Date",
"description": "乾隆年间指的是红楼梦成书的时间约1743年前后"
},
{
"title": "四大家族",
"type": "Concept",
"description": "四大家族是红楼梦中的贾、史、王、薛四个家族,是小说的背景"
},
{
"title": "中国文学史",
"type": "Category",
"description": "红楼梦被视为中国文学史中现实主义的高峰之作"
}
]
extract_relationships_prompt: |
## 任务
从用户提供的实体数组中,提取实体之间存在的明确关系,形成结构化的关系网络。
## 要求
1. 关系提取必须基于提供的文本内容,不得臆测不存在的关系
2. 结果必须以JSON数组格式输出每个关系为数组中的一个对象
3. 每个关系对象必须包含 source, target, description 和 strength 字段
4. 不要输出任何解释或额外内容只输出JSON数组
5. 若没有找到任何关系,返回空数组 []
## 关系提取规则
- 只有在文本中明确体现的关系才应被提取
- 源实体(source)和目标实体(target)必须是实体数组中已有的实体
- 关系描述(description)应简明扼要地说明两个实体间的具体关系
- 关系强度(strength)应根据以下标准确定:
* 10分直接创造/从属关系(如作者与作品、发明者与发明、母公司与子公司)
* 9分同一实体的不同表现形式如别名、曾用名
* 8分紧密相关且互相影响的关系如密切合作伙伴、家庭成员
* 7分明确但非直接的关系如作品中的角色、组织中的成员
* 6分间接关联且有明确联系如同事关系、相似产品
* 5分存在关联但较为松散如同一领域的不同概念
## 提取步骤
1. 仔细分析文本内容,确定哪些实体之间存在明确关系
2. 只考虑文本中明确提及的关系,不要臆测
3. 对每个找到的关系,确定:
- source: 关系的源实体标题(必须是实体列表中已有的实体)
- target: 关系的目标实体标题(必须是实体列表中已有的实体)
- description: 简明准确的关系描述(用中文表述)
- strength: 基于上述标准的关系强度5-10之间的整数
4. 检查每个关系是否双向:
- 如果关系是双向的(如"A是B的朋友"意味着"B也是A的朋友"),考虑是否需要创建反向关系
- 如果关系是单向的(如"A创作了B"),则只保留单向关系
5. 验证所有关系的一致性和合理性:
- 确保没有矛盾的关系如A同时是B的父亲和兄弟
- 确保关系描述与关系强度匹配
6. 将所有有效关系组织为JSON数组
## 示例
[输入]
实体: [
{
"title": "红楼梦",
"type": "Work",
"description": "红楼梦是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书"
},
{
"title": "石头记",
"type": "Work",
"description": "石头记是红楼梦的别名"
},
{
"title": "曹雪芹",
"type": "Person",
"description": "曹雪芹是清代作家红楼梦的作者创作了前80回"
},
{
"title": "高鹗",
"type": "Person",
"description": "高鹗是红楼梦后40回的续作者"
},
{
"title": "贾宝玉",
"type": "Person",
"description": "贾宝玉是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "林黛玉",
"type": "Person",
"description": "林黛玉是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "薛宝钗",
"type": "Person",
"description": "薛宝钗是红楼梦中的主要角色,爱情悲剧的主角之一"
},
{
"title": "四大家族",
"type": "Concept",
"description": "四大家族是红楼梦中的贾、史、王、薛四个家族,是小说的背景"
},
{
"title": "金陵十二钗",
"type": "Concept",
"description": "金陵十二钗是红楼梦中以贾宝玉为中心的十二位主要女性角色"
},
{
"title": "乾隆年间",
"type": "Date",
"description": "乾隆年间指的是红楼梦成书的时间约1743年前后"
},
{
"title": "中国文学史",
"type": "Category",
"description": "红楼梦被视为中国文学史中现实主义的高峰之作"
}
]
文本: 《红楼梦》又名《石头记》是清代作家曹雪芹创作的中国古典四大名著之一被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间1743年前后是中国文学史上现实主义的高峰对后世影响深远。
[输出]
[
{
"source": "曹雪芹",
"target": "红楼梦",
"description": "曹雪芹是红楼梦的主要作者创作了前80回",
"strength": 10
},
{
"source": "高鹗",
"target": "红楼梦",
"description": "高鹗是红楼梦后40回的续作者",
"strength": 10
},
{
"source": "红楼梦",
"target": "石头记",
"description": "石头记是红楼梦的别名",
"strength": 9
},
{
"source": "红楼梦",
"target": "中国文学史",
"description": "红楼梦被视为中国文学史中现实主义的高峰之作",
"strength": 7
},
{
"source": "贾宝玉",
"target": "林黛玉",
"description": "贾宝玉与林黛玉有深厚的爱情关系,是小说主线之一",
"strength": 8
},
{
"source": "贾宝玉",
"target": "薛宝钗",
"description": "贾宝玉与薛宝钗的关系是小说爱情悲剧主线的一部分",
"strength": 8
},
{
"source": "贾宝玉",
"target": "金陵十二钗",
"description": "贾宝玉是金陵十二钗故事的中心人物",
"strength": 8
},
{
"source": "红楼梦",
"target": "贾宝玉",
"description": "贾宝玉是红楼梦中的主要角色",
"strength": 7
},
{
"source": "红楼梦",
"target": "林黛玉",
"description": "林黛玉是红楼梦中的主要角色",
"strength": 7
},
{
"source": "红楼梦",
"target": "薛宝钗",
"description": "薛宝钗是红楼梦中的主要角色",
"strength": 7
},
{
"source": "红楼梦",
"target": "四大家族",
"description": "四大家族是红楼梦的背景设定",
"strength": 7
},
{
"source": "红楼梦",
"target": "金陵十二钗",
"description": "金陵十二钗是红楼梦中的重要概念",
"strength": 7
},
{
"source": "红楼梦",
"target": "乾隆年间",
"description": "红楼梦成书于乾隆年间约1743年前后",
"strength": 6
}
]
# 知识库配置
knowledge_base:
chunk_size: 512
chunk_overlap: 50
split_markers: ["\n\n", "\n", "。"]
image_processing:
enable_multimodal: true

273
dataset/README Normal file
View File

@ -0,0 +1,273 @@
# QA Dataset Sampling Tool
A comprehensive tool for sampling QA datasets and generating answers using OpenAI's GPT models. This tool helps you create high-quality question-answering datasets from large-scale collections like MS MARCO.
## Features
- **Smart Sampling**: Intelligently sample queries, documents, and relevance judgments from large datasets
- **Answer Generation**: Automatically generate high-quality answers using OpenAI's GPT models
- **Resume Support**: Continue interrupted answer generation from where it left off
- **Progress Tracking**: Real-time progress updates and statistics
- **Result Visualization**: Easy-to-read display of generated QA pairs with context
## Installation
### Prerequisites
- Python 3.7+
- OpenAI API key
### Install Dependencies
```bash
pip install pandas pyarrow openai
```
### Set Environment Variables
```bash
export OPENAI_API_KEY="your-openai-api-key"
# Optional: Use custom OpenAI endpoint
export OPENAI_BASE_URL="https://api.openai.com/v1"
```
### Parpare dataset
We provide pre-processed samples from popular QA datasets:
MarkrAI/msmarco_sample_autorag
## Quick Start
### 1. Sample Data from Large Dataset
First, sample a subset of queries, documents, and relevance judgments from your full dataset:
```bash
python dataset/qa_dataset.py sample \
--queries ~/dataset/mmarco-queries.parquet \
--corpus ~/dataset/mmarco-corpus.parquet \
--qrels ~/dataset/mmarco-qrels.parquet \
--nq 100 \
--output_dir ./dataset/samples
```
### 2. Generate Answers
Use OpenAI's GPT model to generate answers for the sampled questions:
```bash
python dataset/qa_dataset.py generate \
--input_dir ./dataset/samples \
--output_dir ./dataset/samples
```
### 3. View Results
Display the generated QA pairs with their context:
```bash
python dataset/qa_dataset.py show \
--input_dir ./dataset/samples \
-n 5
```
## Detailed Usage
### Sample Command
Create a representative sample from your full dataset.
```bash
python dataset/qa_dataset.py sample [OPTIONS]
```
**Required Parameters:**
- `--queries`: Path to queries parquet file (columns: `id`, `text`)
- `--corpus`: Path to corpus parquet file (columns: `id`, `text`)
- `--qrels`: Path to qrels parquet file (columns: `qid`, `pid`)
**Optional Parameters:**
- `--nq`: Number of queries to sample (default: 1000)
- `--output_dir`: Output directory for sampled data (default: ./save)
**Example:**
```bash
python dataset/qa_dataset.py sample \
--queries data/queries.parquet \
--corpus data/corpus.parquet \
--qrels data/qrels.parquet \
--nq 500 \
--output_dir ./my_sample
```
### Generate Command
Generate answers for sampled questions using OpenAI API.
```bash
python dataset/qa_dataset.py generate [OPTIONS]
```
**Required Parameters:**
- `--input_dir`: Directory containing sampled data (queries.parquet, corpus.parquet, qrels.parquet)
**Optional Parameters:**
- `--output_dir`: Output directory for generated answers (default: ./save)
**Features:**
- **Resume Support**: Automatically continues from where it left off if interrupted
- **Error Handling**: Retries failed API calls up to 3 times
- **Progress Saving**: Saves progress after each successful answer generation
**Example:**
```bash
python dataset/qa_dataset.py generate \
--input_dir ./my_sample \
--output_dir ./my_sample
```
### Show Command
Display generated QA pairs with full context.
```bash
python dataset/qa_dataset.py show [OPTIONS]
```
**Required Parameters:**
- `--input_dir`: Directory containing QA data (queries.parquet, corpus.parquet, qrels.parquet, qas.parquet, answers.parquet)
**Optional Parameters:**
- `-n`: Number of results to display (default: 5)
**Example:**
```bash
python dataset/qa_dataset.py show \
--input_dir ./my_sample \
-n 3
```
## Input Data Format
### Queries File (queries.parquet)
| Column | Type | Description |
|--------|------|-------------|
| id | string | Unique query identifier |
| text | string | The actual question text |
### Corpus File (corpus.parquet)
| Column | Type | Description |
|--------|------|-------------|
| id | string | Unique passage/document identifier |
| text | string | The passage/document content |
### Qrels File (qrels.parquet)
| Column | Type | Description |
|--------|------|-------------|
| qid | string | Query ID (matches queries.id) |
| pid | string | Passage ID (matches corpus.id) |
## Output Files
After running all commands, your output directory will contain:
### Sampled Data
- `queries.parquet`: Sampled queries subset
- `corpus.parquet`: Sampled documents subset
- `qrels.parquet`: Sampled relevance judgments
### Generated Answers
- `answers.parquet`: Generated answers with unique IDs
- `qas.parquet`: Question-answer mapping (qid → aid)
## Advanced Usage
### Custom OpenAI Configuration
You can use different OpenAI models or endpoints:
```bash
# Use GPT-4 Turbo
export OPENAI_API_KEY="your-key"
python dataset/qa_dataset.py generate --input_dir ./samples
# Use Azure OpenAI
export OPENAI_API_KEY="azure-key"
export OPENAI_BASE_URL="https://your-resource.openai.azure.com/openai/deployments/gpt-4"
python dataset/qa_dataset.py generate --input_dir ./samples
```
### Large Dataset Sampling
For very large datasets, consider sampling in batches:
```bash
# First batch
python dataset/qa_dataset.py sample --nq 1000 --output_dir ./batch1
python dataset/qa_dataset.py generate --input_dir ./batch1
# Second batch
python dataset/qa_dataset.py sample --nq 1000 --output_dir ./batch2
python dataset/qa_dataset.py generate --input_dir ./batch2
```
## Troubleshooting
### Common Issues
**1. OpenAI API Errors**
- Ensure your API key is set correctly: `echo $OPENAI_API_KEY`
- Check your API quota and billing status
- Verify network connectivity to OpenAI
**2. Memory Issues with Large Datasets**
- Reduce `--nq` parameter for smaller samples
- Ensure sufficient RAM for pandas operations
- Consider using smaller parquet files
**3. File Not Found Errors**
- Verify all input file paths are correct
- Ensure parquet files have correct column names
- Check file permissions
### Debug Mode
Enable verbose output by adding print statements or using Python debugger:
```bash
python -m pdb dataset/qa_dataset.py sample --queries ...
```
## Example Workflow
```bash
# 1. Setup environment
export OPENAI_API_KEY="sk-..."
# 2. Sample 200 queries from MS MARCO
python dataset/qa_dataset.py sample \
--queries ~/mmarco/queries.parquet \
--corpus ~/mmarco/corpus.parquet \
--qrels ~/mmarco/qrels.parquet \
--nq 200 \
--output_dir ./marco_sample
# 3. Generate answers (may take time depending on API rate limits)
python dataset/qa_dataset.py generate \
--input_dir ./marco_sample \
--output_dir ./marco_sample
# 4. Review results
python dataset/qa_dataset.py show \
--input_dir ./marco_sample \
-n 10
```
## Contributing
Feel free to submit issues and enhancement requests!
## License
MIT License - feel free to use this tool for your research and projects.

284
dataset/README_zh.md Normal file
View File

@ -0,0 +1,284 @@
# QA数据集采样工具
一个全面的QA数据集采样工具使用OpenAI的GPT模型生成答案。该工具帮助您从大规模数据集如MS MARCO创建高质量的问答数据集。
## 功能特性
- **智能采样**:智能地从大型数据集中采样查询、文档和相关性判断
- **答案生成**使用OpenAI的GPT模型自动生成高质量答案
- **断点续传**:支持中断后继续生成,从上次位置开始
- **进度跟踪**:实时进度更新和统计信息
- **结果可视化**:易于阅读的问答对展示,包含完整上下文
## 安装指南
### 系统要求
- Python 3.7+
- OpenAI API密钥
### 安装依赖
```bash
pip install pandas pyarrow openai
```
### 设置环境变量
```bash
export OPENAI_API_KEY="你的openai-api-key"
# 可选使用自定义OpenAI端点
export OPENAI_BASE_URL="https://api.openai.com/v1"
```
### 准备数据集
您可以使用任何符合格式要求的QA数据集或下载预处理好的样本
**使用HuggingFace/ModelScope样本**
我们提供了来自流行QA数据集的预处理样本
- MarkrAI/eli5_sample_autorag
- MarkrAI/msmarco_sample_autorag
- MarkrAI/triviaqa_sample_autorag
- gnekt/hotpotqa_small_sample_autorag
**使用您自己的数据集**
确保您的数据集包含以下文件:
- `queries.parquet`id, text
- `corpus.parquet`id, text
- `qrels.parquet`qid, pid
## 快速开始
### 1. 从大型数据集采样
首先,从完整数据集中采样查询、文档和相关性判断的子集:
```bash
python dataset/qa_dataset.py sample \
--queries ~/dataset/mmarco-queries.parquet \
--corpus ~/dataset/mmarco-corpus.parquet \
--qrels ~/dataset/mmarco-qrels.parquet \
--nq 100 \
--output_dir ./dataset/samples
```
### 2. 生成答案
使用OpenAI的GPT模型为采样的问答生成答案
```bash
python dataset/qa_dataset.py generate \
--input_dir ./dataset/samples \
--output_dir ./dataset/samples
```
### 3. 查看结果
展示生成的问答对及其上下文:
```bash
python dataset/qa_dataset.py show \
--input_dir ./dataset/samples \
-n 5
```
## 详细使用说明
### 采样命令
从完整数据集中创建代表性样本。
```bash
python dataset/qa_dataset.py sample [选项]
```
**必需参数:**
- `--queries`查询parquet文件路径`id`, `text`
- `--corpus`语料库parquet文件路径`id`, `text`
- `--qrels`相关性判断parquet文件路径`qid`, `pid`
**可选参数:**
- `--nq`要采样的查询数量默认1000
- `--output_dir`:采样数据输出目录(默认:./save
**示例:**
```bash
python dataset/qa_dataset.py sample \
--queries data/queries.parquet \
--corpus data/corpus.parquet \
--qrels data/qrels.parquet \
--nq 500 \
--output_dir ./my_sample
```
### 生成命令
使用OpenAI API为采样问题生成答案。
```bash
python dataset/qa_dataset.py generate [选项]
```
**必需参数:**
- `--input_dir`包含采样数据的目录queries.parquet, corpus.parquet, qrels.parquet
**可选参数:**
- `--output_dir`:生成答案的输出目录(默认:./save
**特性:**
- **断点续传**:中断后自动从上次位置继续
- **错误处理**API调用失败自动重试3次
- **进度保存**:每成功生成一个答案就保存进度
**示例:**
```bash
python dataset/qa_dataset.py generate \
--input_dir ./my_sample \
--output_dir ./my_sample
```
### 展示命令
展示生成的问答对及完整上下文。
```bash
python dataset/qa_dataset.py show [选项]
```
**必需参数:**
- `--input_dir`包含QA数据的目录queries.parquet, corpus.parquet, qrels.parquet, qas.parquet, answers.parquet
**可选参数:**
- `-n`要展示的结果数量默认5
**示例:**
```bash
python dataset/qa_dataset.py show \
--input_dir ./my_sample \
-n 3
```
## 输入数据格式
### 查询文件 (queries.parquet)
| 列名 | 类型 | 描述 |
|------|------|------|
| id | string | 唯一查询标识符 |
| text | string | 实际的问题文本 |
### 语料库文件 (corpus.parquet)
| 列名 | 类型 | 描述 |
|------|------|------|
| id | string | 唯一段落/文档标识符 |
| text | string | 段落/文档内容 |
### 相关性判断文件 (qrels.parquet)
| 列名 | 类型 | 描述 |
|------|------|------|
| qid | string | 查询ID匹配queries.id |
| pid | string | 段落ID匹配corpus.id |
## 输出文件
运行所有命令后,输出目录将包含:
### 采样数据
- `queries.parquet`:采样的查询子集
- `corpus.parquet`:采样的文档子集
- `qrels.parquet`:采样的相关性判断
### 生成的答案
- `answers.parquet`生成的答案含唯一ID
- `qas.parquet`问答映射qid → aid
## 高级用法
### 自定义OpenAI配置
您可以使用不同的OpenAI模型或端点
```bash
# 使用GPT-4 Turbo
export OPENAI_API_KEY="你的密钥"
python dataset/qa_dataset.py generate --input_dir ./samples
# 使用Azure OpenAI
export OPENAI_API_KEY="azure密钥"
export OPENAI_BASE_URL="https://你的资源.openai.azure.com/openai/deployments/gpt-4"
python dataset/qa_dataset.py generate --input_dir ./samples
```
### 大型数据集采样
对于非常大的数据集,建议分批采样:
```bash
# 第一批
python dataset/qa_dataset.py sample --nq 1000 --output_dir ./batch1
python dataset/qa_dataset.py generate --input_dir ./batch1
# 第二批
python dataset/qa_dataset.py sample --nq 1000 --output_dir ./batch2
python dataset/qa_dataset.py generate --input_dir ./batch2
```
## 故障排除
### 常见问题
**1. OpenAI API错误**
- 确保API密钥设置正确`echo $OPENAI_API_KEY`
- 检查API配额和账单状态
- 验证与OpenAI的网络连接
**2. 大数据集内存问题**
- 减小`--nq`参数以获得更小的样本
- 确保pandas操作有足够的RAM
- 考虑使用更小的parquet文件
**3. 文件未找到错误**
- 验证所有输入文件路径是否正确
- 确保parquet文件有正确的列名
- 检查文件权限
### 调试模式
通过添加打印语句或使用Python调试器启用详细输出
```bash
python -m pdb dataset/qa_dataset.py sample --queries ...
```
## 示例工作流
```bash
# 1. 设置环境
export OPENAI_API_KEY="sk-..."
# 2. 从MS MARCO采样200个查询
python dataset/qa_dataset.py sample \
--queries ~/mmarco/queries.parquet \
--corpus ~/mmarco/corpus.parquet \
--qrels ~/mmarco/qrels.parquet \
--nq 200 \
--output_dir ./marco_sample
# 3. 生成答案根据API速率限制可能需要一些时间
python dataset/qa_dataset.py generate \
--input_dir ./marco_sample \
--output_dir ./marco_sample
# 4. 查看结果
python dataset/qa_dataset.py show \
--input_dir ./marco_sample \
-n 10
```
## 贡献
欢迎提交问题和功能增强请求!
## 许可证
MIT许可证 - 可自由用于研究和项目。

381
dataset/qa_dataset.py Normal file
View File

@ -0,0 +1,381 @@
"""
QA Dataset Sampling Tool
```
pip install pandas pyarrow
pip install openai
```
# 采样数据
python dataset/qa_dataset.py sample \
--queries ~/dataset/mmarco-queries.parquet \
--corpus ~/dataset/mmarco-corpus.parquet \
--qrels ~/dataset/mmarco-qrels.parquet \
--nq 100 \
--output_dir ./dataset/samples
# 生成答案(基于采样结果)
python dataset/qa_dataset.py generate \
--input_dir ./dataset/samples \
--output_dir ./dataset/samples
# 展示结果
python dataset/qa_dataset.py show \
--input_dir ./dataset/samples \
-n 1
"""
import os
from pathlib import Path
import argparse
import pandas as pd
import openai
def read_parquet(path):
return pd.read_parquet(path)
def save_to_parquet(df: pd.DataFrame, path: str):
"""Save DataFrame to parquet file"""
Path(path).parent.mkdir(parents=True, exist_ok=True)
df.to_parquet(path)
print(f"Saved to {path}")
def print_stats(df: pd.DataFrame, name: str):
"""Print statistics of a DataFrame"""
print(f"\n{name} Statistics:")
print(f"- Total records: {len(df)}")
if "id" in df.columns:
print(f"- Unique ids: {df['id'].nunique()}")
if "qid" in df.columns:
print(f"- Unique qids: {df['qid'].nunique()}")
if "pid" in df.columns:
print(f"- Unique pids: {df['pid'].nunique()}")
def sample_data(
queries: pd.DataFrame, corpus: pd.DataFrame, qrels: pd.DataFrame, nq=1000
):
"""
Sample data from the dataset with validation checks.
Args:
queries: DataFrame with qid and text columns (one-to-one)
corpus: DataFrame with pid and text columns (one-to-one)
qrels: DataFrame with qid and pid columns (many-to-many)
nq: Number of queries to sample (default: 1000)
Returns:
Tuple of (sampled_queries, sampled_corpus, sampled_qrels)
"""
# 1. Filter qrels to only include qids that exist in queries
valid_qids = set(queries["id"])
qrels = qrels[qrels["qid"].isin(valid_qids)]
# 2. Filter qrels to only include pids that exist in corpus
valid_pids = set(corpus["id"])
qrels = qrels[qrels["pid"].isin(valid_pids)]
# 3. Sample queries (ensure we have enough qrels samples for each)
# Get qids with most associated pids to ensure diversity
qid_counts = qrels["qid"].value_counts()
sampled_qids = qid_counts.nlargest(min(nq, len(qid_counts))).index
# 4. Get all pids associated with sampled qids
sampled_qrels = qrels[qrels["qid"].isin(sampled_qids)]
sampled_pids = set(sampled_qrels["pid"])
# 5. Add extra pids from corpus for redundancy (20% of sampled pids)
extra_pids = set(corpus["id"].sample(int(0.2 * len(sampled_pids))))
all_pids = sampled_pids.union(extra_pids)
# 6. Create final sampled datasets
sampled_queries = queries[queries["id"].isin(sampled_qids)]
sampled_corpus = corpus[corpus["id"].isin(all_pids)]
return sampled_queries, sampled_corpus, sampled_qrels
class QAAnsweringSystem:
def __init__(
self, queries: pd.DataFrame, corpus: pd.DataFrame, qrels: pd.DataFrame
):
"""
Initialize QA system with data
Args:
queries: DataFrame with qid and text columns
corpus: DataFrame with pid and text columns
qrels: DataFrame with qid and pid mapping
"""
self.queries = queries
self.corpus = corpus
self.qrels = qrels
self.client = openai.Client(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
# Create lookup dictionaries
self.qid_to_text = dict(zip(queries["id"], queries["text"]))
self.pid_to_text = dict(zip(corpus["id"], corpus["text"]))
self.qid_to_pids = qrels.groupby("qid")["pid"].apply(list).to_dict()
def get_context_for_qid(self, qid: str) -> str:
"""
Get all relevant text for a query ID
Args:
qid: Query ID to search for
Returns:
Combined context text from all related passages
"""
if qid not in self.qid_to_pids:
raise ValueError("Question ID not found")
context_parts = []
print(f"Context for Question ID {qid}: {self.qid_to_pids[qid]}")
for pid in self.qid_to_pids[qid]:
if pid in self.pid_to_text:
context_parts.append(self.pid_to_text[pid])
return "\n\n".join(context_parts)
def answer_question(self, qid: str, model: str = "gpt-4o-2024-05-13") -> str:
"""
Use OpenAI API to answer question based on qid context
Args:
qid: Query ID to answer
model: OpenAI model to use
Returns:
Generated answer from LLM
"""
if qid not in self.qid_to_text:
raise ValueError("Question ID not found")
question = self.qid_to_text[qid]
context = self.get_context_for_qid(qid)
if not context:
raise ValueError("No context found for this question")
prompt = f"""Answer the question based on the context below. Keep the answer concise.
Question: {question}
Context: {context}
Answer:"""
response = self.client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
return response.choices[0].message.content
def sample_command(args):
"""Handle sample command"""
# Load data
print("Loading data...")
queries = read_parquet(args.queries)
corpus = read_parquet(args.corpus)
qrels = read_parquet(args.qrels)
# Print original stats
print("\nOriginal Dataset Statistics:")
print_stats(queries, "Queries")
print_stats(corpus, "Corpus")
print_stats(qrels, "Qrels")
# Sample data
print(f"\nSampling {args.nq} queries...")
sampled_queries, sampled_corpus, sampled_qrels = sample_data(
queries, corpus, qrels, args.nq
)
# Print sampled stats
print("\nSampled Dataset Statistics:")
print_stats(sampled_queries, "Sampled Queries")
print_stats(sampled_corpus, "Sampled Corpus")
print_stats(sampled_qrels, "Sampled Qrels")
# Save sampled data
print("\nSaving sampled data...")
save_to_parquet(sampled_queries, f"{args.output_dir}/queries.parquet")
save_to_parquet(sampled_corpus, f"{args.output_dir}/corpus.parquet")
save_to_parquet(sampled_qrels, f"{args.output_dir}/qrels.parquet")
print("\nSampling completed successfully!")
def generate_answers(input_dir: str, output_dir: str, max_retries: int = 3):
"""
Generate answers for sampled queries with resume support
Args:
input_dir: Directory containing sampled queries/corpus/qrels
output_dir: Directory to save answer files
max_retries: Maximum retry attempts for failed queries
"""
print("\nLoading sampled data...")
queries = read_parquet(f"{input_dir}/queries.parquet")
corpus = read_parquet(f"{input_dir}/corpus.parquet")
qrels = read_parquet(f"{input_dir}/qrels.parquet")
# Try to load existing answers if any
answers_path = f"{output_dir}/answers.parquet"
qa_pairs_path = f"{output_dir}/qas.parquet"
try:
existing_answers = read_parquet(answers_path)
existing_qas = read_parquet(qa_pairs_path)
processed_qids = set(existing_qas["qid"])
print(f"\nFound {len(processed_qids)} previously processed queries")
except (FileNotFoundError, KeyError):
print("No existing answers found, use empty state")
existing_answers = pd.DataFrame(columns=["id", "text"])
existing_qas = pd.DataFrame(columns=["qid", "aid"])
processed_qids = set()
qa_system = QAAnsweringSystem(queries, corpus, qrels)
answers = existing_answers.to_dict("records")
qa_pairs = existing_qas.to_dict("records")
answer_id_counter = len(answers) + 1
for qid in queries["id"]:
if qid in processed_qids:
continue
retry_count = 0
while retry_count <= max_retries:
try:
answer_text = qa_system.answer_question(qid)
aid = answer_id_counter
answers.append({"id": aid, "text": answer_text})
qa_pairs.append({"qid": qid, "aid": aid})
answer_id_counter += 1
# Save progress after each successful answer
save_to_parquet(pd.DataFrame(answers), answers_path)
save_to_parquet(pd.DataFrame(qa_pairs), qa_pairs_path)
print(f"Processed qid: {qid}")
break
except (openai.APIError, openai.APIConnectionError) as e:
retry_count += 1
if retry_count > max_retries:
print(
f"\nFailed to process qid {qid} after {max_retries} attempts: {str(e)}"
)
# Save failed state
save_to_parquet(pd.DataFrame(answers), answers_path)
save_to_parquet(pd.DataFrame(qa_pairs), qa_pairs_path)
else:
print(f"\nRetry {retry_count} for qid {qid}...")
print("\nAnswer generation completed!")
print(f"Total queries: {len(queries)}")
print(f"Successfully processed: {len(qa_pairs)}")
print(f"Failed queries: {len(queries) - len(qa_pairs)}")
def show_results(input_dir: str, n: int = 5):
"""
Show n random results with question, context and answer
Args:
input_dir: Directory containing the QA data
n: Number of results to show (default: 5)
"""
print(f"\nShowing {n} random results:")
# Load data
queries = read_parquet(f"{input_dir}/queries.parquet")
corpus = read_parquet(f"{input_dir}/corpus.parquet")
qrels = read_parquet(f"{input_dir}/qrels.parquet")
qa_pairs = read_parquet(f"{input_dir}/qas.parquet")
answers = read_parquet(f"{input_dir}/answers.parquet")
# Create QA system for context lookup
qa_system = QAAnsweringSystem(queries, corpus, qrels)
# Get first n QA pairs
for _, row in qa_pairs.sample(n).iterrows():
qid = row["qid"]
aid = row["aid"]
# Get question
question = qa_system.qid_to_text[qid]
# Get context
context = qa_system.get_context_for_qid(qid)
# Get answer
answer = answers[answers["id"] == aid]["text"].values[0]
print("\n" + "=" * 50)
print(f"Question (qid={qid}):\n{question}")
print("\nContext:")
print(context)
print(f"\nAnswer (aid={aid}):\n{answer}")
print("=" * 50 + "\n")
def main():
# Set up command line arguments
parser = argparse.ArgumentParser(description="QA Dataset Tool")
subparsers = parser.add_subparsers(dest="command", required=True)
# Sample command
sample_parser = subparsers.add_parser("sample", help="Sample dataset")
sample_parser.add_argument(
"--queries", type=str, required=True, help="Path to queries parquet file"
)
sample_parser.add_argument(
"--corpus", type=str, required=True, help="Path to corpus parquet file"
)
sample_parser.add_argument(
"--qrels", type=str, required=True, help="Path to qrels parquet file"
)
sample_parser.add_argument(
"--nq", type=int, default=1000, help="Number of queries to sample"
)
sample_parser.add_argument(
"--output_dir", type=str, default="./save", help="Output directory"
)
sample_parser.set_defaults(func=sample_command)
# Generate command
generate_parser = subparsers.add_parser("generate", help="Generate answers")
generate_parser.add_argument(
"--input_dir", type=str, required=True, help="Directory with sampled data"
)
generate_parser.add_argument(
"--output_dir", type=str, default="./save", help="Output directory"
)
generate_parser.set_defaults(
func=lambda args: generate_answers(args.input_dir, args.output_dir)
)
# Show command
show_parser = subparsers.add_parser("show", help="Show QA results")
show_parser.add_argument(
"--input_dir", type=str, required=True, help="Directory with QA data"
)
show_parser.add_argument(
"-n", type=int, default=5, help="Number of results to show (default: 5)"
)
show_parser.set_defaults(func=lambda args: show_results(args.input_dir, args.n))
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

BIN
dataset/samples/qas.parquet Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

198
docker-compose.yml Normal file
View File

@ -0,0 +1,198 @@
services:
app:
image: wechatopenai/weknora-app:latest
container_name: WeKnora-app
ports:
- "8080:8080"
volumes:
- data-files:/data/files
- ./config:/app/config
environment:
- GIN_MODE=${GIN_MODE}
- DB_DRIVER=postgres
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
- TZ=Asia/Shanghai
- OTEL_EXPORTER_OTLP_ENDPOINT=jaeger:4317
- OTEL_SERVICE_NAME=WeKnora
- OTEL_TRACES_EXPORTER=otlp
- OTEL_METRICS_EXPORTER=none
- OTEL_LOGS_EXPORTER=none
- OTEL_PROPAGATORS=tracecontext,baggage
- RETRIEVE_DRIVER=${RETRIEVE_DRIVER}
- ELASTICSEARCH_ADDR=${ELASTICSEARCH_ADDR}
- ELASTICSEARCH_USERNAME=${ELASTICSEARCH_USERNAME}
- ELASTICSEARCH_PASSWORD=${ELASTICSEARCH_PASSWORD}
- ELASTICSEARCH_INDEX=${ELASTICSEARCH_INDEX}
- DOCREADER_ADDR=docreader:50051
- STORAGE_TYPE=${STORAGE_TYPE}
- LOCAL_STORAGE_BASE_DIR=${LOCAL_STORAGE_BASE_DIR}
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY_ID=${MINIO_ACCESS_KEY_ID:-minioadmin}
- MINIO_SECRET_ACCESS_KEY=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
- STREAM_MANAGER_TYPE=${STREAM_MANAGER_TYPE}
- REDIS_ADDR=redis:6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_DB=${REDIS_DB}
- REDIS_PREFIX=${REDIS_PREFIX}
- ENABLE_GRAPH_RAG=${ENABLE_GRAPH_RAG}
- TENANT_AES_KEY=${TENANT_AES_KEY}
- CONCURRENCY_POOL_SIZE=${CONCURRENCY_POOL_SIZE:-5}
- INIT_LLM_MODEL_NAME=${INIT_LLM_MODEL_NAME}
- INIT_LLM_MODEL_BASE_URL=${INIT_LLM_MODEL_BASE_URL}
- INIT_LLM_MODEL_API_KEY=${INIT_LLM_MODEL_API_KEY}
- INIT_EMBEDDING_MODEL_NAME=${INIT_EMBEDDING_MODEL_NAME}
- INIT_EMBEDDING_MODEL_BASE_URL=${INIT_EMBEDDING_MODEL_BASE_URL}
- INIT_EMBEDDING_MODEL_API_KEY=${INIT_EMBEDDING_MODEL_API_KEY}
- INIT_EMBEDDING_MODEL_DIMENSION=${INIT_EMBEDDING_MODEL_DIMENSION}
- INIT_EMBEDDING_MODEL_ID=${INIT_EMBEDDING_MODEL_ID}
- INIT_RERANK_MODEL_NAME=${INIT_RERANK_MODEL_NAME}
- INIT_RERANK_MODEL_BASE_URL=${INIT_RERANK_MODEL_BASE_URL}
- INIT_RERANK_MODEL_API_KEY=${INIT_RERANK_MODEL_API_KEY}
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
minio:
condition: service_started
networks:
- WeKnora-network
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
minio:
image: minio/minio:latest
container_name: WeKnora-minio
ports:
- "${MINIO_PORT:-9000}:9000"
- "${MINIO_CONSOLE_PORT:-9001}:9001"
environment:
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY_ID:-minioadmin}
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
command: server --console-address ":9001" /data
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
networks:
- WeKnora-network
frontend:
image: wechatopenai/weknora-ui:latest
container_name: WeKnora-frontend
ports:
- "80:80"
depends_on:
- app
networks:
- WeKnora-network
restart: unless-stopped
docreader:
image: wechatopenai/weknora-docreader:latest
container_name: WeKnora-docreader
ports:
- "50051:50051"
environment:
- COS_SECRET_ID=${COS_SECRET_ID}
- COS_SECRET_KEY=${COS_SECRET_KEY}
- COS_REGION=${COS_REGION}
- COS_BUCKET_NAME=${COS_BUCKET_NAME}
- COS_APP_ID=${COS_APP_ID}
- COS_PATH_PREFIX=${COS_PATH_PREFIX}
- COS_ENABLE_OLD_DOMAIN=${COS_ENABLE_OLD_DOMAIN}
- VLM_MODEL_BASE_URL=${VLM_MODEL_BASE_URL}
- VLM_MODEL_NAME=${VLM_MODEL_NAME}
- VLM_MODEL_API_KEY=${VLM_MODEL_API_KEY}
- STORAGE_TYPE=${STORAGE_TYPE}
- MINIO_PUBLIC_ENDPOINT=http://localhost:${MINIO_PORT:-9000}
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY_ID=${MINIO_ACCESS_KEY_ID:-minioadmin}
- MINIO_SECRET_ACCESS_KEY=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
- MINIO_USE_SSL=${MINIO_USE_SSL}
- WEB_PROXY=${WEB_PROXY}
networks:
- WeKnora-network
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "6831:6831/udp" # Jaeger Thrift接收器
- "6832:6832/udp" # Jaeger Thrift接收器(Compact)
- "5778:5778" # 配置端口
- "16686:16686" # Web UI
- "4317:4317" # OTLP gRPC接收器
- "4318:4318" # OTLP HTTP接收器
- "14250:14250" # 接收模型端口
- "14268:14268" # Jaeger HTTP接收器
- "9411:9411" # Zipkin兼容性端口
environment:
- COLLECTOR_OTLP_ENABLED=true
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
volumes:
- jaeger_data:/var/lib/jaeger # 持久化 Jaeger 数据
networks:
- WeKnora-network
restart: unless-stopped
# 修改的PostgreSQL配置
postgres:
image: paradedb/paradedb:latest
container_name: WeKnora-postgres
ports:
- "${DB_PORT}:5432"
environment:
- POSTGRES_USER=${DB_USER}
# NOCC:hardcode-password(工具误报)
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./migrations/paradedb:/docker-entrypoint-initdb.d
networks:
- WeKnora-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s # 增加时间间隔
timeout: 10s # 增加超时时间
retries: 3 # 减少重试次数,让失败更快反馈
start_period: 30s # 给予初始启动更多时间
restart: unless-stopped
# 添加停机时的优雅退出时间
stop_grace_period: 1m
redis:
image: redis:7.0-alpine
container_name: WeKnora-redis
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
restart: always
networks:
- WeKnora-network
networks:
WeKnora-network:
driver: bridge
volumes:
postgres-data:
data-files:
jaeger_data:
redis_data:
minio_data:

74
docker/Dockerfile.app Normal file
View File

@ -0,0 +1,74 @@
# Build stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
# Install dependencies
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk add --no-cache git build-base
# 通过构建参数接收敏感信息
ARG GOPRIVATE_ARG
ARG GOPROXY_ARG
ARG GOSUMDB_ARG=off
# 设置Go环境变量
ENV GOPRIVATE=${GOPRIVATE_ARG}
ENV GOPROXY=${GOPROXY_ARG}
ENV GOSUMDB=${GOSUMDB_ARG}
# Copy go mod and sum files
COPY go.mod go.sum ./
RUN go mod download
ENV CGO_ENABLED=1
# Install migrate tool
RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Copy source code
COPY . .
# Build the application
RUN make build-prod
# Final stage
FROM alpine:3.17
WORKDIR /app
# Install runtime dependencies
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk update && apk upgrade && \
apk add --no-cache build-base postgresql-client mysql-client ca-certificates tzdata sed curl bash supervisor vim wget
# Copy the binary from the builder stage
COPY --from=builder /app/WeKnora .
COPY --from=builder /app/config ./config
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/migrations ./migrations
COPY --from=builder /app/dataset/samples ./dataset/samples
# Copy migrate tool from builder stage
COPY --from=builder /go/bin/migrate /usr/local/bin/
COPY --from=builder /go/pkg/mod/github.com/yanyiwu /go/pkg/mod/github.com/yanyiwu/
# Make scripts executable
RUN chmod +x ./scripts/*.sh
# Setup supervisor configuration
RUN mkdir -p /etc/supervisor.d/
COPY docker/config/supervisord.conf /etc/supervisor.d/supervisord.conf
# Expose ports
EXPOSE 8080
# Set environment variables
ENV CGO_ENABLED=1
# Create a non-root user and switch to it
RUN mkdir -p /data/files && \
adduser -D -g '' appuser && \
chown -R appuser:appuser /app /data/files
# Run supervisor instead of direct application start
CMD ["supervisord", "-c", "/etc/supervisor.d/supervisord.conf"]

135
docker/Dockerfile.docreader Normal file
View File

@ -0,0 +1,135 @@
# =========================
# 构建阶段
# =========================
FROM python:3.10.18-bookworm AS builder
# 切换 apt 源到清华
RUN sed -i 's@http://deb.debian.org@https://mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources && \
sed -i 's@http://security.debian.org@https://mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources
WORKDIR /app
# 安装构建依赖
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
libjpeg-dev \
zlib1g-dev \
libpq-dev \
libffi-dev \
libgl1 \
libglib2.0-0 \
wget \
antiword \
curl \
unzip \
&& rm -rf /var/lib/apt/lists/*
# 安装 protoc
RUN curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip && \
unzip protoc-3.19.4-linux-x86_64.zip -d /usr/local && \
chmod +x /usr/local/bin/protoc && \
rm protoc-3.19.4-linux-x86_64.zip
# 复制依赖文件
COPY services/docreader/requirements.txt .
# 安装依赖
RUN pip cache purge && pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 预下载 PP-OCRv5 模型
RUN mkdir -p /root/.paddlex/official_models && \
wget https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/PP-OCRv5_server_det_infer.tar \
-O /root/.paddlex/official_models/PP-OCRv5_server_det_infer.tar && \
wget https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/PP-OCRv5_server_rec_infer.tar \
-O /root/.paddlex/official_models/PP-OCRv5_server_rec_infer.tar && \
tar -xf /root/.paddlex/official_models/PP-OCRv5_server_det_infer.tar -C /root/.paddlex/official_models/ && \
tar -xf /root/.paddlex/official_models/PP-OCRv5_server_rec_infer.tar -C /root/.paddlex/official_models/ && \
rm -rf /root/.paddlex/official_models/PP-OCRv5_server_det_infer.tar /root/.paddlex/official_models/PP-OCRv5_server_rec_infer.tar
# 复制源代码和生成脚本
COPY services/docreader/src/ /app/src/
COPY services/docreader/scripts/ /app/scripts/
# 确保模型目录存在
RUN ls -la /root/.paddlex/official_models
# 生成 protobuf 代码
RUN chmod +x /app/scripts/generate_proto.sh && bash /app/scripts/generate_proto.sh
# =========================
# 运行阶段
# =========================
FROM python:3.10.18-bookworm AS runner
# 切换 apt 源到清华
RUN sed -i 's@http://deb.debian.org@https://mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources && \
sed -i 's@http://security.debian.org@https://mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources
WORKDIR /app
# 安装运行时依赖
RUN apt-get update && apt-get install -y \
libjpeg62-turbo \
libpq5 \
wget \
gnupg \
libgl1 \
libglib2.0-0 \
antiword \
supervisor \
vim \
tar \
dpkg \
libxinerama1 \
libfontconfig1 \
libdbus-glib-1-2 \
libcairo2 \
libcups2 \
libglu1-mesa \
libsm6 \
libreoffice \
&& rm -rf /var/lib/apt/lists/*
# # 下载并安装 LibreOffice区分架构
# RUN mkdir -p /tmp/libreoffice && cd /tmp/libreoffice && \
# if [ "$(uname -m)" = "x86_64" ]; then \
# wget https://mirrors.tuna.tsinghua.edu.cn/libreoffice/libreoffice/stable/25.2.5/deb/x86_64/LibreOffice_25.2.5_Linux_x86-64_deb.tar.gz && \
# tar -xzf LibreOffice_25.2.5_Linux_x86-64_deb.tar.gz && \
# cd LibreOffice_25.2.5.2_Linux_x86-64_deb/DEBS && dpkg -i *.deb; \
# elif [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then \
# wget https://mirrors.aliyun.com/libreoffice/testing/25.8.0/deb/aarch64/LibreOffice_25.8.0.3_Linux_aarch64_deb.tar.gz && \
# tar -xzf LibreOffice_25.8.0.3_Linux_aarch64_deb.tar.gz && \
# cd LibreOffice_25.8.0.3_Linux_aarch64_deb/DEBS && dpkg -i *.deb; \
# else \
# echo "Unsupported architecture: $(uname -m)" && exit 1; \
# fi && \
# cd / && rm -rf /tmp/libreoffice
# 设置 LibreOffice 环境变量
# RUN echo 'export LIBREOFFICE_PATH=/opt/libreoffice25.2/program/soffice' >> /etc/environment;
# 从构建阶段复制已安装的依赖和生成的代码
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY --from=builder /root/.paddlex/official_models /root/.paddlex/official_models
COPY --from=builder /app/src /app/src
# 安装 Playwright 浏览器
RUN python -m playwright install webkit
RUN python -m playwright install-deps webkit
# 设置 Python 路径
ENV PYTHONPATH=/app/src
RUN cd /app/src && python -m download_deps
# 创建supervisor配置
RUN mkdir -p /etc/supervisor/conf.d
COPY services/docreader/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# 暴露 gRPC 端口
EXPOSE 50051
# 使用supervisor启动服务
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -0,0 +1,31 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/var/run/supervisord.pid
user=root
[program:WeKnora]
command=/app/WeKnora
directory=/app
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/WeKnora.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
environment=CGO_ENABLED=1
user=appuser
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

View File

@ -0,0 +1,29 @@
version: "3.8"
services:
minio:
image: minio/minio:latest
container_name: WeKnora-minio
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
environment:
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY_ID}
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_ACCESS_KEY}
command: server --console-address ":9001" /data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
networks:
- WeKnora-network
volumes:
minio-data:
networks:
WeKnora-network:
external: true

2105
docs/API.md Normal file

File diff suppressed because it is too large Load Diff

104
docs/QA.md Normal file
View File

@ -0,0 +1,104 @@
# 常见问题
## 1. 如何查看日志?
```bash
# 查看 主服务 日志
docker exec -it WeKnora-app tail -f /var/log/WeKnora.log
# 查看 文档解析模块 日志
docker exec -it WeKnora-docreader tail -f /var/log/docreader.log
```
## 2. 如何启动和停止服务?
```bash
# 启动服务
./scripts/start_all.sh
# 停止服务
./scripts/start_all.sh --stop
# 清空数据库
./scripts/start_all.sh --stop && make clean-db
```
## 3. 服务启动后无法正常上传文档?
通常是Embedding模型和对话模型没有正确被设置导致。按照以下步骤进行排查
1. 查看`.env`配置中的模型信息是否配置完整其中如果使用ollama访问本地模型需要确保本地ollama服务正常运行同时在`.env`中的如下环境变量需要正确设置:
```bash
# LLM Model
INIT_LLM_MODEL_NAME=your_llm_model
# Embedding Model
INIT_EMBEDDING_MODEL_NAME=your_embedding_model
# Embedding模型向量维度
INIT_EMBEDDING_MODEL_DIMENSION=your_embedding_model_dimension
# Embedding模型的ID通常是一个字符串
INIT_EMBEDDING_MODEL_ID=your_embedding_model_id
```
如果是通过remote api访问模型则需要额外提供对应的`BASE_URL`和`API_KEY`:
```bash
# LLM模型的访问地址
INIT_LLM_MODEL_BASE_URL=your_llm_model_base_url
# LLM模型的API密钥如果需要身份验证可以设置
INIT_LLM_MODEL_API_KEY=your_llm_model_api_key
# Embedding模型的访问地址
INIT_EMBEDDING_MODEL_BASE_URL=your_embedding_model_base_url
# Embedding模型的API密钥如果需要身份验证可以设置
INIT_EMBEDDING_MODEL_API_KEY=your_embedding_model_api_key
```
当需要重排序功能时需要额外配置Rerank模型具体配置如下
```bash
# 使用的Rerank模型名称
INIT_RERANK_MODEL_NAME=your_rerank_model_name
# Rerank模型的访问地址
INIT_RERANK_MODEL_BASE_URL=your_rerank_model_base_url
# Rerank模型的API密钥如果需要身份验证可以设置
INIT_RERANK_MODEL_API_KEY=your_rerank_model_api_key
```
2. 查看主服务日志,是否有`ERROR`日志输出
## 4. 如何开启多模态功能?
1. 确保 `.env` 如下配置被正确设置:
```bash
# VLM_MODEL_NAME 使用的多模态模型名称
VLM_MODEL_NAME=your_vlm_model_name
# VLM_MODEL_BASE_URL 使用的多模态模型访问地址
VLM_MODEL_BASE_URL=your_vlm_model_base_url
# VLM_MODEL_API_KEY 使用的多模态模型API密钥
VLM_MODEL_API_KEY=your_vlm_model_api_key
```
多模态大模型当前仅支持remote api访问固需要提供`VLM_MODEL_BASE_URL`和`VLM_MODEL_API_KEY`
2. 解析后的文件需要上传到COS中确保 `.env``COS` 信息正确设置:
```bash
# 腾讯云COS的访问密钥ID
COS_SECRET_ID=your_cos_secret_id
# 腾讯云COS的密钥
COS_SECRET_KEY=your_cos_secret_key
# 腾讯云COS的区域例如 ap-guangzhou
COS_REGION=your_cos_region
# 腾讯云COS的桶名称
COS_BUCKET_NAME=your_cos_bucket_name
# 腾讯云COS的应用ID
COS_APP_ID=your_cos_app_id
# 腾讯云COS的路径前缀用于存储文件
COS_PATH_PREFIX=your_cos_path_prefix
```
重要务必将COS中文件的权限设置为**公有读**,否则文档解析模块无法正常解析文件
3. 查看文档解析模块日志查看OCR和Caption是否正确解析和打印
## P.S.
如果以上方式未解决问题请在issue中描述您的问题并提供必要的日志信息辅助我们进行问题排查

BIN
docs/images/answer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

BIN
docs/images/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

BIN
docs/images/graph1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

BIN
docs/images/graph2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

BIN
docs/images/knowledges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/images/pipeline.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/images/qa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,190 @@
### 如何集成新的向量数据库
本文提供了向 WeKnora 项目添加新向量数据库支持的完整指南。通过实现标准化接口和遵循结构化流程,开发者可以高效地集成自定义向量数据库。
### 集成流程
#### 1. 实现基础检索引擎接口
首先需要实现 `interfaces` 包中的 `RetrieveEngine` 接口,定义检索引擎的核心能力:
```go
type RetrieveEngine interface {
// 返回检索引擎的类型标识
EngineType() types.RetrieverEngineType
// 执行检索操作,返回匹配结果
Retrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error)
// 返回该引擎支持的检索类型列表
Support() []types.RetrieverType
}
```
#### 2. 实现存储层接口
实现 `RetrieveEngineRepository` 接口,扩展基础检索引擎能力,添加索引管理功能:
```go
type RetrieveEngineRepository interface {
// 保存单个索引信息
Save(ctx context.Context, indexInfo *types.IndexInfo, params map[string]any) error
// 批量保存多个索引信息
BatchSave(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) error
// 估算索引存储所需空间
EstimateStorageSize(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) int64
// 通过分块ID列表删除索引
DeleteByChunkIDList(ctx context.Context, indexIDList []string, dimension int) error
// 复制索引数据,避免重新计算嵌入向量
CopyIndices(
ctx context.Context,
sourceKnowledgeBaseID string,
sourceToTargetKBIDMap map[string]string,
sourceToTargetChunkIDMap map[string]string,
targetKnowledgeBaseID string,
dimension int,
) error
// 通过知识ID列表删除索引
DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int) error
// 继承RetrieveEngine接口
RetrieveEngine
}
```
#### 3. 实现服务层接口
创建实现 `RetrieveEngineService` 接口的服务,负责处理索引创建和管理的业务逻辑:
```go
type RetrieveEngineService interface {
// 创建单个索引
Index(ctx context.Context,
embedder embedding.Embedder,
indexInfo *types.IndexInfo,
retrieverTypes []types.RetrieverType,
) error
// 批量创建索引
BatchIndex(ctx context.Context,
embedder embedding.Embedder,
indexInfoList []*types.IndexInfo,
retrieverTypes []types.RetrieverType,
) error
// 估算索引存储空间
EstimateStorageSize(ctx context.Context,
embedder embedding.Embedder,
indexInfoList []*types.IndexInfo,
retrieverTypes []types.RetrieverType,
) int64
// 复制索引数据
CopyIndices(
ctx context.Context,
sourceKnowledgeBaseID string,
sourceToTargetKBIDMap map[string]string,
sourceToTargetChunkIDMap map[string]string,
targetKnowledgeBaseID string,
dimension int,
) error
// 删除索引
DeleteByChunkIDList(ctx context.Context, indexIDList []string, dimension int) error
DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int) error
// 继承RetrieveEngine接口
RetrieveEngine
}
```
#### 4. 添加环境变量配置
在环境配置中添加新数据库的必要连接参数:
```
# 在RETRIEVE_DRIVER中添加新数据库驱动名称多个驱动用逗号分隔
RETRIEVE_DRIVER=postgres,elasticsearch_v8,your_database
# 新数据库的连接参数
YOUR_DATABASE_ADDR=your_database_host:port
YOUR_DATABASE_USERNAME=username
YOUR_DATABASE_PASSWORD=password
# 其他必要的连接参数...
```
#### 5. 注册检索引擎
`internal/container/container.go` 文件的 `initRetrieveEngineRegistry` 函数中添加新数据库的初始化与注册逻辑:
```go
func initRetrieveEngineRegistry(db *gorm.DB, cfg *config.Config) (interfaces.RetrieveEngineRegistry, error) {
registry := retriever.NewRetrieveEngineRegistry()
retrieveDriver := strings.Split(os.Getenv("RETRIEVE_DRIVER"), ",")
log := logger.GetLogger(context.Background())
// 已有的PostgreSQL和Elasticsearch初始化代码...
// 添加新向量数据库的初始化代码
if slices.Contains(retrieveDriver, "your_database") {
// 初始化数据库客户端
client, err := your_database.NewClient(your_database.Config{
Addresses: []string{os.Getenv("YOUR_DATABASE_ADDR")},
Username: os.Getenv("YOUR_DATABASE_USERNAME"),
Password: os.Getenv("YOUR_DATABASE_PASSWORD"),
// 其他连接参数...
})
if err != nil {
log.Errorf("Create your_database client failed: %v", err)
} else {
// 创建检索引擎仓库
yourDatabaseRepo := your_database.NewYourDatabaseRepository(client, cfg)
// 注册检索引擎
if err := registry.Register(
retriever.NewKVHybridRetrieveEngine(
yourDatabaseRepo, types.YourDatabaseRetrieverEngineType,
),
); err != nil {
log.Errorf("Register your_database retrieve engine failed: %v", err)
} else {
log.Infof("Register your_database retrieve engine success")
}
}
}
return registry, nil
}
```
#### 6. 定义检索引擎类型常量
`internal/types/retriever.go` 文件中添加新的检索引擎类型常量:
```go
// RetrieverEngineType 定义检索引擎类型
const (
ElasticsearchRetrieverEngineType RetrieverEngineType = "elasticsearch"
PostgresRetrieverEngineType RetrieverEngineType = "postgres"
YourDatabaseRetrieverEngineType RetrieverEngineType = "your_database" // 添加新数据库类型
)
```
## 参考实现示例
建议参考现有的 PostgreSQL 和 Elasticsearch 实现作为开发模板。这些实现位于以下目录:
- PostgreSQL: `internal/application/repository/retriever/postgres/`
- ElasticsearchV7: `internal/application/repository/retriever/elasticsearch/v7/`
- ElasticsearchV8: `internal/application/repository/retriever/elasticsearch/v8/`
通过遵循以上步骤和参考现有实现,你可以成功集成新的向量数据库到 WeKnora 系统中,扩展其向量检索能力。

8
frontend/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.git
.gitignore
README.md
.vscode
*.log
.DS_Store

30
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

36
frontend/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
# 构建阶段
FROM node:24-alpine as build-stage
WORKDIR /app
# 设置环境变量,忽略类型检查错误
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV VITE_IS_DOCKER=true
# 复制依赖文件
COPY package*.json ./
# 安装依赖
RUN corepack enable
RUN pnpm install
# 复制项目文件
COPY . .
# 构建应用
RUN pnpm run build
# 生产阶段
FROM nginx:stable-alpine as production-stage
# 复制构建产物到nginx服务目录
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 复制nginx配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

6
frontend/env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="vite/client" />
// 配置这个文件是 解决错误:找不到模块“@/views/login/index.vue”或其相应的类型声明。ts(2307)
// 这段代码告诉 TypeScript所有以 .vue 结尾的文件都是 Vue 组件,可以通过 import 语句进行导入。这样做通常可以解决无法识别模块的问题。
declare module '*.vue' {
import { Component } from 'vue'; const component: Component; export default component;
}

22
frontend/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>WeKnora</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="keywords" content="知识问答、微信对话开放平台、对话开放平台、对话平台、人工智能定制、人机对话、智能问答、人机交互、自然语言处理、自然语言理解、NLP、人工智能产品、人工智能开源、人工智能算法、语音助手"/>
<meta name="description" content="WeKnora是一款基于大语言模型的文档理解与语义检索框架专为结构复杂、内容异构的文档场景而打造。"/>
<link rel="shortcut icon" type="image/png" href="./public/favicon.ico"/>
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="./public/favicon.ico"/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

36
frontend/nginx.conf Normal file
View File

@ -0,0 +1,36 @@
server {
listen 80;
server_name localhost;
client_max_body_size 50M;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API请求代理到后端服务
location /api/ {
proxy_pass http://app:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 相关配置
proxy_http_version 1.1; # 使用 HTTP/1.1
proxy_set_header Connection ""; # 禁用 Connection: close保持连接打开
chunked_transfer_encoding off; # 关闭分块传输编码
proxy_buffering off; # 关闭缓冲
proxy_cache off; # 关闭缓存
proxy_read_timeout 3600s; # 增加读取超时时间
proxy_send_timeout 3600s; # 增加发送超时时间
}
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

3781
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
frontend/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "knowledage-base",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build-with-types": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"axios": "^1.8.4",
"marked": "^5.1.2",
"pagefind": "^1.1.1",
"pinia": "^3.0.1",
"tdesign-vue-next": "^1.11.5",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"webpack": "^5.94.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.1",
"@types/marked": "^5.0.2",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.1",
"@vue/tsconfig": "^0.7.0",
"less": "^4.3.0",
"less-loader": "^12.2.0",
"npm-run-all2": "^7.0.2",
"typescript": "~5.8.0",
"vite": "7.0.4",
"vue-tsc": "^2.2.8"
},
"overrides": {
"lightningcss": "none",
"esbuild": "^0.25.0"
},
"resolutions": {
"lightningcss": "none",
"esbuild": "^0.25.0"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

23
frontend/src/App.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
</script>
<template>
<div id="app">
<RouterView />
</div>
</template>
<style>
body,
html,
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-size: 14px;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, SimSun, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f8f9fa;
}
</style>

View File

@ -0,0 +1,62 @@
import { get, post, put, del, postChat } from "../../utils/request";
import { loadTestData } from "../test-data";
// 从localStorage获取设置
function getSettings() {
const settingsStr = localStorage.getItem("WeKnora_settings");
if (settingsStr) {
try {
const settings = JSON.parse(settingsStr);
if (settings.apiKey && settings.endpoint) {
return settings;
}
} catch (e) {
console.error("解析设置失败:", e);
}
}
return null;
}
// 根据是否有设置决定是否需要加载测试数据
async function ensureConfigured() {
const settings = getSettings();
// 如果没有设置APIKey和Endpoint则加载测试数据
if (!settings) {
await loadTestData();
}
}
export async function createSessions(data = {}) {
await ensureConfigured();
return post("/api/v1/sessions", data);
}
export async function getSessionsList(page: number, page_size: number) {
await ensureConfigured();
return get(`/api/v1/sessions?page=${page}&page_size=${page_size}`);
}
export async function generateSessionsTitle(session_id: string, data: any) {
await ensureConfigured();
return post(`/api/v1/sessions/${session_id}/generate_title`, data);
}
export async function knowledgeChat(data: { session_id: string; query: string; }) {
await ensureConfigured();
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query });
}
export async function getMessageList(data: { session_id: string; limit: number, created_at: string }) {
await ensureConfigured();
if (data.created_at) {
return get(`/api/v1/messages/${data.session_id}/load?before_time=${encodeURIComponent(data.created_at)}&limit=${data.limit}`);
} else {
return get(`/api/v1/messages/${data.session_id}/load?limit=${data.limit}`);
}
}
export async function delSession(session_id: string) {
await ensureConfigured();
return del(`/api/v1/sessions/${session_id}`);
}

View File

@ -0,0 +1,147 @@
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref, type Ref, onUnmounted, nextTick } from 'vue'
import { generateRandomString } from '@/utils/index';
import { getTestData } from '@/utils/request';
import { loadTestData } from '@/api/test-data';
// 从localStorage获取设置
function getSettings() {
const settingsStr = localStorage.getItem("WeKnora_settings");
if (settingsStr) {
try {
const settings = JSON.parse(settingsStr);
return settings;
} catch (e) {
console.error("解析设置失败:", e);
}
}
return null;
}
interface StreamOptions {
// 请求方法 (默认POST)
method?: 'GET' | 'POST'
// 请求头
headers?: Record<string, string>
// 请求体自动序列化
body?: Record<string, any>
// 流式渲染间隔 (ms)
chunkInterval?: number
}
export function useStream() {
// 响应式状态
const output = ref('') // 显示内容
const isStreaming = ref(false) // 流状态
const isLoading = ref(false) // 初始加载
const error = ref<string | null>(null)// 错误信息
let controller = new AbortController()
// 流式渲染缓冲
let buffer: string[] = []
let renderTimer: number | null = null
// 启动流式请求
const startStream = async (params: { session_id: any; query: any; method: string; url: string }) => {
// 重置状态
output.value = '';
error.value = null;
isStreaming.value = true;
isLoading.value = true;
// 获取设置信息
const settings = getSettings();
let apiUrl = '';
let apiKey = '';
// 如果有设置信息,优先使用设置信息
if (settings && settings.endpoint && settings.apiKey) {
apiUrl = settings.endpoint;
apiKey = settings.apiKey;
} else {
// 否则加载测试数据
await loadTestData();
const testData = getTestData();
if (!testData) {
error.value = "测试数据未初始化,无法进行聊天";
stopStream();
return;
}
apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
apiKey = testData.tenant.api_key;
}
try {
let url =
params.method == "POST"
? `${apiUrl}${params.url}/${params.session_id}`
: `${apiUrl}${params.url}/${params.session_id}?message_id=${params.query}`;
await fetchEventSource(url, {
method: params.method,
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
"X-Request-ID": `${generateRandomString(12)}`,
},
body:
params.method == "POST"
? JSON.stringify({ query: params.query })
: null,
signal: controller.signal,
openWhenHidden: true,
onopen: async (res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
isLoading.value = false;
},
onmessage: (ev) => {
buffer.push(JSON.parse(ev.data)); // 数据存入缓冲
// 执行自定义处理
if (chunkHandler) {
chunkHandler(JSON.parse(ev.data));
}
},
onerror: (err) => {
throw new Error(`流式连接失败: ${err}`);
},
onclose: () => {
stopStream();
},
});
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
stopStream()
}
}
let chunkHandler: ((data: any) => void) | null = null
// 注册块处理器
const onChunk = (handler: () => void) => {
chunkHandler = handler
}
// 停止流
const stopStream = () => {
controller.abort();
controller = new AbortController(); // 重置控制器(如需重新发起)
isStreaming.value = false;
isLoading.value = false;
}
// 组件卸载时自动清理
onUnmounted(stopStream)
return {
output, // 显示内容
isStreaming, // 是否在流式传输中
isLoading, // 初始连接状态
error,
onChunk,
startStream, // 启动流
stopStream // 手动停止
}
}

View File

@ -0,0 +1,332 @@
import { get, post } from '../../utils/request';
// 初始化配置数据类型
export interface InitializationConfig {
llm: {
source: string;
modelName: string;
baseUrl?: string;
apiKey?: string;
};
embedding: {
source: string;
modelName: string;
baseUrl?: string;
apiKey?: string;
dimension?: number; // 添加embedding维度字段
};
rerank: {
modelName: string;
baseUrl: string;
apiKey?: string;
};
multimodal: {
enabled: boolean;
storageType: 'cos' | 'minio';
vlm?: {
modelName: string;
baseUrl: string;
apiKey?: string;
interfaceType?: string; // "ollama" or "openai"
};
cos?: {
secretId: string;
secretKey: string;
region: string;
bucketName: string;
appId: string;
pathPrefix?: string;
};
minio?: {
bucketName: string;
pathPrefix?: string;
};
};
documentSplitting: {
chunkSize: number;
chunkOverlap: number;
separators: string[];
};
// Frontend-only hint for storage selection UI
storageType?: 'cos' | 'minio';
}
// 下载任务状态类型
export interface DownloadTask {
id: string;
modelName: string;
status: 'pending' | 'downloading' | 'completed' | 'failed';
progress: number;
message: string;
startTime: string;
endTime?: string;
}
// 系统初始化状态检查
export function checkInitializationStatus(): Promise<{ initialized: boolean }> {
return new Promise((resolve, reject) => {
get('/api/v1/initialization/status')
.then((response: any) => {
resolve(response.data || { initialized: false });
})
.catch((error: any) => {
console.warn('检查初始化状态失败,假设需要初始化:', error);
resolve({ initialized: false });
});
});
}
// 执行系统初始化
export function initializeSystem(config: InitializationConfig): Promise<any> {
return new Promise((resolve, reject) => {
console.log('开始系统初始化...', config);
post('/api/v1/initialization/initialize', config)
.then((response: any) => {
console.log('系统初始化完成', response);
// 设置本地初始化状态标记
localStorage.setItem('system_initialized', 'true');
resolve(response);
})
.catch((error: any) => {
console.error('系统初始化失败:', error);
reject(error);
});
});
}
// 检查Ollama服务状态
export function checkOllamaStatus(): Promise<{ available: boolean; version?: string; error?: string; baseUrl?: string }> {
return new Promise((resolve, reject) => {
get('/api/v1/initialization/ollama/status')
.then((response: any) => {
resolve(response.data || { available: false });
})
.catch((error: any) => {
console.error('检查Ollama状态失败:', error);
resolve({ available: false, error: error.message || '检查失败' });
});
});
}
// 列出已安装的 Ollama 模型
export function listOllamaModels(): Promise<string[]> {
return new Promise((resolve, reject) => {
get('/api/v1/initialization/ollama/models')
.then((response: any) => {
resolve((response.data && response.data.models) || []);
})
.catch((error: any) => {
console.error('获取 Ollama 模型列表失败:', error);
resolve([]);
});
});
}
// 检查Ollama模型状态
export function checkOllamaModels(models: string[]): Promise<{ models: Record<string, boolean> }> {
return new Promise((resolve, reject) => {
post('/api/v1/initialization/ollama/models/check', { models })
.then((response: any) => {
resolve(response.data || { models: {} });
})
.catch((error: any) => {
console.error('检查Ollama模型状态失败:', error);
reject(error);
});
});
}
// 启动Ollama模型下载异步
export function downloadOllamaModel(modelName: string): Promise<{ taskId: string; modelName: string; status: string; progress: number }> {
return new Promise((resolve, reject) => {
post('/api/v1/initialization/ollama/models/download', { modelName })
.then((response: any) => {
resolve(response.data || { taskId: '', modelName, status: 'failed', progress: 0 });
})
.catch((error: any) => {
console.error('启动Ollama模型下载失败:', error);
reject(error);
});
});
}
// 查询下载进度
export function getDownloadProgress(taskId: string): Promise<DownloadTask> {
return new Promise((resolve, reject) => {
get(`/api/v1/initialization/ollama/download/progress/${taskId}`)
.then((response: any) => {
resolve(response.data);
})
.catch((error: any) => {
console.error('查询下载进度失败:', error);
reject(error);
});
});
}
// 获取所有下载任务
export function listDownloadTasks(): Promise<DownloadTask[]> {
return new Promise((resolve, reject) => {
get('/api/v1/initialization/ollama/download/tasks')
.then((response: any) => {
resolve(response.data || []);
})
.catch((error: any) => {
console.error('获取下载任务列表失败:', error);
reject(error);
});
});
}
// 获取当前系统配置
export function getCurrentConfig(): Promise<InitializationConfig & { hasFiles: boolean }> {
return new Promise((resolve, reject) => {
get('/api/v1/initialization/config')
.then((response: any) => {
resolve(response.data || {});
})
.catch((error: any) => {
console.error('获取当前配置失败:', error);
reject(error);
});
});
}
// 检查远程API模型
export function checkRemoteModel(modelConfig: {
modelName: string;
baseUrl: string;
apiKey?: string;
}): Promise<{
available: boolean;
message?: string;
}> {
return new Promise((resolve, reject) => {
post('/api/v1/initialization/remote/check', modelConfig)
.then((response: any) => {
resolve(response.data || {});
})
.catch((error: any) => {
console.error('检查远程模型失败:', error);
reject(error);
});
});
}
// 测试 Embedding 模型(本地/远程)是否可用
export function testEmbeddingModel(modelConfig: {
source: 'local' | 'remote';
modelName: string;
baseUrl?: string;
apiKey?: string;
dimension?: number;
}): Promise<{ available: boolean; message?: string; dimension?: number }> {
return new Promise((resolve, reject) => {
post('/api/v1/initialization/embedding/test', modelConfig)
.then((response: any) => {
resolve(response.data || {});
})
.catch((error: any) => {
console.error('测试Embedding模型失败:', error);
reject(error);
});
});
}
export function checkRerankModel(modelConfig: {
modelName: string;
baseUrl: string;
apiKey?: string;
}): Promise<{
available: boolean;
message?: string;
}> {
return new Promise((resolve, reject) => {
post('/api/v1/initialization/rerank/check', modelConfig)
.then((response: any) => {
resolve(response.data || {});
})
.catch((error: any) => {
console.error('检查Rerank模型失败:', error);
reject(error);
});
});
}
export function testMultimodalFunction(testData: {
image: File;
vlm_model: string;
vlm_base_url: string;
vlm_api_key?: string;
vlm_interface_type?: string;
storage_type?: 'cos'|'minio';
// COS optional fields (required only when storage_type === 'cos')
cos_secret_id?: string;
cos_secret_key?: string;
cos_region?: string;
cos_bucket_name?: string;
cos_app_id?: string;
cos_path_prefix?: string;
// MinIO optional fields
minio_bucket_name?: string;
minio_path_prefix?: string;
chunk_size: number;
chunk_overlap: number;
separators: string[];
}): Promise<{
success: boolean;
caption?: string;
ocr?: string;
processing_time?: number;
message?: string;
}> {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('image', testData.image);
formData.append('vlm_model', testData.vlm_model);
formData.append('vlm_base_url', testData.vlm_base_url);
if (testData.vlm_api_key) {
formData.append('vlm_api_key', testData.vlm_api_key);
}
if (testData.vlm_interface_type) {
formData.append('vlm_interface_type', testData.vlm_interface_type);
}
if (testData.storage_type) {
formData.append('storage_type', testData.storage_type);
}
// Append COS fields only when storage_type is COS
if (testData.storage_type === 'cos') {
if (testData.cos_secret_id) formData.append('cos_secret_id', testData.cos_secret_id);
if (testData.cos_secret_key) formData.append('cos_secret_key', testData.cos_secret_key);
if (testData.cos_region) formData.append('cos_region', testData.cos_region);
if (testData.cos_bucket_name) formData.append('cos_bucket_name', testData.cos_bucket_name);
if (testData.cos_app_id) formData.append('cos_app_id', testData.cos_app_id);
if (testData.cos_path_prefix) formData.append('cos_path_prefix', testData.cos_path_prefix);
}
// MinIO fields
if (testData.minio_bucket_name) formData.append('minio_bucket_name', testData.minio_bucket_name);
if (testData.minio_path_prefix) formData.append('minio_path_prefix', testData.minio_path_prefix);
formData.append('chunk_size', testData.chunk_size.toString());
formData.append('chunk_overlap', testData.chunk_overlap.toString());
formData.append('separators', JSON.stringify(testData.separators));
// 使用原生fetch因为需要发送FormData
fetch('/api/v1/initialization/multimodal/test', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then((data: any) => {
if (data.success) {
resolve(data.data || {});
} else {
resolve({ success: false, message: data.message || '测试失败' });
}
})
.catch((error: any) => {
console.error('多模态测试失败:', error);
reject(error);
});
});
}

View File

@ -0,0 +1,62 @@
import { get, post, put, del, postUpload, getDown, getTestData } from "../../utils/request";
import { loadTestData } from "../test-data";
// 获取知识库ID优先从设置中获取
async function getKnowledgeBaseID() {
// 从localStorage获取设置中的知识库ID
const settingsStr = localStorage.getItem("WeKnora_settings");
let knowledgeBaseId = "";
if (settingsStr) {
try {
const settings = JSON.parse(settingsStr);
if (settings.knowledgeBaseId) {
return settings.knowledgeBaseId;
}
} catch (e) {
console.error("解析设置失败:", e);
}
}
// 如果设置中没有知识库ID则使用测试数据
await loadTestData();
const testData = getTestData();
if (!testData || testData.knowledge_bases.length === 0) {
console.error("测试数据未初始化或不包含知识库");
throw new Error("测试数据未初始化或不包含知识库");
}
return testData.knowledge_bases[0].id;
}
export async function uploadKnowledgeBase(data = {}) {
const kbId = await getKnowledgeBaseID();
return postUpload(`/api/v1/knowledge-bases/${kbId}/knowledge/file`, data);
}
export async function getKnowledgeBase({page, page_size}) {
const kbId = await getKnowledgeBaseID();
return get(
`/api/v1/knowledge-bases/${kbId}/knowledge?page=${page}&page_size=${page_size}`
);
}
export function getKnowledgeDetails(id: any) {
return get(`/api/v1/knowledge/${id}`);
}
export function delKnowledgeDetails(id: any) {
return del(`/api/v1/knowledge/${id}`);
}
export function downKnowledgeDetails(id: any) {
return getDown(`/api/v1/knowledge/${id}/download`);
}
export function batchQueryKnowledge(ids: any) {
return get(`/api/v1/knowledge/batch?${ids}`);
}
export function getKnowledgeDetailsCon(id: any, page) {
return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`);
}

View File

@ -0,0 +1,55 @@
import { get, setTestData } from '../../utils/request';
export interface TestDataResponse {
success: boolean;
data: {
tenant: {
id: number;
name: string;
api_key: string;
};
knowledge_bases: Array<{
id: string;
name: string;
description: string;
}>;
}
}
// 是否已加载测试数据
let isTestDataLoaded = false;
/**
*
* API调用前调用此函数以确保测试数据已加载
* @returns Promise<boolean>
*/
export async function loadTestData(): Promise<boolean> {
// 如果已经加载过,直接返回
if (isTestDataLoaded) {
return true;
}
try {
console.log('开始加载测试数据...');
const response = await get('/api/v1/test-data');
console.log('测试数据', response);
if (response && response.data) {
// 设置测试数据
setTestData({
tenant: response.data.tenant,
knowledge_bases: response.data.knowledge_bases
});
isTestDataLoaded = true;
console.log('测试数据加载成功');
return true;
} else {
console.warn('测试数据响应为空');
return false;
}
} catch (error) {
console.error('加载测试数据失败:', error);
return false;
}
}

View File

@ -0,0 +1,6 @@
@font-face {
font-family: 'TencentSans';
src: url('fonts/TencentSans.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

Binary file not shown.

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" class="r4oyk8abi__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2C5.79086 2 4 3.79086 4 6C4 7.56981 4.90422 8.92935 6.22252 9.58448C6.39251 9.66896 6.5 9.84241 6.5 10.0322V11H9.5V10.0322C9.5 9.84241 9.60749 9.66896 9.77748 9.58448C11.0958 8.92935 12 7.56981 12 6C12 3.79086 10.2091 2 8 2ZM3 6C3 3.23858 5.23858 1 8 1C10.7614 1 13 3.23858 13 6C13 7.85144 11.9936 9.46702 10.5 10.331V11.5C10.5 11.7761 10.2761 12 10 12H6C5.72386 12 5.5 11.7761 5.5 11.5V10.331C4.00636 9.46702 3 7.85144 3 6Z" fill="#32CE3F"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 13.5H6V12.5H10V13.5Z" fill="#32CE3F"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15H7V14H9V15Z" fill="#32CE3F"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4269 5.11946L7.60516 7.94123L5.7821 6.11819L6.48921 5.41108L7.60516 6.52702L9.71982 4.41235L10.4269 5.11946Z" fill="#32CE3F"></path>
</svg>

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="r6a8hvpwe__design-iconfont" width="128" height="128">
<path d="M6 12V6H7V12H6Z"></path>
<path d="M9 6V12H10V6H9Z"></path>
<path d="M10.5 3H14V4H13V14C13 14.5523 12.5523 15 12 15H4C3.44772 15 3 14.5523 3 14V4H2V3H5.5L5.5 1.8C5.5 1.35817 5.85817 1 6.3 1H9.7C10.1418 1 10.5 1.35817 10.5 1.8V3ZM6.5 3H9.5L9.5 2L6.5 2V3ZM4 4V14H12V4H4Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="wb1yuxk13__design-iconfont" width="128" height="128">
<path d="M12.2636 5.81387L8.5 9.57746L8.49998 0.5L7.49998 0.500002L7.5 9.57746L3.73641 5.81387L3.02931 6.52098L7.64645 11.1381C7.84171 11.3334 8.15829 11.3334 8.35355 11.1381L12.9707 6.52098L12.2636 5.81387Z" fill-opacity=".9" fill="#07C05F"></path>
<path d="M2 11V13C2 13.5523 2.44772 14 3 14H13C13.5523 14 14 13.5523 14 13V11H13V13H3V11H2Z" fill-opacity=".9" fill="#07C05F"></path>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="g8nqf16e8__design-iconfont" width="128" height="128">
<path d="M5 1.25C4.30482 1.25 3.75 1.80482 3.75 2.5V16.25C3.75 16.9451 4.30482 17.5 5 17.5H10.6702V16.25H5V2.5H10V7.5H15V10.6055H16.25V7.13388C16.25 6.80236 16.1183 6.48442 15.8839 6.25L15.7808 6.14692L15.7794 6.14556L11.1419 1.50806C11.036 1.40209 10.9104 1.3443 10.7858 1.32256C10.6523 1.27498 10.5105 1.25 10.3661 1.25H5ZM14.1161 6.25H11.25V3.38388L14.1161 6.25Z" fill="#07C05F"></path>
<path d="M15 18.75V16.25H12.5V15H15V12.5H16.25V15H18.75V16.25H16.25V18.75H15Z" fill="#07C05F"></path>
</svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 1.25C4.30482 1.25 3.75 1.80482 3.75 2.5V16.25C3.75 16.9451 4.30482 17.5 5 17.5H10.6702V16.25H5V2.5H10V7.5H15V10.6055H16.25V7.13388C16.25 6.80236 16.1183 6.48442 15.8839 6.25L15.7808 6.14692L15.7794 6.14556L11.1419 1.50806C11.036 1.40209 10.9104 1.3443 10.7858 1.32256C10.6523 1.27498 10.5105 1.25 10.3661 1.25H5ZM14.1161 6.25H11.25V3.38388L14.1161 6.25Z" fill="#07C05F"/>
<path d="M15 18.75V16.25H12.5V15H15V12.5H16.25V15H18.75V16.25H16.25V18.75H15Z" fill="#07C05F"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="g8nqf16e8__design-iconfont" width="128" height="128">
<path d="M5 1.25C4.30482 1.25 3.75 1.80482 3.75 2.5V16.25C3.75 16.9451 4.30482 17.5 5 17.5H10.6702V16.25H5V2.5H10V7.5H15V10.6055H16.25V7.13388C16.25 6.80236 16.1183 6.48442 15.8839 6.25L15.7808 6.14692L15.7794 6.14556L11.1419 1.50806C11.036 1.40209 10.9104 1.3443 10.7858 1.32256C10.6523 1.27498 10.5105 1.25 10.3661 1.25H5ZM14.1161 6.25H11.25V3.38388L14.1161 6.25Z" fill="#666666"></path>
<path d="M15 18.75V16.25H12.5V15H15V12.5H16.25V15H18.75V16.25H16.25V18.75H15Z" fill="#666666"></path>
</svg>

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="45o4l8wsy__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.875 2.5C1.875 2.15483 2.15482 1.875 2.5 1.875H17.5C17.8451 1.875 18.125 2.15483 18.125 2.5V13.75C18.125 14.0951 17.8451 14.375 17.5 14.375H9.04274L6.10514 17.9001C5.93669 18.1023 5.65965 18.1773 5.41224 18.0876C5.16481 17.9981 5 17.7631 5 17.5V14.375H2.5C2.15482 14.375 1.875 14.0951 1.875 13.75V2.5ZM3.125 3.125V13.125H5.625C5.97017 13.125 6.25 13.4049 6.25 13.75V15.7738L8.26986 13.3499C8.38861 13.2074 8.56451 13.125 8.75 13.125H16.875V3.125H3.125Z" fill="#07C05F" fill-opacity=".9"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.375 5H10.625V8.44961L13.5154 10.762L12.7346 11.738L9.375 9.05039V5Z" fill="#07C05F" fill-opacity=".9"></path>
</svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="45o4l8wsy__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.875 2.5C1.875 2.15483 2.15482 1.875 2.5 1.875H17.5C17.8451 1.875 18.125 2.15483 18.125 2.5V13.75C18.125 14.0951 17.8451 14.375 17.5 14.375H9.04274L6.10514 17.9001C5.93669 18.1023 5.65965 18.1773 5.41224 18.0876C5.16481 17.9981 5 17.7631 5 17.5V14.375H2.5C2.15482 14.375 1.875 14.0951 1.875 13.75V2.5ZM3.125 3.125V13.125H5.625C5.97017 13.125 6.25 13.4049 6.25 13.75V15.7738L8.26986 13.3499C8.38861 13.2074 8.56451 13.125 8.75 13.125H16.875V3.125H3.125Z" fill="#737373" fill-opacity=".9"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.375 5H10.625V8.44961L13.5154 10.762L12.7346 11.738L9.375 9.05039V5Z" fill="#737373" fill-opacity=".9"></path>
</svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="45o4l8wsy__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.875 2.5C1.875 2.15483 2.15482 1.875 2.5 1.875H17.5C17.8451 1.875 18.125 2.15483 18.125 2.5V13.75C18.125 14.0951 17.8451 14.375 17.5 14.375H9.04274L6.10514 17.9001C5.93669 18.1023 5.65965 18.1773 5.41224 18.0876C5.16481 17.9981 5 17.7631 5 17.5V14.375H2.5C2.15482 14.375 1.875 14.0951 1.875 13.75V2.5ZM3.125 3.125V13.125H5.625C5.97017 13.125 6.25 13.4049 6.25 13.75V15.7738L8.26986 13.3499C8.38861 13.2074 8.56451 13.125 8.75 13.125H16.875V3.125H3.125Z" fill="#000000" fill-opacity=".9"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.375 5H10.625V8.44961L13.5154 10.762L12.7346 11.738L9.375 9.05039V5Z" fill="#000000" fill-opacity=".9"></path>
</svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.942 2.05808C18.1094 2.22548 18.1677 2.47308 18.093 2.69767L13.093 17.6976C13.02 17.9164 12.8328 18.0771 12.6055 18.116C12.3784 18.155 12.1482 18.0656 12.0067 17.8838L7.67964 12.3204L2.1163 7.99337C1.93436 7.85185 1.84507 7.62169 1.88399 7.3945C1.9229 7.16732 2.0837 6.97998 2.30237 6.90709L17.3024 1.90709C17.527 1.83223 17.7745 1.89068 17.942 2.05808ZM8.9571 11.9268L12.2764 16.1945L16.5118 3.48823L3.80555 7.72365L8.07321 11.0429L10.8081 8.30808L11.692 9.19197L8.9571 11.9268Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 651 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="#07C05F" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.66675 10.7333V9.26667C1.66675 8.4 2.37508 7.68333 3.25008 7.68333C4.75841 7.68333 5.35008 6.61667 4.59175 5.30833C4.14175 4.55833 4.38341 3.58333 5.14175 3.13333L6.58342 2.30833C7.24175 1.90833 8.09175 2.125 8.49175 2.78333L8.57508 2.92501C9.32508 4.23334 10.6751 4.23334 11.4334 2.92501L11.5167 2.78333C11.9167 2.125 12.7667 1.90833 13.4251 2.30833L14.8667 3.13333C15.6251 3.58333 15.8667 4.55833 15.4167 5.30833C14.6584 6.61667 15.2501 7.68333 16.7584 7.68333C17.6251 7.68333 18.3417 8.39167 18.3417 9.26667V10.7333C18.3417 11.6 17.6334 12.3167 16.7584 12.3167C15.2501 12.3167 14.6584 13.3833 15.4167 14.6917C15.8667 15.45 15.6251 16.4167 14.8667 16.8667L13.4251 17.6917C12.7667 18.0917 11.9167 17.875 11.5167 17.2167L11.4334 17.075C10.6834 15.7667 9.33341 15.7667 8.57508 17.075L8.49175 17.2167C8.09175 17.875 7.24175 18.0917 6.58342 17.6917L5.14175 16.8667C4.38341 16.4167 4.14175 15.4417 4.59175 14.6917C5.35008 13.3833 4.75841 12.3167 3.25008 12.3167C2.37508 12.3167 1.66675 11.6 1.66675 10.7333Z" stroke="#07C05F" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.66675 10.7333V9.26667C1.66675 8.4 2.37508 7.68333 3.25008 7.68333C4.75841 7.68333 5.35008 6.61667 4.59175 5.30833C4.14175 4.55833 4.38341 3.58333 5.14175 3.13333L6.58342 2.30833C7.24175 1.90833 8.09175 2.125 8.49175 2.78333L8.57508 2.92501C9.32508 4.23334 10.6751 4.23334 11.4334 2.92501L11.5167 2.78333C11.9167 2.125 12.7667 1.90833 13.4251 2.30833L14.8667 3.13333C15.6251 3.58333 15.8667 4.55833 15.4167 5.30833C14.6584 6.61667 15.2501 7.68333 16.7584 7.68333C17.6251 7.68333 18.3417 8.39167 18.3417 9.26667V10.7333C18.3417 11.6 17.6334 12.3167 16.7584 12.3167C15.2501 12.3167 14.6584 13.3833 15.4167 14.6917C15.8667 15.45 15.6251 16.4167 14.8667 16.8667L13.4251 17.6917C12.7667 18.0917 11.9167 17.875 11.5167 17.2167L11.4334 17.075C10.6834 15.7667 9.33341 15.7667 8.57508 17.075L8.49175 17.2167C8.09175 17.875 7.24175 18.0917 6.58342 17.6917L5.14175 16.8667C4.38341 16.4167 4.14175 15.4417 4.59175 14.6917C5.35008 13.3833 4.75841 12.3167 3.25008 12.3167C2.37508 12.3167 1.66675 11.6 1.66675 10.7333Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -0,0 +1,64 @@
<svg width="164" height="162" viewBox="0 0 164 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_6022_51714)">
<path d="M44.6 135C22.7296 135 5 117.238 5 95.3265C5 73.4155 22.7296 55.6531 44.6 55.6531C48.5782 55.6531 52.4203 56.2409 56.0426 57.3345C58.7553 40.1439 73.6125 27 91.5333 27C111.378 27 127.467 43.1177 127.467 63C127.467 65.7803 127.152 68.487 126.556 71.0862C126.737 71.0832 126.919 71.0816 127.1 71.0816C144.718 71.0816 159 85.3902 159 103.041C159 120.691 144.718 135 127.1 135H44.6Z" fill="url(#paint0_linear_6022_51714)"/>
</g>
<g filter="url(#filter1_d_6022_51714)">
<path d="M50.4857 131C30.8876 131 15 115.047 15 95.3673C15 75.688 30.8876 59.7347 50.4857 59.7347C54.0506 59.7347 57.4935 60.2626 60.7395 61.2448C63.1704 45.8051 76.4839 34 92.5429 34C110.326 34 124.743 48.4761 124.743 66.3333C124.743 68.8305 124.46 71.2615 123.927 73.5959C124.089 73.5932 124.252 73.5918 124.414 73.5918C140.202 73.5918 153 86.4431 153 102.296C153 118.149 140.202 131 124.414 131H50.4857Z" fill="url(#paint1_linear_6022_51714)"/>
</g>
<g filter="url(#filter2_d_6022_51714)">
<path d="M54.8571 127C37.8153 127 24 113.185 24 96.1429C24 79.1009 37.8153 65.2857 54.8571 65.2857C57.957 65.2857 60.9509 65.7429 63.7734 66.5935C65.8873 53.223 77.4643 43 91.4286 43C106.892 43 119.429 55.536 119.429 71C119.429 73.1625 119.183 75.2677 118.719 77.2893C118.86 77.2869 119.002 77.2857 119.143 77.2857C132.871 77.2857 144 88.4146 144 102.143C144 115.871 132.871 127 119.143 127H54.8571Z" fill="url(#paint2_linear_6022_51714)"/>
</g>
<path d="M97.8271 117.721C97.8271 116.644 98.7004 115.771 99.7775 115.771H104.978C106.056 115.771 106.929 116.644 106.929 117.721V117.721C106.929 118.799 106.056 119.672 104.978 119.672H99.7775C98.7004 119.672 97.8271 118.799 97.8271 117.721V117.721Z" fill="#1485EE"/>
<path d="M113.431 117.721C113.431 116.644 114.304 115.771 115.381 115.771H120.582C121.659 115.771 122.532 116.644 122.532 117.721V117.721C122.532 118.799 121.659 119.672 120.582 119.672H115.381C114.304 119.672 113.431 118.799 113.431 117.721V117.721Z" fill="#07C05F"/>
<rect x="97.8271" y="115.121" width="9.10166" height="3.90071" rx="1.95036" fill="#439DF1"/>
<rect x="113.431" y="115.121" width="9.10166" height="3.90071" rx="1.95036" fill="#39CD80"/>
<path d="M87.4705 71.1458L102.226 88.5665C102.926 89.3929 102.339 90.6604 101.256 90.6604L93.4597 90.6604C92.7819 90.6604 92.2232 91.1918 92.1894 91.8688L91.2554 110.568C91.2153 111.37 90.5531 112 89.7498 112L83.2502 112C82.4469 112 81.7847 111.37 81.7446 110.568L80.8106 91.8688C80.7768 91.1918 80.2181 90.6604 79.5403 90.6604L71.7441 90.6604C70.661 90.6604 70.0735 89.3929 70.7736 88.5665L85.5295 71.1458C86.0375 70.546 86.9625 70.546 87.4705 71.1458Z" fill="url(#paint3_linear_6022_51714)"/>
<defs>
<filter id="filter0_d_6022_51714" x="0.789706" y="24.8949" width="162.421" height="116.421" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.10515"/>
<feGaussianBlur stdDeviation="2.10515"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.192691 0 0 0 0 0.192691 0 0 0 0 0.192691 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6022_51714"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6022_51714" result="shape"/>
</filter>
<filter id="filter1_d_6022_51714" x="11" y="32" width="146" height="105" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6022_51714"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6022_51714" result="shape"/>
</filter>
<filter id="filter2_d_6022_51714" x="20" y="41" width="128" height="92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6022_51714"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6022_51714" result="shape"/>
</filter>
<linearGradient id="paint0_linear_6022_51714" x1="82" y1="27" x2="82" y2="134.714" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0FFF7"/>
<stop offset="1" stop-color="#9EDEBD"/>
</linearGradient>
<linearGradient id="paint1_linear_6022_51714" x1="84" y1="131" x2="84" y2="34" gradientUnits="userSpaceOnUse">
<stop stop-color="#DBFAE9"/>
<stop offset="1" stop-color="#2CD87E"/>
</linearGradient>
<linearGradient id="paint2_linear_6022_51714" x1="84" y1="43" x2="84" y2="127" gradientUnits="userSpaceOnUse">
<stop stop-color="#F3FFF7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint3_linear_6022_51714" x1="86.5001" y1="73.1689" x2="86.5001" y2="138.762" gradientUnits="userSpaceOnUse">
<stop stop-color="#83C1FA"/>
<stop offset="1" stop-color="#83C1FA" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,100 @@
<svg width="162" height="162" viewBox="0 0 162 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_6022_51731)">
<path d="M36.875 78L20 111.75V133.047C20 140.76 26.2526 147.013 33.9655 147.013H80.75H127.534C135.247 147.013 141.5 140.76 141.5 133.047V111.75L124.625 78H80.75H36.875Z" fill="url(#paint0_linear_6022_51731)"/>
</g>
<path d="M37.125 111.375V77.625L20.25 111.375H37.125Z" fill="url(#paint1_linear_6022_51731)"/>
<path d="M125 111.75V78L141.875 111.75H125Z" fill="url(#paint2_linear_6022_51731)"/>
<path d="M77.9864 108.627L66.274 93.7436C65.4029 92.6365 66.1915 91.0125 67.6002 91.0125H72.635C73.567 91.0125 74.3292 90.2625 74.2011 89.3394C72.6754 78.3503 56.8805 59.4355 33.1007 50.8549C32.1729 50.5201 32.4067 48.9375 33.393 48.9375H125.232C126.218 48.9375 126.452 50.5201 125.524 50.8549C101.744 59.4355 85.9496 78.3503 84.4239 89.3394C84.2957 90.2625 85.058 91.0125 85.99 91.0125H91.0248C92.4335 91.0125 93.2221 92.6365 92.351 93.7436L80.6386 108.627C79.963 109.486 78.662 109.486 77.9864 108.627Z" fill="url(#paint3_linear_6022_51731)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.604 111.375H20.25V133.138C20.25 140.594 26.2942 146.638 33.75 146.638H128.25C135.706 146.638 141.75 140.594 141.75 133.138V111.375H94.3951C93.5647 118.034 87.8839 123.188 80.9995 123.188C74.1152 123.188 68.4344 118.034 67.604 111.375Z" fill="url(#paint4_linear_6022_51731)"/>
<path d="M46.9366 18.5436C46.7408 18.1411 46.7129 17.6774 46.8591 17.2544L49.1102 10.7375C49.871 8.5352 52.2729 7.36661 54.4752 8.12734L68.8304 13.0861C71.0326 13.8468 72.2012 16.2488 71.4405 18.4511L67.5837 29.6162C66.8229 31.8185 64.421 32.9871 62.2187 32.2263L52.5117 28.8732C52.0887 28.7271 51.7411 28.4189 51.5453 28.0165L46.9366 18.5436Z" fill="url(#paint5_linear_6022_51731)"/>
<mask id="mask0_6022_51731" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="46" y="7" width="26" height="26">
<path d="M46.9366 18.5436C46.7408 18.1411 46.7129 17.6774 46.8591 17.2544L49.1102 10.7375C49.871 8.5352 52.2729 7.36661 54.4752 8.12734L68.8304 13.0861C71.0326 13.8468 72.2012 16.2488 71.4405 18.4511L67.5837 29.6162C66.8229 31.8185 64.421 32.9871 62.2187 32.2263L52.5117 28.8732C52.0887 28.7271 51.7411 28.4189 51.5453 28.0165L46.9366 18.5436Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_6022_51731)">
<path d="M43.876 25.8901L46.6308 17.915L51.4159 19.568C53.1777 20.1765 54.1126 22.0981 53.504 23.8599L51.8511 28.645L43.876 25.8901Z" fill="#B5ECCF"/>
</g>
<path d="M89.5572 16.4385C89.6665 16.1181 89.8987 15.8543 90.2025 15.7051L94.8832 13.4066C96.4649 12.6299 98.3768 13.2825 99.1535 14.8642L104.217 25.1746C104.993 26.7563 104.341 28.6682 102.759 29.445L94.7398 33.3829C93.1581 34.1596 91.2462 33.5071 90.4695 31.9253L87.0458 24.9535C86.8966 24.6496 86.8742 24.299 86.9836 23.9786L89.5572 16.4385Z" fill="url(#paint6_linear_6022_51731)"/>
<mask id="mask1_6022_51731" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="86" y="13" width="19" height="21">
<path d="M89.5572 16.4385C89.6665 16.1181 89.8987 15.8543 90.2025 15.7051L94.8832 13.4066C96.4649 12.6299 98.3768 13.2825 99.1535 14.8642L104.217 25.1746C104.993 26.7563 104.341 28.6682 102.759 29.445L94.7398 33.3829C93.1581 34.1596 91.2462 33.5071 90.4695 31.9253L87.0458 24.9535C86.8966 24.6496 86.8742 24.299 86.9836 23.9786L89.5572 16.4385Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask1_6022_51731)">
<path d="M84 18.751L89.728 15.9382L91.4157 19.375C92.0371 20.6403 91.515 22.1699 90.2496 22.7913L86.8128 24.479L84 18.751Z" fill="#E7E7E7"/>
</g>
<path d="M46.3734 57.2289C46.2502 57.6125 45.9796 57.9315 45.6213 58.1157L40.1001 60.9532C38.2344 61.9121 35.9445 61.1769 34.9857 59.3111L28.7354 47.1494C27.7766 45.2836 28.5118 42.9938 30.3775 42.0349L39.8366 37.1736C41.7024 36.2148 43.9922 36.95 44.9511 38.8157L49.1775 47.0395C49.3617 47.3979 49.3959 47.8147 49.2728 48.1984L46.3734 57.2289Z" fill="url(#paint7_linear_6022_51731)"/>
<mask id="mask2_6022_51731" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="28" y="36" width="22" height="26">
<path d="M46.3734 57.2289C46.2502 57.6125 45.9796 57.9315 45.6213 58.1157L40.1001 60.9532C38.2344 61.9121 35.9445 61.1769 34.9857 59.3111L28.7354 47.1494C27.7766 45.2836 28.5118 42.9938 30.3775 42.0349L39.8366 37.1736C41.7024 36.2148 43.9922 36.95 44.9511 38.8157L49.1775 47.0395C49.3617 47.3979 49.3959 47.8147 49.2728 48.1984L46.3734 57.2289Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask2_6022_51731)">
<path d="M52.9375 54.3557L46.181 57.8281L44.0976 53.7742C43.3305 52.2816 43.9186 50.4497 45.4112 49.6826L49.4651 47.5992L52.9375 54.3557Z" fill="#E7E7E7"/>
</g>
<path d="M120.88 37.3892C121.203 37.4758 121.479 37.6872 121.646 37.9769L124.223 42.4399C125.093 43.9481 124.577 45.8766 123.069 46.7474L113.238 52.4233C111.729 53.294 109.801 52.7773 108.93 51.2691L104.515 43.6228C103.645 42.1146 104.161 40.1861 105.67 39.3153L112.317 35.4773C112.607 35.31 112.951 35.2647 113.274 35.3513L120.88 37.3892Z" fill="url(#paint8_linear_6022_51731)"/>
<mask id="mask3_6022_51731" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="104" y="35" width="21" height="18">
<path d="M120.88 37.3892C121.203 37.4758 121.479 37.6872 121.646 37.9769L124.223 42.4399C125.093 43.9481 124.577 45.8766 123.069 46.7474L113.238 52.4233C111.729 53.294 109.801 52.7773 108.93 51.2691L104.515 43.6228C103.645 42.1146 104.161 40.1861 105.67 39.3153L112.317 35.4773C112.607 35.31 112.951 35.2647 113.274 35.3513L120.88 37.3892Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask3_6022_51731)">
<path d="M118.231 32.0627L121.385 37.5244L118.108 39.4164C116.901 40.113 115.358 39.6996 114.662 38.493L112.77 35.216L118.231 32.0627Z" fill="#B5ECCF"/>
</g>
<path d="M73.3483 45.0984C73.3478 44.8468 73.4473 44.6053 73.6248 44.427L76.3603 41.6805C77.2847 40.7524 78.7864 40.7494 79.7145 41.6738L85.7643 47.6993C86.6924 48.6237 86.6955 50.1255 85.7711 51.0536L81.0845 55.759C80.1601 56.6871 78.6584 56.6901 77.7303 55.7657L73.6394 51.6912C73.4611 51.5137 73.3607 51.2726 73.3602 51.021L73.3483 45.0984Z" fill="url(#paint9_linear_6022_51731)"/>
<mask id="mask4_6022_51731" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="73" y="40" width="14" height="17">
<path d="M73.3483 45.0984C73.3478 44.8468 73.4473 44.6053 73.6248 44.427L76.3603 41.6805C77.2847 40.7524 78.7864 40.7494 79.7145 41.6738L85.7643 47.6993C86.6924 48.6237 86.6955 50.1255 85.7711 51.0536L81.0845 55.759C80.1601 56.6871 78.6584 56.6901 77.7303 55.7657L73.6394 51.6912C73.4611 51.5137 73.3607 51.2726 73.3602 51.021L73.3483 45.0984Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask4_6022_51731)">
<path d="M70 48.0664L73.3475 44.7054L75.3641 46.7139C76.1066 47.4534 76.109 48.6548 75.3695 49.3973L73.361 51.4139L70 48.0664Z" fill="#07C05F"/>
</g>
<path d="M106.138 120.103C106.138 118.946 107.076 118.009 108.233 118.009H113.819C114.976 118.009 115.914 118.946 115.914 120.103V120.103C115.914 121.26 114.976 122.198 113.819 122.198H108.233C107.076 122.198 106.138 121.26 106.138 120.103V120.103Z" fill="#1485EE"/>
<path d="M122.896 120.103C122.896 118.946 123.834 118.009 124.991 118.009H130.578C131.734 118.009 132.672 118.946 132.672 120.103V120.103C132.672 121.26 131.734 122.198 130.578 122.198H124.991C123.834 122.198 122.896 121.26 122.896 120.103V120.103Z" fill="#07C05F"/>
<rect x="106.138" y="117.31" width="9.77586" height="4.18966" rx="2.09483" fill="#439DF1"/>
<rect x="122.896" y="117.31" width="9.77586" height="4.18966" rx="2.09483" fill="#39CD80"/>
<defs>
<filter id="filter0_d_6022_51731" x="14.4138" y="75.2069" width="132.672" height="80.1854" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.7931"/>
<feGaussianBlur stdDeviation="2.7931"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.192691 0 0 0 0 0.192691 0 0 0 0 0.192691 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6022_51731"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6022_51731" result="shape"/>
</filter>
<linearGradient id="paint0_linear_6022_51731" x1="80.75" y1="78" x2="80.75" y2="132" gradientUnits="userSpaceOnUse">
<stop stop-color="#E4F9EE"/>
<stop offset="1" stop-color="#9EDEBD"/>
</linearGradient>
<linearGradient id="paint1_linear_6022_51731" x1="28.6875" y1="77.625" x2="28.6875" y2="111.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#DBFAE9"/>
<stop offset="1" stop-color="#2CD87E"/>
</linearGradient>
<linearGradient id="paint2_linear_6022_51731" x1="133.438" y1="78" x2="133.438" y2="111.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#DBFAE9"/>
<stop offset="1" stop-color="#2CD87E"/>
</linearGradient>
<linearGradient id="paint3_linear_6022_51731" x1="79.3125" y1="106.312" x2="79.3125" y2="48.9375" gradientUnits="userSpaceOnUse">
<stop stop-color="#83C1FA"/>
<stop offset="1" stop-color="#83C1FA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint4_linear_6022_51731" x1="81" y1="111.375" x2="81" y2="146.638" gradientUnits="userSpaceOnUse">
<stop stop-color="#F3FFF7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint5_linear_6022_51731" x1="47.1818" y1="16.32" x2="69.5121" y2="24.0336" gradientUnits="userSpaceOnUse">
<stop stop-color="#07C05F"/>
<stop offset="1" stop-color="#07C05F" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint6_linear_6022_51731" x1="90.8736" y1="15.3756" x2="98.7494" y2="31.4139" gradientUnits="userSpaceOnUse">
<stop stop-color="#C5C5C5"/>
<stop offset="1" stop-color="#C5C5C5" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint7_linear_6022_51731" x1="44.8297" y1="58.5225" x2="35.1071" y2="39.6043" gradientUnits="userSpaceOnUse">
<stop stop-color="#C5C5C5"/>
<stop offset="1" stop-color="#C5C5C5" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear_6022_51731" x1="121.732" y1="38.1262" x2="105.974" y2="45.8244" gradientUnits="userSpaceOnUse">
<stop stop-color="#54E89A"/>
<stop offset="1" stop-color="#07C05F" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint9_linear_6022_51731" x1="73.6562" y1="44.3955" x2="82.2446" y2="54.3731" gradientUnits="userSpaceOnUse">
<stop stop-color="#6EE1A5"/>
<stop offset="1" stop-color="#6EE1A5" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="cvw0hqhj9__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.17736 1.28207C9.32257 1.23367 9.4805 1.24024 9.6212 1.30054L18.3713 5.05054C18.5619 5.13222 18.6995 5.30316 18.7388 5.50681C18.778 5.71045 18.7136 5.9203 18.567 6.06694C18.5138 6.12012 18.4366 6.24314 18.3744 6.46076C18.3146 6.66994 18.2812 6.92361 18.2812 7.1875C18.2812 7.45139 18.3146 7.70506 18.3744 7.91424C18.4366 8.13186 18.5138 8.25487 18.567 8.30806C18.811 8.55214 18.811 8.94786 18.567 9.19194C18.5138 9.24512 18.4366 9.36814 18.3744 9.58576C18.3146 9.79494 18.2812 10.0486 18.2812 10.3125C18.2812 10.5764 18.3146 10.8301 18.3744 11.0392C18.4366 11.2569 18.5138 11.3799 18.567 11.4331C18.811 11.6771 18.811 12.0729 18.567 12.3169C18.5138 12.3701 18.4366 12.4931 18.3744 12.7108C18.3146 12.92 18.2812 13.1736 18.2812 13.4375C18.2812 13.7014 18.3146 13.955 18.3744 14.1642C18.4366 14.3819 18.5138 14.5049 18.567 14.558C18.709 14.7001 18.7741 14.9017 18.7419 15.1001C18.7097 15.2984 18.5843 15.4691 18.4045 15.559L12.1545 18.684C11.9886 18.767 11.7944 18.772 11.6245 18.6976L1.62449 14.3226C1.397 14.2231 1.25 13.9983 1.25 13.75V4.375C1.25 4.10599 1.42215 3.86715 1.67736 3.78207L9.17736 1.28207ZM2.5 5.33064L10.9868 9.04362C10.9627 9.10997 10.9413 9.1765 10.9225 9.24236C10.826 9.58006 10.7812 9.95139 10.7812 10.3125C10.7812 10.6736 10.826 11.0449 10.9225 11.3826C10.9694 11.5469 11.0321 11.7153 11.1153 11.875C11.0321 12.0347 10.9694 12.2031 10.9225 12.3674C10.826 12.705 10.7812 13.0764 10.7812 13.4375C10.7812 13.7986 10.826 14.17 10.9225 14.5076C10.9694 14.6719 11.0321 14.8403 11.1153 15C11.0321 15.1597 10.9694 15.3281 10.9225 15.4924C10.826 15.83 10.7812 16.2014 10.7812 16.5625C10.7812 16.6986 10.7876 16.8361 10.8007 16.9728L2.5 13.3413V5.33064ZM12.1275 17.3C12.1265 17.2964 12.1254 17.2929 12.1244 17.2892C12.0646 17.08 12.0312 16.8264 12.0312 16.5625C12.0312 16.2986 12.0646 16.045 12.1244 15.8358C12.1695 15.6779 12.2225 15.5697 12.2686 15.502L17.0432 13.1146C17.0351 13.2224 17.0312 13.3304 17.0312 13.4375C17.0312 13.7986 17.076 14.17 17.1725 14.5076C17.1944 14.5844 17.2198 14.662 17.249 14.7393L12.1275 17.3ZM17.249 11.6142L12.1275 14.175C12.1265 14.1714 12.1254 14.1679 12.1244 14.1642C12.0646 13.955 12.0312 13.7014 12.0312 13.4375C12.0312 13.1736 12.0646 12.92 12.1244 12.7108C12.1695 12.5529 12.2225 12.4447 12.2686 12.377L17.0432 9.98965C17.0351 10.0974 17.0312 10.2054 17.0312 10.3125C17.0312 10.6736 17.076 11.0449 17.1725 11.3826C17.1944 11.4594 17.2198 11.537 17.249 11.6142ZM17.249 8.48921L12.1275 11.05C12.1265 11.0464 12.1254 11.0428 12.1244 11.0392C12.0646 10.8301 12.0312 10.5764 12.0312 10.3125C12.0312 10.0486 12.0646 9.79494 12.1244 9.58576C12.1695 9.42785 12.2225 9.31975 12.2686 9.252L17.0432 6.86465C17.0351 6.9724 17.0312 7.0804 17.0312 7.1875C17.0312 7.54861 17.076 7.91994 17.1725 8.25764C17.1944 8.33437 17.2198 8.412 17.249 8.48921ZM16.6402 5.66864L9.34721 2.54307L3.61469 4.45391L11.8573 8.06006L16.6402 5.66864Z" fill="#07C05F"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" class="cvw0hqhj9__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.17736 1.28207C9.32257 1.23367 9.4805 1.24024 9.6212 1.30054L18.3713 5.05054C18.5619 5.13222 18.6995 5.30316 18.7388 5.50681C18.778 5.71045 18.7136 5.9203 18.567 6.06694C18.5138 6.12012 18.4366 6.24314 18.3744 6.46076C18.3146 6.66994 18.2812 6.92361 18.2812 7.1875C18.2812 7.45139 18.3146 7.70506 18.3744 7.91424C18.4366 8.13186 18.5138 8.25487 18.567 8.30806C18.811 8.55214 18.811 8.94786 18.567 9.19194C18.5138 9.24512 18.4366 9.36814 18.3744 9.58576C18.3146 9.79494 18.2812 10.0486 18.2812 10.3125C18.2812 10.5764 18.3146 10.8301 18.3744 11.0392C18.4366 11.2569 18.5138 11.3799 18.567 11.4331C18.811 11.6771 18.811 12.0729 18.567 12.3169C18.5138 12.3701 18.4366 12.4931 18.3744 12.7108C18.3146 12.92 18.2812 13.1736 18.2812 13.4375C18.2812 13.7014 18.3146 13.955 18.3744 14.1642C18.4366 14.3819 18.5138 14.5049 18.567 14.558C18.709 14.7001 18.7741 14.9017 18.7419 15.1001C18.7097 15.2984 18.5843 15.4691 18.4045 15.559L12.1545 18.684C11.9886 18.767 11.7944 18.772 11.6245 18.6976L1.62449 14.3226C1.397 14.2231 1.25 13.9983 1.25 13.75V4.375C1.25 4.10599 1.42215 3.86715 1.67736 3.78207L9.17736 1.28207ZM2.5 5.33064L10.9868 9.04362C10.9627 9.10997 10.9413 9.1765 10.9225 9.24236C10.826 9.58006 10.7812 9.95139 10.7812 10.3125C10.7812 10.6736 10.826 11.0449 10.9225 11.3826C10.9694 11.5469 11.0321 11.7153 11.1153 11.875C11.0321 12.0347 10.9694 12.2031 10.9225 12.3674C10.826 12.705 10.7812 13.0764 10.7812 13.4375C10.7812 13.7986 10.826 14.17 10.9225 14.5076C10.9694 14.6719 11.0321 14.8403 11.1153 15C11.0321 15.1597 10.9694 15.3281 10.9225 15.4924C10.826 15.83 10.7812 16.2014 10.7812 16.5625C10.7812 16.6986 10.7876 16.8361 10.8007 16.9728L2.5 13.3413V5.33064ZM12.1275 17.3C12.1265 17.2964 12.1254 17.2929 12.1244 17.2892C12.0646 17.08 12.0312 16.8264 12.0312 16.5625C12.0312 16.2986 12.0646 16.045 12.1244 15.8358C12.1695 15.6779 12.2225 15.5697 12.2686 15.502L17.0432 13.1146C17.0351 13.2224 17.0312 13.3304 17.0312 13.4375C17.0312 13.7986 17.076 14.17 17.1725 14.5076C17.1944 14.5844 17.2198 14.662 17.249 14.7393L12.1275 17.3ZM17.249 11.6142L12.1275 14.175C12.1265 14.1714 12.1254 14.1679 12.1244 14.1642C12.0646 13.955 12.0312 13.7014 12.0312 13.4375C12.0312 13.1736 12.0646 12.92 12.1244 12.7108C12.1695 12.5529 12.2225 12.4447 12.2686 12.377L17.0432 9.98965C17.0351 10.0974 17.0312 10.2054 17.0312 10.3125C17.0312 10.6736 17.076 11.0449 17.1725 11.3826C17.1944 11.4594 17.2198 11.537 17.249 11.6142ZM17.249 8.48921L12.1275 11.05C12.1265 11.0464 12.1254 11.0428 12.1244 11.0392C12.0646 10.8301 12.0312 10.5764 12.0312 10.3125C12.0312 10.0486 12.0646 9.79494 12.1244 9.58576C12.1695 9.42785 12.2225 9.31975 12.2686 9.252L17.0432 6.86465C17.0351 6.9724 17.0312 7.0804 17.0312 7.1875C17.0312 7.54861 17.076 7.91994 17.1725 8.25764C17.1944 8.33437 17.2198 8.412 17.249 8.48921ZM16.6402 5.66864L9.34721 2.54307L3.61469 4.45391L11.8573 8.06006L16.6402 5.66864Z" fill="#717171"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" class="gh6beppe9__design-iconfont" width="128" height="128">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3C2 2.17157 2.67157 1.5 3.5 1.5H12C12.2761 1.5 12.5 1.72386 12.5 2V11C12.5 11.2761 12.2761 11.5 12 11.5H4C3.44772 11.5 3 11.9477 3 12.5C3 13.0523 3.44772 13.5 4 13.5H13V2.5H14V14C14 14.2761 13.7761 14.5 13.5 14.5H4C2.89543 14.5 2 13.6046 2 12.5V3ZM3 10.7676C3.29417 10.5974 3.63571 10.5 4 10.5H11.5V2.5H3.5C3.22386 2.5 3 2.72386 3 3V10.7676ZM12.5 13H3.5V12H12.5V13Z" fill="#32CE3F"></path>
</svg>

After

Width:  |  Height:  |  Size: 583 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted, defineProps, defineExpose } from "vue";
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
import { onBeforeRouteUpdate } from 'vue-router';
import { MessagePlugin } from "tdesign-vue-next";
let { cardList, total, getKnowled } = useKnowledgeBase()
let query = ref("");
const props = defineProps({
isReplying: {
type: Boolean,
required: false
}
});
onMounted(() => {
getKnowled()
})
const emit = defineEmits(['send-msg']);
const createSession = (val: string) => {
if (!val.trim()) {
MessagePlugin.info("请先输入内容!");
return
}
if (!query.value && cardList.value.length == 0) {
MessagePlugin.info("请先上传知识库!");
return;
}
if (props.isReplying) {
return MessagePlugin.error("正在回复中,请稍后再试!");
}
emit('send-msg', val);
clearvalue();
}
const clearvalue = () => {
query.value = "";
}
const onKeydown = (val: string, event: { e: { preventDefault(): unknown; keyCode: number; shiftKey: any; ctrlKey: any; }; }) => {
if ((event.e.keyCode == 13 && event.e.shiftKey) || (event.e.keyCode == 13 && event.e.ctrlKey)) {
return;
}
if (event.e.keyCode == 13) {
event.e.preventDefault();
createSession(val)
}
}
onBeforeRouteUpdate((to, from, next) => {
clearvalue()
next()
})
</script>
<template>
<div class="answers-input">
<t-textarea v-model="query" placeholder="基于知识库提问" name="description" :autosize="true" @keydown="onKeydown" />
<div class="answers-input-source">
<span>{{ total }}个来源</span>
</div>
<div @click="createSession(query)" class="answers-input-send"
:class="[query.length && total ? '' : 'grey-out']">
<img src="../assets/img/sending-aircraft.svg" alt="">
</div>
</div>
</template>
<style scoped lang="less">
.answers-input {
position: absolute;
z-index: 99;
bottom: 60px;
left: 50%;
transform: translateX(-400px);
}
:deep(.t-textarea__inner) {
width: 100%;
width: 800px;
max-height: 250px !important;
min-height: 112px !important;
resize: none;
color: #000000e6;
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: #000000e6;
font-family: "PingFang SC";
padding: 16px 12px 12px 16px;
border-radius: 12px;
border: 1px solid #E7E7E7;
box-sizing: border-box;
background: #FFF;
box-shadow: 0 6px 6px 0 #0000000a, 0 12px 12px -1px #00000014;
&:focus {
border: 1px solid #07C05F;
}
&:placeholder {
color: #00000066;
font-family: "PingFang SC";
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
.answers-input-send {
background-color: #07C05F;
height: 36px;
width: 36px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 12px;
right: 12px;
cursor: pointer;
border-radius: 8px;
}
.answers-input-source {
position: absolute;
bottom: 12px;
right: 64px;
line-height: 32px;
color: #00000066;
font-family: "PingFang SC";
font-size: 12px;
font-weight: 400;
}
.grey-out {
background-color: #b5eccf !important;
cursor: no-drop !important;
}
</style>

View File

@ -0,0 +1,426 @@
:deep(.md-content) {
box-sizing: border-box !important;
img {
max-width: 444px;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 5px;
font-weight: bold;
color: #00000099;
font-family: "PingFang SC", "Cascadia Code";
transition: all 0.2s ease-out;
font-size: 20px;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
white-space: pre-wrap;
word-break: break-all;
}
.proto {
word-break: break-all;
white-space: pre-wrap;
}
h1 tt,
h1 code {
font-size: inherit !important;
}
h2 tt,
h2 code {
font-size: inherit !important;
}
h3 tt,
h3 code {
font-size: inherit !important;
}
h4 tt,
h4 code {
font-size: inherit !important;
}
h5 tt,
h5 code {
font-size: inherit !important;
}
h6 tt,
h6 code {
font-size: inherit !important;
}
h2 a,
h3 a {
color: #34495e;
}
p,
blockquote,
ul,
ol,
dl,
table {
font-size: 14px;
margin: 10px 0;
font-family: "PingFang SC", "Cascadia Code";
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
font-weight: 500;
}
summary {
font-size: 14px;
cursor: pointer;
}
li>ol,
li>ul {
margin: 0 0;
}
hr {
padding: 0;
margin: 32px 0;
border-top: 0.5rem dotted #0590ff57;
overflow: hidden;
box-sizing: content-box;
}
body>h2:first-child {
margin-top: 0;
padding-top: 0;
}
body>h1:first-child {
margin-top: 0;
padding-top: 0;
}
body>h1:first-child+h2 {
margin-top: 0;
padding-top: 0;
}
body>h3:first-child,
body>h4:first-child,
body>h5:first-child,
body>h6:first-child {
margin-top: 0;
padding-top: 0;
}
a:first-child h1,
a:first-child h2,
a:first-child h3,
a:first-child h4,
a:first-child h5,
a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
p {
margin: 0;
}
code {
white-space: pre-wrap;
word-break: break-all;
}
h1 p,
h2 p,
h3 p,
h4 p,
h5 p,
h6 p {
margin-top: 0;
}
li p.first {
display: inline-block;
}
ul,
ol {
padding-left: 30px;
}
ul:first-child,
ol:first-child {
margin-top: 0;
}
ul:last-child,
ol:last-child {
margin-bottom: 0;
}
blockquote {
padding: 0.8em 1.4rem;
margin: 1em 0;
font-weight: 400;
border-left: 4px solid #2196f3;
background-color: #2196f321;
border-radius: 0px 8px 8px 0px;
box-shadow: rgb(149 149 149 / 13%) 0px 5px 10px;
}
table {
padding: 0;
word-break: initial;
/* border-radius: 4px; */
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
table tr {
border-top: 1px solid #2196f31f;
margin: 0;
padding: 0;
}
table tr:nth-child(2n),
thead {
background-color: #fafafa;
}
table tr th {
font-weight: bold;
border: 1px solid #9b9b9b3b;
border-bottom: 0;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr td {
border: 1px solid #9b9b9b3b;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr th:first-child,
table tr td:first-child {
margin-top: 0;
}
table tr th:last-child,
table tr td:last-child {
margin-bottom: 0;
}
tt {
margin: 0 2px;
}
figure {
border-radius: 8px;
margin-left: 0;
margin-right: 0;
background: #fff;
}
.md-task-list-item>input {
margin-left: -1.3em;
}
@media print {
html {
font-size: 13px;
}
table,
pre {
page-break-inside: avoid;
}
pre {
word-wrap: break-word;
}
}
.md-fences {
background-color: #f8f8f8;
}
.md-diagram-panel {
position: static !important;
}
.mathjax-block>.code-tooltip {
bottom: 0.375rem;
}
h3.md-focus:before,
h4.md-focus:before,
h5.md-focus:before,
h6.md-focus:before {
border: 0px;
position: unset;
padding: 0px;
font-size: unset;
line-height: unset;
float: unset;
}
.md-image>.md-meta {
border-radius: 3px;
font-family: var(--font-monospace);
padding: 2px 0 0 4px;
font-size: 0.9em;
color: inherit;
}
.md-tag {
color: inherit;
}
.md-toc {
margin-top: 20px;
padding-bottom: 20px;
}
.sidebar-tabs {
border-bottom: none;
}
/** focus mode */
.on-focus-mode blockquote {
border-left-color: rgba(85, 85, 85, 0.12);
}
header,
.context-menu,
.megamenu-content,
footer {
font-family: var(--font-sans-serif);
}
.file-node-content:hover .file-node-icon,
.file-node-content:hover .file-node-open-state {
visibility: visible;
}
.mac-seamless-mode #typora-sidebar {
background-color: var(--side-bar-bg-color);
}
.md-lang {
color: #b4654d;
}
.html-for-mac .context-menu {
--item-hover-bg-color: #e6f0fe;
}
.pin-outline #outline-content .outline-active strong,
.pin-outline .outline-active {
color: #2196f3;
}
.code-tooltip {
border-radius: 4px;
border: 1px solid #ededed;
background-color: #f8f8f8;
}
.cm-s-inner .cm-comment,
.cm-s-inner.cm-comment {
color: #57a64a;
font-style: italic;
/* font-family: 'PingFang'; */
}
h1.md-end-block.md-heading:after,
h2.md-end-block.md-heading:after,
h3.md-end-block.md-heading:after,
h4.md-end-block.md-heading:after,
h5.md-end-block.md-heading:after,
h6.md-end-block.md-heading:after {
color: #bfbfbf !important;
border: 1px solid;
border-radius: 4px;
position: absolute;
left: -2.5rem;
float: left;
font-size: 14px;
padding-left: 4px;
padding-right: 5px;
vertical-align: bottom;
font-weight: 400;
line-height: normal;
opacity: 0;
}
h1.md-end-block.md-heading:hover:after,
h2.md-end-block.md-heading:hover:after,
h3.md-end-block.md-heading:hover:after,
h4.md-end-block.md-heading:hover:after,
h5.md-end-block.md-heading:hover:after,
h6.md-end-block.md-heading:hover:after {
opacity: 1;
}
h1.md-end-block.md-heading:hover:after {
content: "h1";
top: 1.1rem;
}
h2.md-end-block.md-heading:hover:after {
content: "h2";
top: 0.63rem;
}
h3.md-end-block.md-heading:hover:after {
content: "h3";
top: 0.55rem;
}
h4.md-end-block.md-heading:hover:after {
content: "h4";
top: 0.3rem;
}
h5.md-end-block.md-heading:hover:after {
content: "h5";
top: 0.18rem;
}
h6.md-end-block.md-heading:hover:after {
content: "h6";
top: 0.16rem;
}
.outline-label {
font-family: "Cascadia Code", "PingFang SC";
}
}

View File

@ -0,0 +1,242 @@
<script setup lang="ts">
import { marked } from "marked";
import { onMounted, ref, nextTick, onUnmounted, onUpdated, watch } from "vue";
import { downKnowledgeDetails } from "@/api/knowledge-base/index";
import { MessagePlugin } from "tdesign-vue-next";
import picturePreview from '@/components/picture-preview.vue';
marked.use({
mangle: false,
headerIds: false,
});
const renderer = new marked.Renderer();
let page = 1;
let doc = null;
let down = ref()
let mdContentWrap = ref()
let url = ref('')
let reviewUrl = ref('')
let reviewImg = ref(false)
onMounted(() => {
nextTick(() => {
doc = document.getElementsByClassName('t-drawer__body')[0]
doc.addEventListener('scroll', handleDetailsScroll);
})
})
onUpdated(() => {
page = 1
})
onUnmounted(() => {
doc.removeEventListener('scroll', handleDetailsScroll);
})
const checkImage = (url) => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
});
};
renderer.image = function (href, title, text) {
// HTML
return `<figure>
<img class="markdown-image" src="${href}" alt="${title}" title="${text}">
<figcaption style="text-align: left;">${text}</figcaption>
</figure>`;
};
const props = defineProps(["visible", "details"]);
const emit = defineEmits(["closeDoc", "getDoc"]);
watch(() => props.details.md, (newVal) => {
nextTick(async () => {
const images = mdContentWrap.value.querySelectorAll('img.markdown-image');
if (images) {
images.forEach(async item => {
item.addEventListener('click', function (event) {
reviewUrl.value = event.target.src;
reviewImg.value = true
})
const isValid = await checkImage(item.src);
if (!isValid) {
item.remove();
}
})
}
})
}, {
immediate: true,
deep: true
})
// Markdown
const processMarkdown = (markdownText) => {
//
marked.use({ renderer });
let html = marked.parse(markdownText);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc.body.innerHTML;
};
const closePreImg = () => {
reviewImg.value = false
reviewUrl.value = '';
}
const handleClose = () => {
emit("closeDoc", false);
doc.scrollTop = 0;
};
const downloadFile = () => {
downKnowledgeDetails(props.details.id)
.then((result) => {
if (result) {
url.value = URL.createObjectURL(result);
down.value.click();
// const link = document.createElement("a");
// link.style.display = "none";
// link.setAttribute("href", url);
// link.setAttribute("download", props.details.title);
// link.click();
// document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
})
.catch((err) => {
MessagePlugin.error("获取文件失败!");
});
};
const handleDetailsScroll = () => {
if (doc) {
let pageNum = Math.ceil(props.details.total / 20);
const { scrollTop, scrollHeight, clientHeight } = doc;
if (scrollTop + clientHeight >= scrollHeight) {
page++;
if (props.details.md.length < props.details.total && page <= pageNum) {
emit("getDoc", page);
}
}
}
};
</script>
<template>
<div class="doc_content" ref="mdContentWrap">
<t-drawer :visible="visible" :zIndex="2000" :closeBtn="true" @close="handleClose">
<template #header>{{
details.title.substring(0, details.title.lastIndexOf("."))
}}</template>
<div class="doc_box">
<a :href="url" style="display: none" ref="down" :download="details.title"></a>
<span class="label">文档标题</span>
<div class="download_box">
<span class="doc_t">{{ details.title }}</span>
<div class="icon_box" @click="downloadFile()">
<img class="download_box" src="@/assets/img/download.svg" alt="">
</div>
</div>
</div>
<div class="content_header">
<span class="label">文档内容</span>
<span class="time"> 更新时间{{ details.time }} </span>
</div>
<div v-if="details.md.length == 0" class="no_content">暂无数据</div>
<div v-else class="content" v-for="(item, index) in details.md" :key="index" :style="index % 2 !== 0
? 'background: #07c05f26;'
: 'background: #3032360f;'
">
<div class="md-content" v-html="processMarkdown(item.content)"></div>
</div>
<template #footer>
<t-button @click="handleClose">确定</t-button>
<t-button theme="default" @click="handleClose">取消</t-button>
</template>
</t-drawer>
<picturePreview :reviewImg="reviewImg" :reviewUrl="reviewUrl" @closePreImg="closePreImg"></picturePreview>
</div>
</template>
<style scoped lang="less">
@import "./css/markdown.less";
:deep(.t-drawer .t-drawer__content-wrapper) {
width: 654px !important;
}
:deep(.t-drawer__header) {
font-weight: 800;
}
:deep(.t-drawer__body.narrow-scrollbar) {
padding: 16px 24px;
}
.content {
word-break: break-word;
padding: 4px;
gap: 4px;
margin-top: 12px;
}
.doc_box {
display: flex;
flex-direction: column;
}
.label {
color: #000000e6;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 22px;
margin-bottom: 8px;
}
.download_box {
display: flex;
align-items: center;
}
.doc_t {
box-sizing: border-box;
display: flex;
padding: 5px 8px;
align-items: center;
border-radius: 3px;
border: 1px solid #dcdcdc;
background: #30323605;
word-break: break-all;
text-align: justify;
}
.icon_box {
margin-left: 18px;
display: flex;
overflow: hidden;
color: #07c05f;
.download_box {
width: 16px;
height: 16px;
fill: currentColor;
overflow: hidden;
cursor: pointer;
}
}
.content_header {
margin-top: 22px;
margin-bottom: 24px;
}
.time {
margin-left: 12px;
color: #00000066;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.no_content {
margin-top: 12px;
color: #00000066;
font-size: 12px;
padding: 16px;
background: #fbfbfb;
}
</style>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
</script>
<template>
<div class="empty">
<img class="empty-img" src="@/assets/img/upload.svg" alt="">
<span class="empty-txt">知识为空拖放上传</span>
<span class="empty-type-txt">pdfdoc 格式文件不超过10M</span>
<span class="empty-type-txt">textmarkdown格式文件不超过200K</span>
</div>
</template>
<style scoped lang="less">
.empty {
flex: 1;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.empty-txt {
color: #00000099;
font-family: "PingFang SC";
font-size: 16px;
font-weight: 600;
line-height: 26px;
margin: 12px 0 16px 0;
}
.empty-type-txt {
color: #00000066;
text-align: center;
font-family: "PingFang SC";
font-size: 12px;
font-weight: 400;
width: 217px;
}
.empty-img {
width: 162px;
height: 162px;
}
</style>

View File

@ -0,0 +1,459 @@
<template>
<div class="aside_box">
<div class="logo_box">
<img class="logo" src="@/assets/img/weknora.png" alt="">
</div>
<div class="menu_box" v-for="(item, index) in menuArr" :key="index">
<div @click="gotopage(item.path)"
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
<div class="menu_item-box">
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : prefixIcon)" alt="">
</div>
<span class="menu_title">{{ item.title }}</span>
</div>
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
placement="top" show-arrow destroy-on-close>
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
v-if="item.path == 'knowledgeBase'">
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
:src="getImgSrc(fileAddIcon)" alt="">
</div>
</t-popup>
</div>
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
<div class="submenu_item_p" v-for="(subitem, subindex) in item.children" :key="subindex"
@click="gotopage(subitem.path)">
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
@mouseenter="mouseenteBotDownr(subindex)" @mouseleave="mouseleaveBotDown">
<i v-if="currentSecondpath == subitem.path" class="dot"></i>
<span class="submenu_title"
:style="currentSecondpath == subitem.path ? 'margin-left:14px;max-width:160px;' : 'margin-left:18px;max-width:173px;'">
{{ subitem.title }}
</span>
<t-popup v-model:visible="subitem.isMore" @overlay-click="delCard(subindex, subitem)"
@visible-change="onVisibleChange" overlayClassName="del-menu-popup" trigger="click"
destroy-on-close placement="top-left">
<div v-if="(activeSubmenu == subindex) || (currentSecondpath == subitem.path) || subitem.isMore"
@click.stop="openMore(subindex)" variant="outline" class="menu-more-wrap">
<t-icon name="ellipsis" class="menu-more" />
</div>
<template #content>
<span class="del_submenu">删除记录</span>
</template>
</t-popup>
</div>
</div>
</div>
</div>
<input type="file" @change="upload" style="display: none" ref="uploadInput"
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png" />
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { onMounted, watch, computed, ref, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getSessionsList, delSession } from "@/api/chat/index";
import { useMenuStore } from '@/stores/menu';
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
import { MessagePlugin } from "tdesign-vue-next";
let { requestMethod } = useKnowledgeBase()
let uploadInput = ref();
const usemenuStore = useMenuStore();
const route = useRoute();
const router = useRouter();
const currentpath = ref('');
const currentPage = ref(1);
const page_size = ref(30);
const total = ref(0);
const currentSecondpath = ref('');
const submenuscrollContainer = ref(null);
//
const totalPages = computed(() => Math.ceil(total.value / page_size.value));
const hasMore = computed(() => currentPage.value < totalPages.value);
const { menuArr } = storeToRefs(usemenuStore);
let activeSubmenu = ref(-1);
const loading = ref(false)
const uploadFile = () => {
uploadInput.value.click()
}
const upload = (e) => {
requestMethod(e.target.files[0], uploadInput)
}
const mouseenteBotDownr = (val) => {
activeSubmenu.value = val;
}
const mouseleaveBotDown = () => {
activeSubmenu.value = -1;
}
const onVisibleChange = (e) => {
}
const delCard = (index, item) => {
delSession(item.id).then(res => {
if (res && res.success) {
menuArr.value[1].children.splice(index, 1);
if (item.id == route.params.chatid) {
router.push('/platform/creatChat');
}
} else {
MessagePlugin.error("删除失败,请稍后再试!");
}
})
}
const debounce = (fn, delay) => {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
//
const checkScrollBottom = () => {
const container = submenuscrollContainer.value
if (!container) return
const { scrollTop, scrollHeight, clientHeight } = container[0]
const isBottom = scrollHeight - (scrollTop + clientHeight) < 100 //
if (isBottom && hasMore.value) {
currentPage.value++;
getMessageList();
}
}
const handleScroll = debounce(checkScrollBottom, 200)
const getMessageList = () => {
if (loading.value) return;
loading.value = true;
usemenuStore.clearMenuArr();
getSessionsList(currentPage.value, page_size.value).then(res => {
if (res.data && res.data.length) {
res.data.forEach(item => {
let obj = { title: item.title ? item.title : "新会话", path: `chat/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
usemenuStore.updatemenuArr(obj)
});
loading.value = false;
}
if (res.total) {
total.value = res.total;
}
})
}
const openMore = (e) => { }
onMounted(() => {
currentpath.value = route.name;
if (route.params.chatid) {
currentSecondpath.value = `${route.name}/${route.params.chatid}`;
}
getMessageList();
});
watch([() => route.name, () => route.params], (newvalue) => {
currentpath.value = newvalue[0];
if (newvalue[1].chatid) {
currentSecondpath.value = `${newvalue[0]}/${newvalue[1].chatid}`;
} else {
currentSecondpath.value = "";
}
});
let fileAddIcon = ref('file-add-green.svg');
let knowledgeIcon = ref('zhishiku-green.svg');
let prefixIcon = ref('prefixIcon.svg');
let settingIcon = ref('setting.svg');
let pathPrefix = ref(route.name)
const getIcon = (path) => {
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
}
getIcon(route.name)
const gotopage = (path) => {
pathPrefix.value = path;
//
if (path === 'settings') {
router.push('/initialization');
} else {
router.push(`/platform/${path}`);
}
getIcon(path)
}
const getImgSrc = (url) => {
return new URL(`/src/assets/img/${url}`, import.meta.url).href;
}
const mouseenteMenu = (path) => {
if (pathPrefix.value != 'knowledgeBase' && pathPrefix.value != 'creatChat' && path != 'knowledgeBase') {
prefixIcon.value = 'prefixIcon-grey.svg';
}
}
const mouseleaveMenu = (path) => {
if (pathPrefix.value != 'knowledgeBase' && pathPrefix.value != 'creatChat' && path != 'knowledgeBase') {
getIcon(route.name)
}
}
</script>
<style lang="less" scoped>
.del_submenu {
color: #fa5151;
cursor: pointer;
}
.aside_box {
min-width: 260px;
padding: 8px;
background: #fff;
box-sizing: border-box;
.logo_box {
height: 80px;
display: flex;
align-items: center;
.logo{
width: 134px;
height: auto;
margin-left: 24px;
}
}
.logo_img {
margin-left: 24px;
width: 30px;
height: 30px;
margin-right: 7.25px;
}
.logo_txt {
transform: rotate(0.049deg);
color: #000000;
font-family: "TencentSans";
font-size: 24.12px;
font-style: normal;
font-weight: W7;
line-height: 21.7px;
}
.menu_box {
display: flex;
flex-direction: column;
}
.upload-file-wrap {
padding: 6px;
border-radius: 3px;
height: 32px;
width: 32px;
box-sizing: border-box;
}
.upload-file-wrap:hover {
background-color: #dbede4;
color: #07C05F;
}
.upload-file-icon {
width: 20px;
height: 20px;
color: rgba(0, 0, 0, 0.6);
}
.active-upload {
color: #07C05F;
}
.menu_item_active {
border-radius: 4px;
background: #07c05f1a !important;
.menu_icon,
.menu_title {
color: #07c05f !important;
}
}
.menu_item_c_active {
.menu_icon,
.menu_title {
color: #000000e6;
}
}
.menu_p {
height: 56px;
padding: 6px 0;
box-sizing: border-box;
}
.menu_item {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: 13px 8px 13px 16px;
box-sizing: border-box;
margin-bottom: 4px;
.menu_item-box {
display: flex;
align-items: center;
}
&:hover {
border-radius: 4px;
background: #30323605;
color: #00000099;
.menu_icon,
.menu_title {
color: #00000099;
}
}
}
.menu_icon {
display: flex;
margin-right: 10px;
color: #00000099;
.icon {
width: 20px;
height: 20px;
fill: currentColor;
overflow: hidden;
}
}
.menu_title {
color: #00000099;
text-overflow: ellipsis;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 22px;
}
.submenu {
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
overflow-y: scroll;
scrollbar-width: none;
height: calc(98vh - 276px);
}
.submenu_item_p {
height: 44px;
padding: 4px 8px 4px 12px;
box-sizing: border-box;
}
.submenu_item {
cursor: pointer;
display: flex;
align-items: center;
color: #00000099;
font-weight: 400;
line-height: 22px;
height: 36px;
padding-left: 18px;
padding-right: 14px;
position: relative;
.submenu_title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.menu-more-wrap {
margin-left: auto;
}
.menu-more {
display: inline-block;
font-weight: bold;
color: #07C05F;
}
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #07C05F;
}
.sub_title {
margin-left: 14px;
}
&:hover {
background: #30323605;
color: #00000099;
border-radius: 3px;
.menu-more {
color: #00000099;
}
.submenu_title {
max-width: 160px !important;
}
}
}
.submenu_item_active {
background: #07c05f1a !important;
color: #07c05f !important;
border-radius: 3px;
.menu-more {
color: #07c05f !important;
}
}
}
</style>
<style lang="less">
.upload-popup {
background-color: rgba(0, 0, 0, 0.9);
color: #FFFFFF;
border-color: rgba(0, 0, 0, 0.9) !important;
box-shadow: none;
margin-bottom: 10px !important;
.t-popup__arrow::before {
border-color: rgba(0, 0, 0, 0.9) !important;
background-color: rgba(0, 0, 0, 0.9) !important;
box-shadow: none !important;
}
}
.del-menu-popup {
z-index: 99 !important;
.t-popup__content {
width: 100px;
height: 40px;
line-height: 30px;
padding-left: 14px;
cursor: pointer;
margin-top: 4px !important;
}
}
</style>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { watch } from "vue"
const props = defineProps(['reviewImg', 'reviewUrl'])
const emit = defineEmits(['closePreImg'])
const close = () => {
emit('closePreImg')
}
</script>
<template>
<t-image-viewer :visible="reviewImg" closeOnOverlay closeOnEscKeydown @close="close"
:images="[{
mainImage: reviewUrl,
download: false
}]">
</t-image-viewer>
</template>
<style scoped lang="less"></style>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
</script>
<template>
<div class="mask">
<img class="upload-mask-img" src="@/assets/img/upload-mask.svg" alt="">
<span class="drag-txt">将文件拖放到此处</span>
<span class="drag-type-txt">pdfdoc 格式文件不超过30M</span>
<span class="drag-type-txt">textmarkdown格式文件不超过30M</span>
</div>
</template>
<style scoped lang="less">
.mask{
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.drag-txt {
color: #07c05f;
font-family: "PingFang SC";
font-size: 24px;
font-weight: 600;
line-height: 26px;
display: inline-block;
margin: 12px 0 16px 0;
}
.drag-type-txt {
width: 217px;
color: #00000066;
text-align: center;
font-family: "PingFang SC";
font-size: 12px;
font-weight: 400;
}
.upload-img{
width: 162px;
height: 162px;
}
</style>

View File

@ -0,0 +1,171 @@
import { ref, reactive, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { formatStringDate, kbFileTypeVerification } from "../utils/index";
import { MessagePlugin } from "tdesign-vue-next";
import {
uploadKnowledgeBase,
getKnowledgeBase,
getKnowledgeDetails,
delKnowledgeDetails,
getKnowledgeDetailsCon,
} from "@/api/knowledge-base/index";
import { knowledgeStore } from "@/stores/knowledge";
const usemenuStore = knowledgeStore();
export default function () {
const { cardList, total } = storeToRefs(usemenuStore);
let moreIndex = ref(-1);
const details = reactive({
title: "",
time: "",
md: [],
id: "",
total: 0
});
const getKnowled = (query = { page: 1, page_size: 35 }) => {
getKnowledgeBase(query)
.then((result: any) => {
let { data, total: totalResult } = result;
let cardList_ = data.map((item) => {
item["file_name"] = item.file_name.substring(
0,
item.file_name.lastIndexOf(".")
);
return {
...item,
updated_at: formatStringDate(new Date(item.updated_at)),
isMore: false,
file_type: item.file_type.toLocaleUpperCase(),
};
});
if (query.page == 1) {
cardList.value = cardList_;
} else {
cardList.value.push(...cardList_);
}
total.value = totalResult;
})
.catch((err) => {});
};
const delKnowledge = (index: number, item) => {
cardList.value[index].isMore = false;
moreIndex.value = -1;
delKnowledgeDetails(item.id)
.then((result: any) => {
if (result.success) {
MessagePlugin.info("知识删除成功!");
getKnowled();
} else {
MessagePlugin.error("知识删除失败!");
}
})
.catch((err) => {
MessagePlugin.error("知识删除失败!");
});
};
const openMore = (index: number) => {
moreIndex.value = index;
};
const onVisibleChange = (visible: boolean) => {
if (!visible) {
moreIndex.value = -1;
}
};
const requestMethod = (file: any, uploadInput) => {
if (file instanceof File && uploadInput) {
if (kbFileTypeVerification(file)) {
return;
}
uploadKnowledgeBase({ file })
.then((result: any) => {
if (result.success) {
MessagePlugin.info("上传成功!");
getKnowled();
} else {
// 改进错误信息提取逻辑
let errorMessage = "上传失败!";
// 优先从 error 对象中获取错误信息
if (result.error && result.error.message) {
errorMessage = result.error.message;
} else if (result.message) {
errorMessage = result.message;
}
// 检查错误码,如果是重复文件则显示特定提示
if (result.code === 'duplicate_file' || (result.error && result.error.code === 'duplicate_file')) {
errorMessage = "文件已存在";
}
MessagePlugin.error(errorMessage);
}
uploadInput.value.value = "";
})
.catch((err: any) => {
// 改进 catch 中的错误处理
let errorMessage = "上传失败!";
if (err.code === 'duplicate_file') {
errorMessage = "文件已存在";
} else if (err.error && err.error.message) {
errorMessage = err.error.message;
} else if (err.message) {
errorMessage = err.message;
}
MessagePlugin.error(errorMessage);
uploadInput.value.value = "";
});
} else {
MessagePlugin.error("file文件类型错误");
}
};
const getCardDetails = (item) => {
Object.assign(details, {
title: "",
time: "",
md: [],
id: "",
});
getKnowledgeDetails(item.id)
.then((result: any) => {
if (result.success && result.data) {
let { data } = result;
Object.assign(details, {
title: data.file_name,
time: formatStringDate(new Date(data.updated_at)),
id: data.id,
});
}
})
.catch((err) => {});
getfDetails(item.id, 1);
};
const getfDetails = (id, page) => {
getKnowledgeDetailsCon(id, page)
.then((result: any) => {
if (result.success && result.data) {
let { data, total: totalResult } = result;
if (page == 1) {
details.md = data;
} else {
details.md.push(...data);
}
details.total = totalResult;
}
})
.catch((err) => {});
};
return {
cardList,
moreIndex,
getKnowled,
details,
delKnowledge,
openMore,
onVisibleChange,
requestMethod,
getCardDetails,
total,
getfDetails,
};
}

16
frontend/src/main.ts Normal file
View File

@ -0,0 +1,16 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import "./assets/fonts.css";
import TDesign from "tdesign-vue-next";
// 引入组件库的少量全局样式变量
import "tdesign-vue-next/es/style/index.css";
import "@/assets/theme/theme.css";
const app = createApp(App);
app.use(TDesign);
app.use(createPinia());
app.use(router);
app.mount("#app");

View File

@ -0,0 +1,89 @@
import { createRouter, createWebHistory } from 'vue-router'
import { checkInitializationStatus } from '@/api/initialization'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
redirect: "/platform",
},
{
path: "/initialization",
name: "initialization",
component: () => import("../views/initialization/InitializationConfig.vue"),
meta: { requiresInit: false } // 初始化页面不需要检查初始化状态
},
{
path: "/knowledgeBase",
name: "home",
component: () => import("../views/knowledge/KnowledgeBase.vue"),
meta: { requiresInit: true }
},
{
path: "/platform",
name: "Platform",
redirect: "/platform/knowledgeBase",
component: () => import("../views/platform/index.vue"),
meta: { requiresInit: true },
children: [
{
path: "knowledgeBase",
name: "knowledgeBase",
component: () => import("../views/knowledge/KnowledgeBase.vue"),
meta: { requiresInit: true }
},
{
path: "creatChat",
name: "creatChat",
component: () => import("../views/creatChat/creatChat.vue"),
meta: { requiresInit: true }
},
{
path: "chat/:chatid",
name: "chat",
component: () => import("../views/chat/index.vue"),
meta: { requiresInit: true }
},
{
path: "settings",
name: "settings",
component: () => import("../views/settings/Settings.vue"),
meta: { requiresInit: true }
},
],
},
],
});
// 路由守卫:检查系统初始化状态
router.beforeEach(async (to, from, next) => {
// 如果访问的是初始化页面,直接放行
if (to.meta.requiresInit === false) {
next();
return;
}
1
try {
// 检查系统是否已初始化
const { initialized } = await checkInitializationStatus();
if (initialized) {
// 系统已初始化,记录到本地存储并正常跳转
localStorage.setItem('system_initialized', 'true');
next();
} else {
// 系统未初始化,跳转到初始化页面
console.log('系统未初始化,跳转到初始化页面');
next('/initialization');
}
} catch (error) {
console.error('检查初始化状态失败:', error);
// 如果检查失败,默认认为需要初始化
next('/initialization');
}
});
export default router

View File

@ -0,0 +1,11 @@
import { ref, computed, reactive } from "vue";
import { defineStore } from "pinia";
export const knowledgeStore = defineStore("knowledge", {
state: () => ({
cardList: ref([]),
total: ref(0),
}),
actions: {},
});

Some files were not shown because too many files have changed in this diff Show More