Merge branch 'main' into dev

This commit is contained in:
Wenjie Zhang 2025-02-26 12:38:28 +08:00
commit c4ea78f0e5
40 changed files with 1181 additions and 698 deletions

5
.gitignore vendored
View File

@ -33,9 +33,10 @@ src/data
*/package-lock.json
web/package-lock.json
saves
saves_dev
notebooks
graphrag
docker/volumes
.cursorrules
src/static/config.yaml
src/static/config.dev.yaml

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Wenjie Zhang
Copyright (c) 2025 Wenjie Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

119
README.md
View File

@ -1,39 +1,57 @@
<h1 align="center">语析 (基于大模型的知识图谱问答平台)</h1>
<h1 align="center">语析 (基于大模型的知识库+知识图谱问答平台)</h1>
<div align="center">
![](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=ffffff)
![Vue.js](https://img.shields.io/badge/vuejs-%2335495e.svg?style=flat&logo=vuedotjs&logoColor=%234FC08D)
![Vue.js](https://img.shields.io/badge/vuejs-%2335495e.svg?style=flat&logo=vuedotjs&logoColor=%234FC08D) 
![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat&logo=fastapi)
![](https://img.shields.io/github/issues/xerrors/Yuxi-Know?color=F48D73)
![](https://img.shields.io/github/license/bitcookies/winrar-keygen.svg?logo=github)
![](https://img.shields.io/github/stars/xerrors/Yuxi-Know)
</div>
> [!NOTE]
> 当前项目还处于开发的早期,还存在一些 BUG有问题随时提 issue。
## 概述
基于大模型 RAG 知识库与知识图谱的问答平台。Llamaindex + VueJS + Flask + Neo4j。大模型适配 OpenAI、国内主流大模型平台的模型调用、本地 vllm 部署。只需要配置对应服务平台的 `API_KEY` 即可使用。
![main](./images/main.png)
![image](https://github.com/user-attachments/assets/75010511-4ac5-4924-8268-fea9a589839c)
> [!NOTE]
> 当前项目还处于开发的早期,还存在一些 BUG有问题随时提 issue。
代办清单
- [ ] Ollma Embedding 支持Open-like Embedding 支持)
- [x] 知识图谱索引支持自定义 Embedding 模型
- [x] DeepSeek-R1 支持
## 更新日志
- 2025.02.24 新增网页检索以及内容展示,需要配置 `TAVILY_API_KEY`, 感谢 [littlewwwhite](https://github.com/littlewwwhite)。
- 2025.02.23 SiliconFlow 的 Rerank 和 Embedding model 支持。现在默认使用 SiliconFlow。
- 2025.02.20 DeepSeek-R1 支持,需配置 `DEEPSEEK_API_KEY` 或者 `SILICONFLOW_API_KEY` 使用
- 2024.10.12 后端修改为 [FastAPI](https://github.com/fastapi),并添加了 [Milvus-Standalone](https://github.com/milvus-io) 的独立部署。
![image](https://github.com/user-attachments/assets/8416a933-cc43-45d0-bf06-00df0ba6c4fb)
## 快速上手
在启动之前,提供 API 服务商的 API_KEY并放置在 `src/.env` 文件中。默认使用的是智谱AI。因此务必需要配置 `ZHIPUAI_API_KEY=<ZHIPUAI_API_KEY>`。其余模型的配置可以参考 [src/config/models.yaml](src/config/models.yaml) 中的 env。
在启动之前,提供 API 服务商的 API_KEY并放置在 `src/.env` 文件中。默认使用的是硅基流动。因此务必需要配置 `SILICONFLOW_API_KEY=<SILICONFLOW_API_KEY>`。其余模型的配置可以参考 [src/static/models.yaml](src/static/models.yaml) 中的 env。
```
ZHIPUAI_API_KEY=270ea********8bfa97.e3XOMd****Q1Sk
OPENAI_API_KEY=sk-*********[可选]
SILICONFLOW_API_KEY=sk-270ea********8bfa97.e3XOMd****Q1Sk
```
本项目的基础对话服务可以在不含显卡的设备上运行,大模型使用在线服务商的接口。但是如果想要完整的知识库对话体验,则需要 8G 以上的显存。因为需要本地运行 embedding 模型和 rerank 模型。
本项目的基础对话服务可以在不含显卡的设备上运行,大模型使用在线服务商的接口。如果需要指定本地模型(如 Embedding、Rerank 模型)所在路径,需要配置 `MODEL_DIR` 参数。并在 Docker Compose 中启用映射
**提醒**:下面的脚本会启动开发版本,源代码的修改会自动更新(含前端和后端)。如果生产环境部署,请使用 `docker/docker-compose.yml` 启动。
```bash
docker-compose -f docker/docker-compose.dev.yml up --build
docker compose -f docker/docker-compose.dev.yml --env-file src/.env up --build
```
**也可以加上 `-d` 参数,后台运行。*
@ -60,7 +78,7 @@ docker-compose -f docker/docker-compose.dev.yml up --build
关闭 docker 服务:
```bash
docker-compose -f docker/docker-compose.dev.yml down
docker compose -f docker/docker-compose.dev.yml --env-file src/.env down
```
查看日志:
@ -69,73 +87,64 @@ docker-compose -f docker/docker-compose.dev.yml down
docker logs <CONTAINER_NAME> # 例如docker logs api-dev
```
如果需要使用到本地模型(不推荐手动指定),比如向量模型或者重排序模型,则需要将环境变量中设置的 `MODEL_ROOT_DIR` 做映射,比如本地模型都是存放在 `/hdd/models` 里面,则需要在 `docker-compose.yml``docker-compose.dev.yml` 中添加:
```yml
services:
api:
build:
context: ..
dockerfile: docker/api.Dockerfile
container_name: api-dev
working_dir: /app
volumes:
- ../src:/app/src
- ../saves:/app/saves
- /hdd/zwj/models:/hdd/zwj/models # <== 修改这一行
```
**生产环境部署**:本项目同时支持使用 Docker 部署生产环境,只需要更换 `docker-compose` 文件就可以了。
```bash
docker-compose -f docker/docker-compose.yml up --build
docker compose -f docker/docker-compose.yml --env-file src/.env up --build
```
注:如果想要在本地使用 PyCharm 等 IDE 加断点调试,可以使用 Docker 运行除 API 以外的容器,然后本地运行 main.py但是需要注意将对应的环境变量添加到 .env 文件中
## 模型支持
### 1. 对话模型支持
模型仅支持通过API调用的模型如果是需要运行本地模型则建议使用 vllm 转成 API 服务之后使用。使用前请在 `.env` 配置 APIKEY 后使用,配置项目参考:[src/config/models.yaml](src/config/models.yaml)
模型仅支持通过API调用的模型如果是需要运行本地模型则建议使用 vllm 转成 API 服务之后使用。使用前请在 `.env` 配置 APIKEY 后使用,配置项目参考:[src/static/models.yaml](src/static/models.yaml)
| 模型供应商 | 默认模型 | 配置项目 |
| :-------------------- | :---------------------------------------- | :--------------------------------------------- |
| `siliconflow` (默认) | `Qwen/Qwen2.5-7B-Instruct` (免费) | `SILICONFLOW_API_KEY` |
| `openai` | `gpt-4o` | `OPENAI_API_KEY` |
| `qianfan`(百度) | `ernie_speed` | `QIANFAN_ACCESS_KEY`, `QIANFAN_SECRET_KEY` |
| `zhipu`(默认) | `glm-4-flash` (免费) | `ZHIPUAI_API_KEY` |
| `dashscope`(阿里) | `qwen-max-latest` | `DASHSCOPE_API_KEY` |
| `deepseek` | `deepseek-chat` | `DEEPSEEK_API_KEY` |
| `siliconflow` | `meta-llama/Meta-Llama-3.1-8B-Instruct` | `SILICONFLOW_API_KEY` |
| `arc`(豆包方舟) | `doubao-1-5-pro-32k-250115` | `ARK_API_KEY` |
| `zhipu`(智谱清言) | `glm-4-flash` | `ZHIPUAI_API_KEY` |
| `dashscope`(阿里) | `qwen-max-latest` | `DASHSCOPE_API_KEY` |
| `qianfan`(百度) | `ernie_speed` | `QIANFAN_ACCESS_KEY`, `QIANFAN_SECRET_KEY` |
同样支持以 OpenAI 的兼容模型运行模型,可以直接在 Web 设置里面添加。比如使用 vllm 和 Ollama 运行本地模型时。
此外,如果想要添加供应商的模型,确认知识 OpenAI 调用的方法之后,只需要在 [src/static/models.yaml](src/static/models.yaml) 中添加对应的模型配置即可。配置示例如下:
### 2. 向量模型支持
```yaml
ark:
name: 豆包Ark
url: https://console.volcengine.com/ark/region:ark+cn-beijing/model # 模型列表
default: doubao-1-5-pro-32k-250115 # 默认模型
base_url: https://ark.cn-beijing.volces.com/api/v3
env: # 需要配置的环境变量仅限API key
- ARK_API_KEY
models:
- doubao-1-5-pro-32k-250115
- doubao-1-5-lite-32k-250115
- deepseek-r1-250120
```
建议直接使用智谱 AI 的 embedding-3这样不需要做任何修改且资费不贵。
同样支持添加以 OpenAI 的兼容模式运行的单个模型,可以直接在 Web 设置里面添加。比如使用 vllm 和 Ollama 运行本地模型时。
![](./images/custom_models.png)
### 2. 向量模型支持和重排序模型支持
建议直接使用硅基流动部署的 bge-m3这样不需要做任何修改且免费。其余模型参考[src/static/models.yaml](src/static/models.yaml)
> [!Warning]
> 需要注意,由于知识库和图数据库的构建都依赖于向量模型,如果中途更改向量模型,会导致知识库不可用。此外,知识图谱的向量索引的建立默认使用 embedding-3 构建,因此检索的时候必须使用 embedding-3现阶段还不支持修改
| 模型名称(`config.embed_model`) | 默认路径/模型 | 需要配置项目(`config.model_local_paths` |
| :------------------------------- | :------------------------------- | :------------------------------------------- |
| `bge-large-zh-v1.5` | `BAAI/bge-large-zh-v1.5` | `bge-large-zh-v1.5` |
| `zhipu` | `embedding-2`, `embedding-3` | `ZHIPUAI_API_KEY` (`.env`) |
### 3. 重排序模型支持
目前仅支持 `BAAI/bge-reranker-v2-m3`
### 4. 本地模型支持
对于**语言模型**,并不支持直接运行本地语言模型,请使用 vllm 或者 ollama 转成 API 服务之后使用。
对于**向量模型**和**重排序模型**,可以不做修改会自动下载模型,如果下载过程中出现问题,请参考 [HF-Mirror](https://hf-mirror.com/) 配置相关内容。如果想要使用本地已经下载好的模型(不建议),可以在 `saves/config/config.yaml` 配置相关内容。同时注意要在 docker 中做映射,参考 README 中的 `docker/docker-compose.yml`
对于**向量模型**和**重排序模型**,选择以 `local` 前缀开头的模型,可以不做修改会自动下载模型,如果下载过程中出现问题,请参考 [HF-Mirror](https://hf-mirror.com/) 配置相关内容。(但请注意,如果是 Docker 运行,模型仅会缓存到 Docker 里面)
例如:
如果想要使用本地已经下载好的模型,可以在网页的 settings 里面做映射。或者修改 `src/static/config.yaml` 来配置映射关系。但请记得,本地模型的路径要在 docker-compose 的文件中映射 volumes。
```yaml
model_local_paths:
bge-large-zh-v1.5: /models/bge-large-zh-v1.5
```
## 知识库支持
@ -152,10 +161,6 @@ model_local_paths:
目前项目中暂不支持同时查询多个知识图谱,短期内也没有计划支持。不过倒是可以通过配置不同的 `NEO4J_URI` 服务来切换知识图谱。如果已经有了基于 neo4j 的知识图谱,可以将 `docker-compose.yml` 中的 `graph` 配置项删除,并将 `api.environment` 中的 `NEO4J_URI` 配置项修改为 neo4j 的服务地址。
## 更新日志
- 2024.10.12 后端修改为 [FastAPI](https://github.com/fastapi),并添加了 [Milvus-Standalone](https://github.com/milvus-io) 的独立部署。
## 相关问题
### 镜像下载问题

View File

@ -1,5 +1,5 @@
# 使用基础镜像
FROM pytorch/pytorch:2.4.1-cuda11.8-cudnn9-runtime
FROM python:3.12
# 设置工作目录
WORKDIR /app
@ -9,8 +9,10 @@ COPY ../requirements.txt /app/requirements.txt
# 安装依赖Docker 会缓存这一步,除非 requirements.txt 发生变化)
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip install -U gunicorn -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip install gunicorn
RUN apt-get clean
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
# 复制代码到容器中
COPY ../src /app/src

View File

@ -7,10 +7,11 @@ services:
working_dir: /app
volumes:
- ../src:/app/src
- ../saves:/app/saves
- /hdd/zwj/models:/hdd/zwj/models
- ../saves_dev:/app/saves
- ../src/static/config.dev.yaml:/app/src/static/config.yaml
# - ${MODEL_DIR}:${MODEL_DIR} # 如果出现 undefined volume MODEL_DIR: invalid compose project请添加此环境变量或者注释此行
ports:
- "5000:5000"
- "5050:5050"
depends_on:
- graph
- milvus
@ -21,7 +22,8 @@ services:
- NEO4J_USERNAME=neo4j
- NEO4J_PASSWORD=0123456789
- MILVUS_URI=http://milvus:19530
command: uvicorn src.main:app --host 0.0.0.0 --port 5000 --reload
- MODEL_DIR=${MODEL_DIR} # 优先级高于 .env 中的 MODEL_DIR
command: uvicorn src.main:app --host 0.0.0.0 --port 5050 --reload
web:
build:
@ -40,7 +42,7 @@ services:
- app-network
environment:
- NODE_ENV=development
- VITE_API_URL=http://api:5000 # 添加这行
- VITE_API_URL=http://api:5050 # 添加这行
command: npm run server
graph:

View File

@ -1,12 +1,13 @@
# 开发阶段
FROM node:22.10.0 AS development
FROM node:latest AS development
WORKDIR /app
# 复制 package.json 和 package-lock.json如果存在
COPY ./web/package*.json ./
# 安装依赖
RUN npm install --registry https://registry.npmmirror.com --verbose --force
RUN npm install --verbose --force
# RUN npm install --registry http://mirrors.cloud.tencent.com/npm/ --verbose --force
# 复制源代码
COPY ./web .
@ -17,11 +18,12 @@ EXPOSE 5173
# 启动开发服务器的命令在 docker-compose 文件中定义
# 生产阶段
FROM node:22.10.0 AS build-stage
FROM node:latest AS build-stage
WORKDIR /app
COPY ./web/package*.json ./
RUN npm install --registry https://registry.npmmirror.com --force
RUN npm install --force
# RUN npm install --registry https://registry.npmmirror.com --force
COPY ./web .
RUN npm run build

BIN
images/custom_models.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
images/reasoning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 KiB

View File

@ -1,7 +1,7 @@
dashscope>=1.20.3
PyMuPDF==1.23.26
peft>=0.11.1
FlagEmbedding>=1.2.11
FlagEmbedding>=1.3.2
Flask>=3.0.3
Flask_Cors>=4.0.1
llama_index>=0.11.8
@ -18,7 +18,7 @@ torch>=2.4.0
tqdm>=4.66.1
zhipuai>=2.1.2
neo4j>=5.22.0
sentencepiece==0.1.99
sentencepiece>=0.1.99
llama-index-readers-file
opencv-python-headless
docx2txt

View File

@ -1,25 +0,0 @@
#!/bin/bash
# 检查是否提供了 API_KEY 参数
if [ -z "$1" ]; then
echo "请提供 API_KEY。"
exit 1
fi
# 获取当前目录路径
CURRENT_DIR=$(pwd)
# 如果 src 目录不存在则创建
if [! -d "${CURRENT_DIR}/src" ]; then
mkdir -p "${CURRENT_DIR}/src"
fi
# 如果.env 文件不存在,则从.env.template 复制一份创建
if [! -f "${CURRENT_DIR}/src/.env" ]; then
cp "${CURRENT_DIR}/src/.env.template" "${CURRENT_DIR}/src/.env"
fi
# 将 API_KEY 写入.env 文件
echo "ZHIPUAI_API_KEY=$1" > "${CURRENT_DIR}/src/.env"
echo "API_KEY 已成功写入 src/.env 文件。"

10
scripts/run_docker.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# 检查是否提供了命令行参数
if [ $# -eq 0 ]; then
echo "请提供命令参数例如up -d 或 down"
exit 1
fi
# 将所有命令行参数传递给 docker compose 命令
docker compose -f docker/docker-compose.dev.yml --env-file src/.env "$@"

View File

@ -1 +1,3 @@
ZHIPUAI_API_KEY=<ZHIPUAI_API_KEY>
ZHIPUAI_API_KEY=270ea71e9******97.e3XOMdWKuZb7Q1Sk
SILICONFLOW_API_KEY=sk-aeoamptic****vdmozbpdsofstmoqnvsmbvql
MODEL_DIR=/home/user/Documents/projects/Yuxi-Know/models

View File

@ -6,7 +6,7 @@ from src.utils.logging_config import setup_logger
logger = setup_logger("Config")
with open(Path("src/config/models.yaml"), "r") as f:
with open(Path("src/static/models.yaml"), "r") as f:
_models = yaml.safe_load(f)
MODEL_NAMES = _models["MODEL_NAMES"]
@ -40,34 +40,35 @@ class SimpleConfig(dict):
class Config(SimpleConfig):
def __init__(self, filename=None):
def __init__(self):
super().__init__()
self._config_items = {}
self.save_dir = "saves"
self.filename = str(Path("src/static/config.yaml"))
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
### >>> 默认配置
# 可以在 config/base.yaml 中覆盖
self.add_item("stream", default=True, des="是否开启流式输出")
self.add_item("save_dir", default="saves", des="保存目录")
# 功能选项
self.add_item("enable_reranker", default=False, des="是否开启重排序")
self.add_item("enable_knowledge_base", default=False, des="是否开启知识库")
self.add_item("enable_knowledge_graph", default=False, des="是否开启知识图谱")
self.add_item("enable_search_engine", default=False, des="是否开启搜索引擎")
self.add_item("enable_web_search", default=False, des="是否开启网页搜索")
self.add_item("enable_web_search", default=False, des="是否开启网页搜索(需配置 TAVILY_API_KEY")
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
self.add_item("model_provider", default="zhipu", des="模型提供商", choices=list(MODEL_NAMES.keys()))
self.add_item("model_name", default=None, des="模型名称")
self.add_item("embed_model", default="zhipu-embedding-3", des="Embedding 模型", choices=list(EMBED_MODEL_INFO.keys()))
self.add_item("reranker", default="bge-reranker-v2-m3", des="Re-Ranker 模型", choices=list(RERANKER_LIST.keys()))
self.add_item("use_rewrite_query", default="off", des="重写查询", choices=["off", "on", "hyde"])
## 如果需要自定义本地模型路径,则在 src/.env 中配置 MODEL_DIR
self.add_item("model_provider", default="siliconflow", des="模型提供商", choices=list(MODEL_NAMES.keys()))
self.add_item("model_provider_lite", default="siliconflow", des="模型提供商(用于轻量任务)", choices=list(MODEL_NAMES.keys()))
self.add_item("model_name", default="Qwen/Qwen2.5-7B-Instruct", des="模型名称")
self.add_item("model_name_lite", default="Qwen/Qwen2.5-7B-Instruct", des="模型名称(用于轻量任务)")
self.add_item("embed_model", default="siliconflow/BAAI/bge-m3", des="Embedding 模型", choices=list(EMBED_MODEL_INFO.keys()))
self.add_item("reranker", default="siliconflow/BAAI/bge-reranker-v2-m3", des="Re-Ranker 模型", choices=list(RERANKER_LIST.keys()))
self.add_item("model_local_paths", default={}, des="本地模型路径")
self.add_item("use_rewrite_query", default="off", des="重写查询", choices=["off", "on", "hyde"])
### <<< 默认配置结束
self.filename = filename or os.path.join(self.save_dir, "config", "config.yaml")
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
self.load()
self.handle_self()
@ -90,7 +91,9 @@ class Config(SimpleConfig):
def handle_self(self):
self.model_names = MODEL_NAMES
model_provider_info = self.model_names.get(self.model_provider, {})
self.model_dir = os.environ.get("MODEL_DIR", "")
# 检查模型提供商是否存在
if self.model_provider != "custom":
if self.model_name not in model_provider_info["models"]:
logger.warning(f"Model name {self.model_name} not in {self.model_provider}, using default model name")
@ -104,6 +107,7 @@ class Config(SimpleConfig):
logger.warning(f"Model name {self.model_name} not in custom models, using default model name")
self.model_name = self.custom_models[0]["custom_id"]
# 检查模型提供商的环境变量
conds = {}
self.model_provider_status = {}
for provider in self.model_names:
@ -111,11 +115,14 @@ class Config(SimpleConfig):
conds_bool = [bool(os.getenv(_k)) for _k in conds[provider]]
self.model_provider_status[provider] = all(conds_bool)
# 检查web_search的环境变量
if self.enable_web_search and not os.getenv("TAVILY_API_KEY"):
logger.warning("TAVILY_API_KEY not set, web search will be disabled")
self.enable_web_search = False
self.valuable_model_provider = [k for k, v in self.model_provider_status.items() if v]
assert len(self.valuable_model_provider) > 0, f"No model provider available, please check your `.env` file. API_KEY_LIST: {conds}"
def load(self):
"""根据传入的文件覆盖掉默认配置"""
logger.info(f"Loading config from {self.filename}")

View File

@ -1,17 +0,0 @@
# 基础配置
stream: true
save_dir: saves
# 功能开关
enable_reranker: false
enable_knowledge_base: false
enable_knowledge_graph: false
enable_search_engine: false
enable_web_search: false
# 模型配置
model_provider: "deepseek" # 设置为 deepseek
model_name: "deepseek-chat" # 设置默认模型名称
embed_model: "zhipu-embedding-3"
reranker: "bge-reranker-v2-m3"
model_local_paths: {}

View File

@ -68,7 +68,9 @@ class DataBaseManager:
assert self.config.enable_knowledge_base, "知识库未启用"
knowledge_base_collections = self.knowledge_base.get_collection_names()
if len(self.data["databases"]) != len(knowledge_base_collections):
logger.warning(f"Database number not match, {knowledge_base_collections}")
logger.warning(
f"Database number not match, {knowledge_base_collections}, "
f"self.data['databases']: {self.data['databases']}, ")
for db in self.data["databases"]:
db.update(self.knowledge_base.get_collection_info(db.metaname))

View File

@ -27,6 +27,7 @@ class GraphDatabase:
self.kgdb_name = kgdb_name
assert embed_model, "embed_model=None"
self.embed_model = embed_model
self.embed_model_name = None
def start(self):
uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
@ -87,7 +88,8 @@ class GraphDatabase:
"relationship_count": relationship_count,
"triples_count": triples_count,
"labels": labels,
"status": self.status
"status": self.status,
"embed_model_name": self.embed_model_name
}
with self.driver.session() as session:
@ -171,6 +173,7 @@ class GraphDatabase:
def jsonl_file_add_entity(self, file_path, kgdb_name='neo4j'):
self.status = "processing"
kgdb_name = kgdb_name or 'neo4j'
self.embed_model_name = self.embed_model_name or self.config.embed_model
self.use_database(kgdb_name) # 切换到指定数据库
def read_triples(file_path):

View File

@ -1,6 +1,5 @@
import os
from src.models.embedding import EmbeddingModel
from pymilvus import MilvusClient, MilvusException
from src.utils import setup_logger, hashstr
logger = setup_logger("KnowledgeBase")
@ -83,7 +82,7 @@ class KnowledgeBase:
def search(self, query, collection_name, limit=3):
query_vectors = self.embed_model.encode_queries([query])
query_vectors = self.embed_model.batch_encode([query])
return self.search_by_vector(query_vectors[0], collection_name, limit)
def search_by_vector(self, vector, collection_name, limit=3):

View File

@ -1,4 +1,4 @@
from src.models.embedding import Reranker
from src.models.rerank_model import get_reranker
from src.utils.logging_config import setup_logger
logger = setup_logger("server-common")
@ -12,7 +12,11 @@ class Retriever:
self.model = model
if self.config.enable_reranker:
self.reranker = Reranker(config)
self.reranker = get_reranker(config)
if self.config.enable_web_search:
from src.utils.web_search import WebSearcher
self.web_searcher = WebSearcher()
def retrieval(self, query, history, meta):
@ -21,10 +25,12 @@ class Retriever:
refs["entities"] = self.reco_entities(query, history, refs)
refs["knowledge_base"] = self.query_knowledgebase(query, history, refs)
refs["graph_base"] = self.query_graph(query, history, refs)
refs["web_search"] = self.query_web(query, history, refs)
return refs
def construct_query(self, query, refs, meta):
logger.debug(f"{refs=}")
if not refs or len(refs) == 0:
return query
@ -44,6 +50,12 @@ class Retriever:
)
external_parts.extend(["图数据库信息:", db_text])
# 解析网络搜索的结果
web_res = refs.get("web_search", {}).get("results", [])
if web_res:
web_text = "\n".join(f"{r['title']}: {r['content']}" for r in web_res)
external_parts.extend(["网络搜索信息:", web_text])
# 构造查询
from src.utils.prompts import knowbase_qa_template
if external_parts and len(external_parts) > 0:
@ -60,8 +72,6 @@ class Retriever:
raise NotImplementedError
def query_graph(self, query, history, refs):
# res = model.predict("qiansdgsa, dasdh ashdsakjdk ak ").content
results = []
if refs["meta"].get("use_graph") and self.config.enable_knowledge_base:
for entity in refs["entities"]:
@ -70,6 +80,7 @@ class Retriever:
results.extend(result)
return {"results": self.format_query_results(results)}
def query_knowledgebase(self, query, history, refs):
"""查询知识库"""
@ -100,15 +111,13 @@ class Retriever:
for r in all_kb_res:
r["file"] = kb.id2file(r["entity"]["file_id"])
# use distance threshold to filter results
if meta.get("mode") == "search":
kb_res = all_kb_res
else:
kb_res = [r for r in all_kb_res if r["distance"] > distance_threshold]
kb_res = [r for r in all_kb_res if r["distance"] > distance_threshold]
if self.config.enable_reranker:
for r in kb_res:
r["rerank_score"] = self.reranker.compute_score([rw_query, r["entity"]["text"]], normalize=True)[0]
if self.config.enable_reranker and len(kb_res) > 0:
texts = [r["entity"]["text"] for r in kb_res]
rerank_scores = self.reranker.compute_score([rw_query, texts], normalize=True)
for i, r in enumerate(kb_res):
r["rerank_score"] = rerank_scores[i]
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
kb_res = [_res for _res in kb_res if _res["rerank_score"] > rerank_threshold]
@ -116,12 +125,26 @@ class Retriever:
return {"results": kb_res, "all_results": all_kb_res, "rw_query": rw_query}
def query_web(self, query, history, refs):
"""查询网络"""
if not (refs["meta"].get("use_web") and self.config.enable_web_search):
return {"results": [], "message": "Web search is disabled"}
try:
search_results = self.web_searcher.search(query, max_results=5)
except Exception as e:
logger.error(f"Web search error: {str(e)}")
return {"results": [], "message": "Web search error"}
return {"results": search_results}
def rewrite_query(self, query, history, refs):
"""重写查询"""
if refs["meta"].get("mode") == "search": # 如果是搜索模式,就使用 meta 的配置,否则就使用全局的配置
rewrite_query_span = refs["meta"].get("use_rewrite_query", "off")
else:
rewrite_query_span = refs["meta"]["config"].get("use_rewrite_query", "off")
rewrite_query_span = self.config.use_rewrite_query
if rewrite_query_span == "off":
rewritten_query = query

View File

@ -10,33 +10,18 @@ logger = setup_logger("Startup")
class Startup:
def __init__(self):
self.config = Config("config/base.yaml")
self._check_environment()
self.start()
def _check_environment(self):
"""检查必要的环境变量"""
required_vars = {
"zhipu": ["ZHIPUAI_API_KEY"],
"openai": ["OPENAI_API_KEY"],
"deepseek": ["DEEPSEEK_API_KEY"],
}
provider = self.config.model_provider
if provider in required_vars:
missing = [var for var in required_vars[provider] if not os.getenv(var)]
if missing:
logger.error(f"Missing required environment variables for {provider}: {missing}")
raise ValueError(f"Missing required environment variables: {missing}")
if self.config.enable_web_search and not os.getenv("TAVILY_API_KEY"):
logger.warning("TAVILY_API_KEY not set, web search will be disabled")
def start(self):
self.config = Config()
self.model = select_model(self.config)
self.dbm = DataBaseManager(self.config)
self.retriever = Retriever(self.config, self.dbm, self.model)
self.model_lite = select_model(self.config,
model_provider=self.config.model_provider_lite,
model_name=self.config.model_name_lite)
def restart(self):
logger.info("Restarting...")
self.start()

View File

@ -1,23 +1,51 @@
import os
from src.utils.logging_config import logger
from src.models.chat_model import OpenModel, DeepSeek, Zhipu, Qianfan, DashScope, SiliconFlow
from src.models.embedding import get_embedding_model
from src.models.chat_model import OpenAIBase
def select_model(config):
"""
根据配置选择模型
"""
if config.model_provider == "deepseek":
return DeepSeek(config.model_name)
elif config.model_provider == "zhipu":
return Zhipu(config.model_name)
elif config.model_provider == "openai":
return OpenModel(config.model_name)
elif config.model_provider == "qianfan":
return Qianfan(config.model_name)
elif config.model_provider == "dashscope":
return DashScope(config.model_name)
elif config.model_provider == "siliconflow":
return SiliconFlow(config.model_name)
def select_model(config, model_provider=None, model_name=None):
model_provider = model_provider or config.model_provider
model_info = config.model_names[model_provider]
model_name = model_name or config.model_name or model_info["default"]
logger.info(f"Selecting model from `{model_provider}` with `{model_name}`")
if model_provider in [
"deepseek",
"ark",
"siliconflow",
"zhipu",
"lingyiwanwu",
]:
return OpenAIBase(
api_key=os.getenv(model_info["env"][0]),
base_url=model_info["base_url"],
model_name=model_name,
)
elif model_provider == "qianfan":
from src.models.chat_model import Qianfan
return Qianfan(model_name)
elif model_provider == "dashscope":
from src.models.chat_model import DashScope
return DashScope(model_name)
elif model_provider == "openai":
from src.models.chat_model import OpenModel
return OpenModel(model_name)
elif model_provider == "custom":
model_info = next((x for x in config.custom_models if x["custom_id"] == model_name), None)
if model_info is None:
raise ValueError(f"Model {model_name} not found in custom models")
from src.models.chat_model import CustomModel
return CustomModel(model_info)
elif model_provider is None:
raise ValueError("Model provider not specified, please modify `model_provider` in `src/config/base.yaml`")
else:
raise ValueError(f"Unsupported model provider: {config.model_provider}")
raise ValueError(f"Model provider {model_provider} not supported")

View File

@ -1,7 +1,6 @@
import os
from openai import OpenAI
from src.utils.logging_config import setup_logger
from zhipuai import ZhipuAI
logger = setup_logger(__name__)
@ -48,28 +47,6 @@ class OpenModel(OpenAIBase):
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)
class DeepSeek(OpenAIBase):
def __init__(self, model_name=None):
model_name = model_name or "deepseek-chat"
api_key = os.getenv("DEEPSEEK_API_KEY", "your-default-api-key")
base_url = os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1")
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)
class Zhipu(OpenAIBase):
def __init__(self, model_name=None):
model_name = model_name or "glm-4-flash"
api_key = os.getenv("ZHIPUAI_API_KEY", "270ea71e9560c0ff406acbcdd48bfd97.e3XOMdWKuZb7Q1Sk")
base_url = "https://open.bigmodel.cn/api/paas/v4/"
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)
class SiliconFlow(OpenAIBase):
def __init__(self, model_name=None):
model_name = model_name or "meta-llama/Meta-Llama-3.1-8B-Instruct"
api_key = os.getenv("SILICONFLOW_API_KEY")
base_url = "https://api.siliconflow.cn/v1"
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)
class CustomModel(OpenAIBase):
def __init__(self, model_info):
model_name = model_info["name"]
@ -166,14 +143,6 @@ class DashScope:
return response.output.choices[0].message
class ChatModel:
def __init__(self, config):
if config.model_provider == "zhipu":
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))
elif config.model_provider == "openai":
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
if __name__ == "__main__":
model = SiliconFlow()
for a in model.predict("你好", stream=True):

View File

@ -1,105 +1,125 @@
import os
import uuid
from FlagEmbedding import FlagModel, FlagReranker
import json
import requests
from FlagEmbedding import FlagModel
from src.config import EMBED_MODEL_INFO, RERANKER_LIST
from src.config import EMBED_MODEL_INFO
from src.utils.logging_config import setup_logger
from src.utils import hashstr
logger = setup_logger("EmbeddingModel")
GLOBAL_EMBED_STATE = {}
class EmbeddingModel(FlagModel):
def __init__(self, model_info, config, **kwargs):
self.info = model_info
model_name_or_path = config.model_local_paths.get(model_info["name"], model_info.get("default_path", None))
logger.info(f"Loading embedding model {model_info['name']} from {model_name_or_path}")
class LocalEmbeddingModel(FlagModel):
def __init__(self, config, **kwargs):
info = EMBED_MODEL_INFO[config.embed_model]
model_name_or_path = config.model_local_paths.get(info["name"], info.get("default_path"))
logger.info(f"Loading embedding model {info['name']} from {model_name_or_path}")
super().__init__(model_name_or_path,
query_instruction_for_retrieval=model_info.get("query_instruction", None),
query_instruction_for_retrieval=info.get("query_instruction", None),
use_fp16=False, **kwargs)
logger.info(f"Embedding model {model_info['name']} loaded")
logger.info(f"Embedding model {info['name']} loaded")
class Reranker(FlagReranker):
def __init__(self, config, **kwargs):
assert config.reranker in RERANKER_LIST.keys(), f"Unsupported Reranker: {config.reranker}, only support {RERANKER_LIST.keys()}"
model_name_or_path = config.model_local_paths.get(config.reranker, default_path=RERANKER_LIST[config.reranker])
logger.info(f"Loading Reranker model {config.reranker} from {model_name_or_path}")
super().__init__(model_name_or_path, use_fp16=True, **kwargs)
logger.info(f"Reranker model {config.reranker} loaded")
from zhipuai import ZhipuAI
class ZhipuEmbedding:
def __init__(self, model_info, config) -> None:
self.config = config
self.model_info = model_info
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))
logger.info("Zhipu Embedding model loaded")
self.query_instruction_for_retrieval = "为这个句子生成表示以用于检索相关文章:"
class RemoteEmbeddingModel:
embed_state = {}
def predict(self, message):
def batch_encode(self, messages, batch_size=20):
data = []
batch_size = 20
if len(message) > batch_size:
global GLOBAL_EMBED_STATE
task_id = hashstr(message)
logger.info(f"Creating new state for process {task_id}")
GLOBAL_EMBED_STATE[task_id] = {
if len(messages) > batch_size:
task_id = hashstr(messages)
self.embed_state[task_id] = {
'status': 'in-progress',
'total': len(message),
'total': len(messages),
'progress': 0
}
for i in range(0, len(message), batch_size):
if len(message) > batch_size:
logger.info(f"Encoding {i} to {i+batch_size} with {len(message)} messages")
GLOBAL_EMBED_STATE[task_id]['progress'] = i
for i in range(0, len(messages), batch_size):
group_msg = messages[i:i+batch_size]
logger.info(f"Encoding {i} to {i+batch_size} with {len(messages)} messages")
response = self.encode_queries(group_msg)
data.extend(response)
group_msg = message[i:i+batch_size]
response = self.client.embeddings.create(
model=self.model_info.get("default_path", None),
input=group_msg,
)
if len(messages) > batch_size:
self.embed_state[task_id]['progress'] = len(messages)
self.embed_state[task_id]['status'] = 'completed'
data.extend([a.embedding for a in response.data])
return data
if len(message) > batch_size:
GLOBAL_EMBED_STATE[task_id]['progress'] = len(message)
GLOBAL_EMBED_STATE[task_id]['status'] = 'completed'
class ZhipuEmbedding(RemoteEmbeddingModel):
def __init__(self, config) -> None:
self.config = config
self.model = EMBED_MODEL_INFO[config.embed_model]["name"]
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))
def predict(self, message):
response = self.client.embeddings.create(
model=self.model,
input=message,
)
data = [a.embedding for a in response.data]
return data
def encode(self, message):
return self.predict(message)
def encode_queries(self, queries):
# queries = [self.query_instruction_for_retrieval + query for query in queries]
return self.predict(queries)
class SiliconFlowEmbedding(RemoteEmbeddingModel):
def __init__(self, config) -> None:
self.url = "https://api.siliconflow.cn/v1/embeddings"
self.model = EMBED_MODEL_INFO[config.embed_model]["name"]
api_key = os.getenv("SILICONFLOW_API_KEY")
assert api_key, "SILICONFLOW_API_KEY is required"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def encode(self, message):
payload = self.build_payload(message)
response = requests.request("POST", self.url, json=payload, headers=self.headers)
response = json.loads(response.text)
# logger.debug(f"SiliconFlow Embedding response: {response}")
assert response["data"], f"SiliconFlow Embedding failed: {response}"
data = [a["embedding"] for a in response["data"]]
return data
def encode_queries(self, queries):
return self.encode(queries)
def build_payload(self, message):
return {
"model": self.model,
"input": message,
}
def get_embedding_model(config):
if not config.enable_knowledge_base:
return None
provider, model_name = config.embed_model.split('/', 1)
assert config.embed_model in EMBED_MODEL_INFO.keys(), f"Unsupported embed model: {config.embed_model}, only support {EMBED_MODEL_INFO.keys()}"
logger.debug(f"Loading embedding model {config.embed_model}")
if provider == "local":
model = LocalEmbeddingModel(config)
if config.embed_model in ["bge-large-zh-v1.5"]:
model = EmbeddingModel(EMBED_MODEL_INFO[config.embed_model], config)
if provider == "zhipu":
model = ZhipuEmbedding(config)
if config.embed_model in ["zhipu-embedding-2", "zhipu-embedding-3"]:
model = ZhipuEmbedding(EMBED_MODEL_INFO[config.embed_model], config)
if provider == "siliconflow":
model = SiliconFlowEmbedding(config)
return model

View File

@ -2,18 +2,20 @@ import os
import requests
import numpy as np
from typing import List, Union, Dict
from src.models.embedding import RemoteEmbeddingModel
from src.utils.logging_config import setup_logger
logger = setup_logger("OllamaEmbedding")
class OllamaEmbedding:
class OllamaEmbedding(RemoteEmbeddingModel):
"""
使用 Ollama API 进行文本嵌入的类
"""
def __init__(self, model_info: Dict, config) -> None:
"""
初始化 Ollama Embedding 模型
Args:
model_info: 模型信息字典
config: 配置对象
@ -28,10 +30,10 @@ class OllamaEmbedding:
def _get_embedding(self, text: str) -> List[float]:
"""
获取单个文本的嵌入向量
Args:
text: 输入文本
Returns:
嵌入向量
"""
@ -50,10 +52,10 @@ class OllamaEmbedding:
def predict(self, messages: List[str]) -> List[List[float]]:
"""
批量获取文本嵌入向量
Args:
messages: 文本列表
Returns:
嵌入向量列表
"""
@ -63,23 +65,23 @@ class OllamaEmbedding:
for i in range(0, len(messages), batch_size):
batch = messages[i:i + batch_size]
logger.info(f"Processing batch {i//batch_size + 1}, size: {len(batch)}")
batch_embeddings = []
for text in batch:
embedding = self._get_embedding(text)
batch_embeddings.append(embedding)
embeddings.extend(batch_embeddings)
return embeddings
def encode(self, messages: Union[str, List[str]]) -> List[List[float]]:
"""
编码文本
Args:
messages: 单个文本或文本列表
Returns:
嵌入向量列表
"""
@ -90,10 +92,10 @@ class OllamaEmbedding:
def encode_queries(self, queries: List[str]) -> List[List[float]]:
"""
编码查询文本
Args:
queries: 查询文本列表
Returns:
查询文本的嵌入向量列表
"""
@ -107,7 +109,7 @@ class OllamaReranker:
def __init__(self, config) -> None:
"""
初始化 Ollama Reranker
Args:
config: 配置对象
"""
@ -119,16 +121,16 @@ class OllamaReranker:
def compute_score(self, query: str, passage: str) -> float:
"""
计算查询和文本段落之间的相关性分数
Args:
query: 查询文本
passage: 段落文本
Returns:
相关性分数
"""
prompt = f"Query: {query}\nPassage: {passage}\nRate the relevance of the passage to the query on a scale of 0 to 1:"
try:
response = requests.post(
f"{self.base_url}/api/generate",
@ -139,7 +141,7 @@ class OllamaReranker:
}
)
response.raise_for_status()
# 提取生成的数字作为分数
result = response.json()["response"].strip()
try:
@ -148,7 +150,7 @@ class OllamaReranker:
except ValueError:
logger.warning(f"Could not parse score from response: {result}")
return 0.0
except Exception as e:
logger.error(f"Error computing rerank score: {str(e)}")
return 0.0
@ -156,12 +158,12 @@ class OllamaReranker:
def rerank(self, query: str, passages: List[str], top_n: int = None) -> List[Dict]:
"""
重新排序文本段落
Args:
query: 查询文本
passages: 段落文本列表
top_n: 返回前 n 个结果
Returns:
排序后的结果列表每个元素包含索引和分数
"""
@ -169,11 +171,11 @@ class OllamaReranker:
for i, passage in enumerate(passages):
score = self.compute_score(query, passage)
scores.append({"index": i, "score": score})
# 按分数降序排序
sorted_results = sorted(scores, key=lambda x: x["score"], reverse=True)
if top_n:
sorted_results = sorted_results[:top_n]
return sorted_results
return sorted_results

View File

@ -0,0 +1,70 @@
import os
import json
import requests
import numpy as np
from FlagEmbedding import FlagReranker
from src.config import RERANKER_LIST
from src.utils.logging_config import setup_logger
logger = setup_logger("RerankModel")
class LocalReranker(FlagReranker):
def __init__(self, config, **kwargs):
model_info = RERANKER_LIST[config.reranker]
model_name_or_path = config.model_local_paths.get(model_info["name"], model_info.get("default_path"))
logger.info(f"Loading Reranker model {config.reranker} from {model_name_or_path}")
super().__init__(model_name_or_path, use_fp16=True, **kwargs)
logger.info(f"Reranker model {config.reranker} loaded")
def sigmoid(x):
return 1 / (1 + np.exp(-x))
class SilconFlowReranker():
def __init__(self, config, **kwargs):
self.url = "https://api.siliconflow.cn/v1/rerank"
self.model = RERANKER_LIST[config.reranker]["name"]
api_key = os.getenv("SILICONFLOW_API_KEY")
assert api_key, "SILICONFLOW_API_KEY is required"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def compute_score(self, sentence_pairs, batch_size = 256, max_length = 512, normalize = False):
# TODO 还没实现 batch_size
query, sentences = sentence_pairs[0], sentence_pairs[1]
payload = self.build_payload(query, sentences, max_length)
response = requests.request("POST", self.url, json=payload, headers=self.headers)
response = json.loads(response.text)
logger.debug(f"SiliconFlow Reranker response: {response}")
results = sorted(response["results"], key=lambda x: x["index"])
all_scores = [result["relevance_score"] for result in results]
if normalize:
all_scores = [sigmoid(score) for score in all_scores]
return all_scores
def build_payload(self, query, sentences, max_length = 512):
return {
"model": self.model,
"query": query,
"documents": sentences,
"max_chunks_per_doc": max_length,
}
def get_reranker(config):
assert config.reranker in RERANKER_LIST.keys(), f"Unsupported Reranker: {config.reranker}, only support {RERANKER_LIST.keys()}"
provider, model_name = config.reranker.split('/', 1)
if provider == "local":
return LocalReranker(config)
elif provider == "siliconflow":
return SilconFlowReranker(config)

View File

@ -6,7 +6,6 @@ from concurrent.futures import ThreadPoolExecutor
from src.core import HistoryManager
from src.core.startup import startup
from src.utils.logging_config import setup_logger
from src.utils.web_search import WebSearcher
chat = APIRouter(prefix="/chat")
logger = setup_logger("server-chat")
@ -14,7 +13,6 @@ logger = setup_logger("server-chat")
executor = ThreadPoolExecutor()
refs_pool = {}
web_searcher = WebSearcher()
@chat.get("/")
async def chat_get():
@ -29,68 +27,68 @@ def chat_post(
history_manager = HistoryManager(history)
def make_chunk(content, status, history):
def make_chunk(content=None, **kwargs):
return json.dumps({
"response": content,
"history": history,
"model_name": startup.config.model_name,
"status": status,
"meta": meta,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n"
def need_retrieve(meta):
return meta.get("use_web") or meta.get("use_graph") or meta.get("db_name")
def generate_response():
modified_query = query
# 处理网页搜索
if meta and meta.get("enable_web_search"):
chunk = make_chunk("正在进行网络搜索...", "searching", history=None)
refs = None
# 处理知识库检索
if meta and need_retrieve(meta):
chunk = make_chunk(status="searching")
yield chunk
try:
search_results = web_searcher.search(query)
if search_results:
search_context = web_searcher.format_search_results(search_results)
# 将搜索结果添加到查询中
modified_query = f"""基于以下网络搜索结果回答问题:
{search_context}
用户问题{query}
请综合以上搜索结果给出准确客观的回答如果搜索结果与问题相关性不大请直接基于你的知识回答
"""
logger.info(f"Web search results added to query")
else:
logger.warning("No web search results found")
modified_query, refs = startup.retriever(modified_query, history_manager.messages, meta)
except Exception as e:
logger.error(f"Web search error: {str(e)}")
chunk = make_chunk("网络搜索失败,将直接回答问题。", "loading", history=None)
yield chunk
logger.error(f"Retriever error: {e}")
yield make_chunk(message=f"Retriever error: {e}", status="error")
return
# 处理知识库检索
if meta and meta.get("enable_retrieval"):
chunk = make_chunk("", "searching", history=None)
yield chunk
modified_query, refs = startup.retriever(modified_query, history_manager.messages, meta)
refs_pool[cur_res_id] = refs
yield make_chunk(status="generating")
messages = history_manager.get_history_with_msg(modified_query, max_rounds=meta.get('history_round'))
history_manager.add_user(query) # 注意这里使用原始查询
logger.debug(f"Final query: {modified_query}")
logger.debug(f"Web history: {history_manager.messages}")
content = ""
for delta in startup.model.predict(messages, stream=True):
if not delta.content:
continue
reasoning_content = ""
try:
for delta in startup.model.predict(messages, stream=True):
if not delta.content and hasattr(delta, 'reasoning_content'):
reasoning_content += delta.reasoning_content or ""
chunk = make_chunk(reasoning_content=reasoning_content, status="reasoning")
yield chunk
continue
if hasattr(delta, 'is_full') and delta.is_full:
content = delta.content
else:
content += delta.content
# 文心一言
if hasattr(delta, 'is_full') and delta.is_full:
content = delta.content
else:
content += delta.content or ""
chunk = make_chunk(content, "loading", history=history_manager.update_ai(content))
yield chunk
chunk = make_chunk(content=content, status="loading")
yield chunk
logger.debug(f"Final response: {content}")
logger.debug(f"Final reasoning response: {reasoning_content}")
yield make_chunk(content=content,
status="finished",
history=history_manager.update_ai(content),
refs=refs)
except Exception as e:
logger.error(f"Model error: {e}")
yield make_chunk(message=f"Model error: {e}", status="error")
return
return StreamingResponse(generate_response(), media_type='application/json')
@ -105,6 +103,17 @@ async def call(query: str = Body(...), meta: dict = Body(None)):
return {"response": response.content}
@chat.post("/call_lite")
async def call(query: str = Body(...), meta: dict = Body(None)):
async def predict_async(query):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, startup.model_lite.predict, query)
response = await predict_async(query)
logger.debug({"query": query, "response": response.content})
return {"response": response.content}
@chat.get("/refs")
def get_refs(cur_res_id: str):
global refs_pool

0
src/static/config.yaml Normal file
View File

View File

@ -14,6 +14,7 @@ MODEL_NAMES:
name: DeepSeek
url: https://platform.deepseek.com/api-docs/zh-cn/pricing
default: deepseek-chat
base_url: https://api.deepseek.com/v1
env:
- DEEPSEEK_API_KEY
models:
@ -23,6 +24,7 @@ MODEL_NAMES:
name: 智谱AI (Zhipu)
url: https://open.bigmodel.cn/dev/api
default: glm-4-flash
base_url: https://open.bigmodel.cn/api/paas/v4/
env:
- ZHIPUAI_API_KEY
models:
@ -33,6 +35,19 @@ MODEL_NAMES:
- glm-4-long
- glm-4-flashx
- glm-4-flash
siliconflow:
name: SiliconFlow
url: https://cloud.siliconflow.cn/models
default: Qwen/Qwen2.5-7B-Instruct
base_url: https://api.siliconflow.cn/v1
env:
- SILICONFLOW_API_KEY
models:
- meta-llama/Meta-Llama-3.1-8B-Instruct
- Qwen/Qwen2.5-7B-Instruct
- deepseek-ai/DeepSeek-R1
- deepseek-ai/DeepSeek-V3
- deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
qianfan:
name: 百度千帆 (QianFan)
url: https://open.bigmodel.cn/dev/api
@ -55,45 +70,51 @@ MODEL_NAMES:
- DASHSCOPE_API_KEY
models:
- qwen-max-latest
- qwen-plus-latest
- qwen-long-latest
- qwen-turbo-latest
- qwen2.5-72b-instruct
- qwen2.5-32b-instruct
- qwen2.5-14b-instruct
- qwen2.5-7b-instruct
- qwen2.5-3b-instruct
- qwen2.5-1.5b-instruct
- qwen2.5-0.5b-instruct
siliconflow:
name: SiliconFlow
url: https://cloud.siliconflow.cn/models
default: meta-llama/Meta-Llama-3.1-8B-Instruct
ark:
name: 豆包Ark
url: https://console.volcengine.com/ark/region:ark+cn-beijing/model
default: doubao-1-5-pro-32k-250115
base_url: https://ark.cn-beijing.volces.com/api/v3
env:
- SILICONFLOW_API_KEY
- ARK_API_KEY
models:
- meta-llama/Meta-Llama-3.1-8B-Instruct
- meta-llama/Meta-Llama-3.1-70B-Instruct
- meta-llama/Meta-Llama-3.1-405B-Instruct
- doubao-1-5-pro-32k-250115
- doubao-1-5-lite-32k-250115
- deepseek-r1-250120
lingyiwanwu:
name: 零一万物
url: https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B%E4%B8%8E%E8%AE%A1%E8%B4%B9
base_url: https://api.lingyiwanwu.com/v1
default: yi-lightning
env:
- LINGYIWANWU_API_KEY
models:
- yi-lightning
EMBED_MODEL_INFO:
bge-large-zh-v1.5:
name: bge-large-zh-v1.5
default_path: BAAI/bge-large-zh-v1.5
dimension: 1024
query_instruction: "为这个句子生成表示以用于检索相关文章:"
bge-m3:
name: bge-m3
local/BAAI/bge-m3:
name: BAAI/bge-m3
default_path: BAAI/bge-m3
dimension: 1024
zhipu-embedding-2:
name: zhipu-embedding-2
default_path: embedding-2
zhipu/zhipu-embedding-2:
name: embedding-2
dimension: 1024
zhipu-embedding-3:
name: zhipu-embedding-3
default_path: embedding-3
zhipu/zhipu-embedding-3:
name: embedding-3
dimension: 2048
siliconflow/BAAI/bge-m3:
name: BAAI/bge-m3
dimension: 1024
RERANKER_LIST:
bge-reranker-v2-m3: BAAI/bge-reranker-v2-m3
local/BAAI/bge-reranker-v2-m3:
name: BAAI/bge-reranker-v2-m3
default_path: BAAI/bge-reranker-v2-m3
siliconflow/BAAI/bge-reranker-v2-m3:
name: BAAI/bge-reranker-v2-m3

View File

@ -16,11 +16,11 @@ class WebSearcher:
def search(self, query: str, max_results: int = 1) -> List[Dict]:
"""
使用 Tavily 搜索相关内容
Args:
query: 搜索查询
max_results: 最大返回结果数
Returns:
搜索结果列表
"""
@ -30,7 +30,7 @@ class WebSearcher:
search_depth="basic",
max_results=max_results
)
# 提取需要的信息
formatted_results = []
for result in search_results['results'][:max_results]:
@ -40,9 +40,9 @@ class WebSearcher:
'url': result.get('url', ''),
'score': result.get('score', 0)
})
return formatted_results
except Exception as e:
logger.error(f"Error during web search: {str(e)}")
return []
@ -50,20 +50,20 @@ class WebSearcher:
def format_search_results(self, results: List[Dict]) -> str:
"""
将搜索结果格式化为文本
Args:
results: 搜索结果列表
Returns:
格式化后的文本
"""
if not results:
return "没有找到相关的网络搜索结果。"
formatted_text = "以下是相关的网络搜索结果:\n\n"
for i, result in enumerate(results, 1):
formatted_text += f"{i}. {result['title']}\n"
formatted_text += f" {result['content']}\n"
formatted_text += f" 来源: {result['url']}\n\n"
return formatted_text
return formatted_text

121
test/test_concurrency.py Normal file
View File

@ -0,0 +1,121 @@
import asyncio
import aiohttp
import time
import json
from typing import List
async def make_request(session: aiohttp.ClientSession, request_id: int) -> dict:
"""发送单个请求到API"""
url = "http://localhost:5000/chat/call"
payload = {
"query": "写一个冒泡排序",
"meta": {}
}
start_time = time.time()
print(f"请求 {request_id} 开始时间: {time.strftime('%H:%M:%S', time.localtime(start_time))}")
try:
async with session.post(url, json=payload) as response:
result = await response.json()
end_time = time.time()
duration = end_time - start_time
print(f"请求 {request_id} 完成时间: {time.strftime('%H:%M:%S', time.localtime(end_time))} (耗时: {duration:.2f}秒)")
return {
"request_id": request_id,
"status": response.status,
"time": duration,
"start_time": start_time,
"end_time": end_time,
"success": True
}
except Exception as e:
end_time = time.time()
duration = end_time - start_time
print(f"请求 {request_id} 失败时间: {time.strftime('%H:%M:%S', time.localtime(end_time))} (耗时: {duration:.2f}秒)")
return {
"request_id": request_id,
"status": None,
"time": duration,
"start_time": start_time,
"end_time": end_time,
"success": False,
"error": str(e)
}
async def run_concurrent_test(num_requests: int = 10) -> List[dict]:
"""运行并发测试"""
async with aiohttp.ClientSession() as session:
tasks = [make_request(session, i) for i in range(num_requests)]
return await asyncio.gather(*tasks)
def analyze_results(results: List[dict]) -> None:
"""分析并打印测试结果"""
total_requests = len(results)
successful_requests = sum(1 for r in results if r["success"])
failed_requests = total_requests - successful_requests
response_times = [r["time"] for r in results if r["success"]]
if response_times:
avg_time = sum(response_times) / len(response_times)
max_time = max(response_times)
min_time = min(response_times)
else:
avg_time = max_time = min_time = 0
print(f"\n=== 并发测试结果 ===")
print(f"总请求数: {total_requests}")
print(f"成功请求: {successful_requests}")
print(f"失败请求: {failed_requests}")
print(f"平均响应时间: {avg_time:.2f}")
print(f"最长响应时间: {max_time:.2f}")
print(f"最短响应时间: {min_time:.2f}")
if failed_requests > 0:
print("\n失败的请求:")
for result in results:
if not result["success"]:
print(f"请求 ID {result['request_id']}: {result.get('error', '未知错误')}")
# 添加请求时间线分析
print("\n=== 请求时间线分析 ===")
sorted_results = sorted(results, key=lambda x: x["start_time"])
test_start_time = sorted_results[0]["start_time"]
print("\n时间线详情:")
print("请求ID 开始时间 结束时间 耗时(秒) 重叠请求数")
active_requests = []
for result in sorted_results:
# 计算当前时间点的活跃请求数
start_time = result["start_time"]
end_time = result["end_time"]
# 清理已完成的请求
active_requests = [t for t in active_requests if t > start_time]
active_requests.append(end_time)
print(f"{result['request_id']:^7} {time.strftime('%H:%M:%S', time.localtime(start_time))} "
f"{time.strftime('%H:%M:%S', time.localtime(end_time))} "
f"{result['time']:^8.2f} {len(active_requests):^6}")
# 计算最大并发数
max_concurrent = 0
timeline = []
for r in results:
timeline.append((r["start_time"], 1))
timeline.append((r["end_time"], -1))
timeline.sort(key=lambda x: x[0])
current_concurrent = 0
for _, change in timeline:
current_concurrent += change
max_concurrent = max(max_concurrent, current_concurrent)
print(f"\n最大并发请求数: {max_concurrent}")
if __name__ == "__main__":
NUM_REQUESTS = 100 # 设置并发请求数
print(f"开始运行 {NUM_REQUESTS} 个并发请求的测试...")
results = asyncio.run(run_concurrent_test(NUM_REQUESTS))
analyze_results(results)

View File

@ -16,26 +16,6 @@
</a-tooltip>
</div>
<div class="header__right">
<div class="nav-btn text metas" v-if="meta.use_graph && meta.enable_retrieval">
<GoldOutlined /> 图数据库
</div>
<a-dropdown v-if="meta.selectedKB !== null && meta.enable_retrieval">
<a class="ant-dropdown-link nav-btn" @click.prevent>
<!-- <component :is="meta.selectedKB === null ? BookOutlined : BookFilled" /> -->
<BookOutlined />
<span class="text">{{ meta.selectedKB === null ? '不使用' : opts.databases[meta.selectedKB]?.name }}</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="useDatabase(index)">
<a href="javascript:;" >{{ db.name }}</a>
</a-menu-item>
<a-menu-item @click="useDatabase(null)">
<a href="javascript:;">不使用</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<div class="nav-btn text" @click="opts.showPanel = !opts.showPanel">
<component :is="opts.showPanel ? FolderOpenOutlined : FolderOutlined" /> <span class="text">选项</span>
</div>
@ -46,47 +26,9 @@
<div class="flex-center" @click="meta.summary_title = !meta.summary_title">
总结对话标题 <div @click.stop><a-switch v-model:checked="meta.summary_title" /></div>
</div>
<div class="flex-center" @click="meta.enable_retrieval = !meta.enable_retrieval">
启用检索 <div @click.stop><a-switch v-model:checked="meta.enable_retrieval" /></div>
</div>
<div class="flex-center">
最大历史轮数 <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
</div>
<a-divider v-if="meta.enable_retrieval"></a-divider>
<div class="flex-center" v-if="configStore.config.enable_knowledge_base && meta.enable_retrieval">
知识库
<div @click.stop>
<a-dropdown>
<a class="ant-dropdown-link " @click.prevent>
<!-- <component :is="meta.selectedKB === null ? BookOutlined : BookFilled" />&nbsp; -->
<BookOutlined />&nbsp;
<span class="text">{{ meta.selectedKB === null ? '不使用' : opts.databases[meta.selectedKB]?.name }}</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="useDatabase(index)">
<a href="javascript:;">{{ db.name }}</a>
</a-menu-item>
<a-menu-item @click="useDatabase(null)">
<a href="javascript:;">不使用</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
<div class="flex-center" @click="meta.use_graph = !meta.use_graph" v-if="configStore.config.enable_knowledge_base && meta.enable_retrieval">
图数据库 <div @click.stop><a-switch v-model:checked="meta.use_graph" /></div>
</div>
<div class="flex-center" @click="meta.use_web = !meta.use_web" v-if="configStore.config.enable_search_engine && meta.enable_retrieval">
搜索引擎Bing <div @click.stop><a-switch v-model:checked="meta.use_web" /></div>
</div>
<div class="flex-center" @click="meta.enable_web_search = !meta.enable_web_search">
网页搜索 <div @click.stop><a-switch v-model:checked="meta.enable_web_search" /></div>
</div>
<!-- <div class="flex-center" v-if="configStore.config.enable_knowledge_base && meta.enable_retrieval">
重写查询 <a-segmented v-model:value="meta.use_rewrite_query" :options="['off', 'on', 'hyde']"/>
</div> -->
</div>
</div>
</div>
@ -110,6 +52,24 @@
class="message-box"
:class="message.role"
>
<div v-if="message.reasoning_content" class="reasoning-msg">
<a-collapse
v-model:activeKey="message.showThinking"
:bordered="false"
style="background: rgb(255, 255, 255)"
>
<template #expandIcon="{ isActive }">
<caret-right-outlined :rotate="isActive ? 90 : 0" />
</template>
<a-collapse-panel
key="show"
:header="message.status=='reasoning' ? '正在思考...' : '推理过程'"
:style="'background: #f7f7f7; border-radius: 8px; margin-bottom: 24px; border: 0; overflow: hidden'"
>
<p style="color: var(--gray-800)">{{ message.reasoning_content }}</p>
</a-collapse-panel>
</a-collapse>
</div>
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
<div v-else-if="message.text.length == 0 && message.status=='init'" class="loading-dots">
<div></div>
@ -117,12 +77,13 @@
<div></div>
</div>
<div v-else-if="message.status == 'searching' && isStreaming" class="searching-msg"><i>正在检索</i></div>
<div v-else-if="message.status == 'generating' && isStreaming" class="searching-msg"><i>正在生成</i></div>
<div
v-else-if="message.text.length == 0 || message.status == 'error' || (message.status != 'finished' && !isStreaming)"
v-else-if="(message.text.length == 0 && message.status!='reasoning') || message.status == 'error' || (message.status != 'finished' && !isStreaming)"
class="err-msg"
@click="retryMessage(message.id)"
>
请求错误请重试
请求错误请重试{{ message.message }}
</div>
<div v-else
v-html="renderMarkdown(message)"
@ -133,18 +94,61 @@
</div>
<div class="bottom">
<div class="input-box">
<a-textarea
class="user-input"
v-model:value="conv.inputText"
@keydown="handleKeyDown"
placeholder="输入问题……"
:auto-size="{ minRows: 1, maxRows: 10 }"
/>
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)" type="link">
<template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
<div class="input-area">
<a-textarea
class="user-input"
v-model:value="conv.inputText"
@keydown="handleKeyDown"
placeholder="输入问题……"
:auto-size="{ minRows: 2, maxRows: 10 }"
/>
</div>
<div class="input-options">
<div class="options__left">
<div
:class="{'switch': true, 'opt-item': true, 'active': meta.use_web}"
v-if="configStore.config.enable_web_search"
@click="meta.use_web=!meta.use_web"
>
<CompassOutlined style="margin-right: 3px;"/>
联网搜索
</div>
<div
:class="{'switch': true, 'opt-item': true, 'active': meta.use_graph}"
v-if="configStore.config.enable_knowledge_graph"
@click="meta.use_graph=!meta.use_graph"
>
<DeploymentUnitOutlined style="margin-right: 3px;"/>
知识图谱
</div>
<a-dropdown
v-if="configStore.config.enable_knowledge_base && opts.databases.length > 0"
:class="{'opt-item': true, 'active': meta.selectedKB !== null}"
>
<a class="ant-dropdown-link" @click.prevent>
<BookOutlined style="margin-right: 3px;"/>
<span class="text">{{ meta.selectedKB === null ? '不使用知识库' : opts.databases[meta.selectedKB]?.name }}</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="useDatabase(index)">
<a href="javascript:;">{{ db.name }}</a>
</a-menu-item>
<a-menu-item @click="useDatabase(null)">
<a href="javascript:;">不使用</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="options__right">
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)" type="link">
<template #icon> <ArrowUpOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
</div>
</div>
</div>
<p class="note">请注意辨别内容的可靠性 模型供应商{{ configStore.config?.model_provider }}: {{ configStore.config?.model_name }}</p>
<p class="note">请注意辨别内容的可靠性 By {{ configStore.config?.model_provider }}: {{ configStore.config?.model_name }}</p>
</div>
</div>
</template>
@ -158,6 +162,8 @@ import {
LoadingOutlined,
BookOutlined,
BookFilled,
CompassOutlined,
ArrowUpOutlined,
CompassFilled,
GoldenFilled,
GoldOutlined,
@ -169,8 +175,8 @@ import {
GlobalOutlined,
FileTextOutlined,
RobotOutlined,
EditOutlined,
PlusOutlined,
CaretRightOutlined,
DeploymentUnitOutlined,
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { Marked } from 'marked';
@ -208,15 +214,12 @@ const opts = reactive({
})
const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
enable_retrieval: false,
use_graph: false,
use_web: false,
enable_web_search: false,
graph_name: "neo4j",
// use_rewrite_query: "off",
selectedKB: null,
stream: true,
summary_title: true,
summary_title: false,
history_round: 5,
db_name: null,
})
@ -235,15 +238,15 @@ const marked = new Marked(
})
);
const consoleMsg = (message) => console.log(message)
const consoleMsg = (msg) => console.log(msg)
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))
onClickOutside(modelCard, () => setTimeout(() => opts.showModelCard = false, 30))
const renderMarkdown = (message) => {
if (message.status === 'loading') {
return marked.parse(message.text + '🟢')
const renderMarkdown = (msg) => {
if (msg.status === 'loading') {
return marked.parse(msg.text + '🟢')
} else {
return marked.parse(message.text)
return marked.parse(msg.text)
}
}
@ -307,45 +310,73 @@ const generateRandomHash = (length) => {
return hash;
}
const appendUserMessage = (message) => {
const appendUserMessage = (msg) => {
conv.value.messages.push({
id: generateRandomHash(16),
role: 'sent',
text: message
text: msg
})
scrollToBottom()
}
const appendAiMessage = (message, refs=null) => {
const appendAiMessage = (text, refs=null) => {
conv.value.messages.push({
id: generateRandomHash(16),
role: 'received',
text: message,
text: text,
reasoning_content: '',
refs,
status: "init",
meta: {},
showThinking: "show"
})
scrollToBottom()
}
const updateMessage = (info) => {
const message = conv.value.messages.find((message) => message.id === info.id);
if (message) {
const msg = conv.value.messages.find((msg) => msg.id === info.id);
if (msg) {
try {
// text
if (info.text !== null && info.text !== undefined && info.text !== '') {
message.text = info.text;
msg.text = info.text;
}
if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') {
msg.reasoning_content = info.reasoning_content;
}
// refs
if (info.refs !== null && info.refs !== undefined) {
msg.refs = info.refs;
}
if (info.model_name !== null && info.model_name !== undefined && info.model_name !== '') {
msg.model_name = info.model_name;
}
// status
if (info.status !== null && info.status !== undefined && info.status !== '') {
message.status = info.status;
msg.status = info.status;
}
if (info.meta !== null && info.meta !== undefined) {
message.meta = info.meta;
msg.meta = info.meta;
}
if (info.message !== null && info.message !== undefined) {
msg.message = info.message;
}
if (info.showThinking !== null && info.showThinking !== undefined) {
msg.showThinking = info.showThinking;
}
scrollToBottom();
} catch (error) {
console.error('Error updating message:', error);
message.status = 'error';
message.text = '消息更新失败';
msg.status = 'error';
msg.text = '消息更新失败';
}
} else {
console.error('Message not found:', info.id);
@ -354,9 +385,9 @@ const updateMessage = (info) => {
const groupRefs = (id) => {
const message = conv.value.messages.find((message) => message.id === id)
if (message.refs && message.refs.knowledge_base.results.length > 0) {
message.groupedResults = message.refs.knowledge_base.results
const msg = conv.value.messages.find((msg) => msg.id === id)
if (msg.refs && msg.refs.knowledge_base.results.length > 0) {
msg.groupedResults = msg.refs.knowledge_base.results
.filter(result => result.file && result.file.filename)
.reduce((acc, result) => {
const { filename } = result.file;
@ -370,11 +401,11 @@ const groupRefs = (id) => {
scrollToBottom()
}
const simpleCall = (message) => {
const simpleCall = (msg) => {
return new Promise((resolve, reject) => {
fetch('/api/chat/call', {
fetch('/api/chat/call_lite', {
method: 'POST',
body: JSON.stringify({ query: message, }),
body: JSON.stringify({ query: msg, }),
headers: { 'Content-Type': 'application/json' }
})
.then((response) => response.json())
@ -393,21 +424,21 @@ const loadDatabases = () => {
}
// fetch
const fetchChatResponse = (requestData) => {
const fetchChatResponse = (user_input, cur_res_id) => {
fetch('/api/chat/', {
method: 'POST',
body: JSON.stringify({
query: user_input,
history: conv.value.history,
meta: meta,
cur_res_id: cur_res_id,
}),
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error("ReadableStream not supported.");
}
.then((response) => {
if (!response.body) throw new Error("ReadableStream not supported.");
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = '';
@ -415,25 +446,10 @@ const fetchChatResponse = (requestData) => {
const readChunk = () => {
return reader.read().then(({ done, value }) => {
if (done) {
const message = conv.value.messages.find((message) => message.id === requestData.cur_res_id)
console.log(message)
if (message.meta.enable_retrieval) {
console.log("fetching refs")
fetchRefs(requestData.cur_res_id).then((data) => {
console.log(data)
updateMessage({
id: requestData.cur_res_id,
refs: data,
status: "finished",
});
groupRefs(requestData.cur_res_id);
})
} else {
updateMessage({
id: requestData.cur_res_id,
status: "finished",
});
}
const msg = conv.value.messages.find((msg) => msg.id === cur_res_id)
console.log(msg)
groupRefs(cur_res_id);
updateMessage({showThinking: "no", id: cur_res_id});
isStreaming.value = false;
if (conv.value.messages.length === 2) { renameTitle(); }
return;
@ -449,11 +465,12 @@ const fetchChatResponse = (requestData) => {
try {
const data = JSON.parse(line);
updateMessage({
id: requestData.cur_res_id,
id: cur_res_id,
text: data.response,
model_name: data.model_name,
reasoning_content: data.reasoning_content,
status: data.status,
meta: data.meta,
...data,
});
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].text)
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].status)
@ -476,14 +493,12 @@ const fetchChatResponse = (requestData) => {
readChunk();
})
.catch((error) => {
console.error('Error in fetchChatResponse:', error);
console.error(error);
updateMessage({
id: requestData.cur_res_id,
id: cur_res_id,
status: "error",
text: `请求错误:${error.message}`,
});
isStreaming.value = false;
message.error(`请求失败:${error.message}`);
});
}
@ -508,36 +523,18 @@ const fetchRefs = (cur_res_id) => {
const sendMessage = () => {
const user_input = conv.value.inputText.trim();
const dbName = opts.databases.length > 0 ? opts.databases[meta.selectedKB]?.metaname : null;
if (isStreaming.value) {
message.error('请等待上一条消息处理完成');
return
}
if (user_input) {
isStreaming.value = true;
appendUserMessage(user_input);
appendAiMessage("", null);
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id;
conv.value.inputText = '';
//
const requestData = {
query: user_input,
history: conv.value.history,
cur_res_id: cur_res_id,
meta: {
enable_retrieval: meta.enable_retrieval,
use_graph: meta.use_graph,
use_web: meta.use_web,
enable_web_search: meta.enable_web_search,
graph_name: meta.graph_name,
rewriteQuery: meta.rewriteQuery,
selectedKB: meta.selectedKB,
stream: meta.stream,
summary_title: meta.summary_title,
history_round: meta.history_round,
db_name: dbName,
}
};
console.log('Sending request with data:', requestData); //
fetchChatResponse(requestData);
meta.db_name = dbName;
fetchChatResponse(user_input, cur_res_id)
} else {
console.log('请输入消息');
}
@ -545,7 +542,7 @@ const sendMessage = () => {
const retryMessage = (id) => {
// id message message message
const index = conv.value.messages.findIndex(message => message.id === id);
const index = conv.value.messages.findIndex(msg => msg.id === id);
const pastMessage = conv.value.messages[index-1]
console.log("retryMessage", id, pastMessage)
conv.value.inputText = pastMessage.text
@ -556,8 +553,8 @@ const retryMessage = (id) => {
sendMessage();
}
const autoSend = (message) => {
conv.value.inputText = message
const autoSend = (msg) => {
conv.value.inputText = msg
sendMessage()
}
@ -670,7 +667,6 @@ watch(
margin-right: 8px;
font-size: 16px;
}
.ant-switch {
&.ant-switch-checked {
background-color: var(--main-500);
@ -755,18 +751,25 @@ watch(
/* animation: slideInUp 0.1s ease-in; */
.err-msg {
color: #FF6B6B;
border: 1px solid #FF6B6B;
padding: 0.2rem 1rem;
color: #eb8080;
border: 1px solid #eb8080;
padding: 0.5rem 1rem;
border-radius: 8px;
text-align: center;
background: #FFF0F0;
text-align: left;
background: #FFF5F5;
margin-bottom: 10px;
cursor: pointer;
}
.searching-msg {
color: var(--gray-500);
animation: colorPulse 2s infinite;
}
@keyframes colorPulse {
0% { color: var(--gray-700); }
50% { color: var(--gray-300); }
100% { color: var(--gray-700); }
}
}
@ -783,6 +786,7 @@ watch(
text-align: left;
word-wrap: break-word;
margin: 0;
max-width: 100%;
padding-bottom: 0;
padding-top: 16px;
padding-left: 0;
@ -812,21 +816,56 @@ watch(
.input-box {
display: flex;
flex-direction: column;
width: 100%;
height: auto;
max-width: 900px;
margin: 0 auto;
align-items: flex-end;
padding: 0.25rem 0.5rem;
// box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
border: 2px solid #E5E5E5;
border: 2px solid var(--gray-200);
border-radius: 1rem;
background: #fcfdfd;
transition: background 0.3s, box-shadow 0.3s;
background: var(--gray-50);
transition: background, border 0.3s, box-shadow 0.3s;
&:focus-within {
border: 2px solid var(--main-500);
background: white;
// box-shadow: rgb(42 60 79 / 5%) 0px 4px 10px 0px;
}
.input-options {
display: flex;
padding: 4px 8px;
.options__left,
.options__right {
display: flex;
align-items: center;
gap: 8px;
}
.options__left {
flex: 1;
.opt-item {
border-radius: 12px;
border: 1px solid var(--gray-300);
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
color: var(--gray-700);
&.active {
color: var(--main-600);
border: 1px solid var(--main-500);
background-color: var(--main-10);
}
}
}
}
.input-area {
display: flex;
align-items: flex-end;
gap: 8px;
}
textarea.user-input {
@ -836,7 +875,7 @@ watch(
background-color: transparent;
border: none;
font-size: 1.2rem;
margin: 0 0.6rem;
margin: 0 0;
color: #111111;
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
@ -854,21 +893,23 @@ watch(
}
button.ant-btn-icon-only {
font-size: 1.25rem;
height: 32px;
width: 32px;
cursor: pointer;
background-color: transparent;
background-color: var(--main-color);
border-radius: 50%;
border: none;
transition: color 0.3s;
box-shadow: none;
color: var(--main-700);;
color: white;
padding: 0;
&:hover {
color: var(--gray-1000);
background-color: var(--main-800);
}
&:disabled {
color: #ccc;
background-color: var(--gray-400);
cursor: not-allowed;
}
}
@ -1009,7 +1050,6 @@ watch(
display: flex;
align-items: center;
gap: 8px;
.search-switch {
margin-right: 8px;
}
@ -1029,6 +1069,7 @@ watch(
}
.message-md {
max-width: 100%;
h1, h2, h3, h4, h5, h6 {
font-size: 1rem;
}

View File

@ -1,95 +1,95 @@
<template>
<div class="graph-container" ref="container"></div>
</template>
<div class="graph-container" ref="container"></div>
</template>
<script setup>
import { Graph } from "@antv/g6";
import { onMounted, watch, ref } from 'vue';
<script setup>
import { Graph } from "@antv/g6";
import { onMounted, watch, ref } from 'vue';
const props = defineProps({
graphData: {
type: Object,
required: true,
default: () => ({ nodes: [], edges: [] })
}
});
const container = ref(null);
let graphInstance = null;
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 20,
collide: {
strength: 1.0,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 70,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
};
const renderGraph = () => {
if (!graphInstance) {
initGraph();
}
const formattedData = {
nodes: props.graphData.nodes.map(node => ({
id: node.id,
data: { label: node.name }
})),
edges: props.graphData.edges.map(edge => ({
source: edge.source_id,
target: edge.target_id,
data: { label: edge.type }
}))
};
graphInstance.setData(formattedData);
graphInstance.render();
};
onMounted(() => {
renderGraph();
window.addEventListener('resize', renderGraph);
});
watch(() => props.graphData, renderGraph, { deep: true });
</script>
<style scoped>
.graph-container {
background: #F7F7F7;
border-radius: 16px;
width: 100%;
height: 600px;
overflow: hidden;
const props = defineProps({
graphData: {
type: Object,
required: true,
default: () => ({ nodes: [], edges: [] })
}
</style>
});
const container = ref(null);
let graphInstance = null;
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 20,
collide: {
strength: 1.0,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 70,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
};
const renderGraph = () => {
if (!graphInstance) {
initGraph();
}
const formattedData = {
nodes: props.graphData.nodes.map(node => ({
id: node.id,
data: { label: node.name }
})),
edges: props.graphData.edges.map(edge => ({
source: edge.source_id,
target: edge.target_id,
data: { label: edge.type }
}))
};
graphInstance.setData(formattedData);
graphInstance.render();
};
onMounted(() => {
renderGraph();
window.addEventListener('resize', renderGraph);
});
watch(() => props.graphData, renderGraph, { deep: true });
</script>
<style scoped>
.graph-container {
background: #F7F7F7;
border-radius: 16px;
width: 100%;
height: 600px;
overflow: hidden;
}
</style>

View File

@ -1,16 +1,23 @@
<template>
<div class="refs" v-if="showRefs">
<div class="tags">
<span class="item btn" @click="copyText(msg.text)"><CopyOutlined /></span>
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
<span class="item"><GlobalOutlined /> {{ msg.model_name }}</span>
<span class="item"><BulbOutlined /> {{ msg.model_name }}</span>
<span class="item btn" @click="copyText(msg.text)"><CopyOutlined /></span>
<span
class="item btn"
@click="openSubGraph(msg)"
v-if="hasSubGraphData(msg)"
>
<GlobalOutlined /> 关系图
<DeploymentUnitOutlined /> 关系图
</span>
<span
class="item btn"
@click="showWebResult(msg)"
v-if="msg.refs?.web_search.results.length > 0"
>
<GlobalOutlined /> 网页搜索 {{ msg.refs.web_search?.results.length }}
</span>
<span class="filetag item btn"
v-for="(results, filename) in msg.groupedResults"
@ -21,7 +28,7 @@
<a-drawer
v-model:open="openDetail[filename]"
:title="filename"
width="800"
width="700"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
class="retrieval-detail"
@ -60,6 +67,35 @@
>
<GraphContainer :graphData="subGraphData" />
</a-modal>
<a-drawer
v-model:open="webResultVisible"
title="网页搜索结果"
width="700"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
class="web-result-detail"
rootClassName="root"
>
<div class="results-list">
<div v-for="result in webResults" :key="result.url" class="result-item">
<div class="result-meta">
<div class="score-info">
<span>
<strong>相关度</strong>
<a-progress :percent="getPercent(result.score)"/>
</span>
</div>
<div class="result-url">
<a :href="result.url" target="_blank">{{ result.url }}</a>
</div>
</div>
<div class="result-content">
<h3 class="result-title">{{ result.title }}</h3>
<div class="result-text">{{ result.content }}</div>
</div>
</div>
</div>
</a-drawer>
</div>
</template>
@ -73,6 +109,8 @@ import {
CopyOutlined,
LikeOutlined,
DislikeOutlined,
DeploymentUnitOutlined,
BulbOutlined,
FileOutlined,
ClockCircleOutlined
} from '@ant-design/icons-vue'
@ -143,6 +181,20 @@ const closeSubGraph = () => {
subGraphVisible.value = false
}
//
const webResultVisible = ref(false)
const webResults = ref(null)
//
const showWebResult = (msg) => {
if (msg.refs?.web_search) {
webResults.value = msg.refs?.web_search.results
webResultVisible.value = true
} else {
console.error('无法获取网页搜索结果')
}
}
const hasSubGraphData = (msg) => {
return msg.refs &&
msg.refs.graph_base &&
@ -165,15 +217,15 @@ const getPercent = (value) => {
display: flex;
margin-bottom: 20px;
color: var(--gray-500);
font-size: 14px;
font-size: 13px;
gap: 10px;
.item {
background: var(--gray-100);
color: var(--gray-800);
color: var(--gray-700);
padding: 2px 8px;
border-radius: 8px;
font-size: 14px;
font-size: 13px;
user-select: none;
&.btn {
@ -275,4 +327,76 @@ const getPercent = (value) => {
margin-bottom: 12px;
}
}
.web-result-detail {
.results-list {
.result-item {
border-bottom: 1px solid #f0f0f0;
padding: 16px 0;
&:last-child {
border-bottom: none;
}
}
.result-meta {
margin-bottom: 12px;
.score-info {
display: flex;
flex-wrap: wrap;
gap: 2rem;
margin-bottom: 8px;
span {
display: flex;
align-items: center;
strong {
margin-right: 8px;
white-space: nowrap;
color: #666;
}
.ant-progress {
width: 170px;
margin-bottom: 0;
margin-inline: 10px;
.ant-progress-bg {
background-color: #666;
}
}
}
}
.result-url {
font-size: 12px;
color: #1677FF;
margin-bottom: 8px;
word-break: break-all;
}
}
.result-content {
.result-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.result-text {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
background-color: #f9f9f9;
padding: 12px;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
}
}
}
</style>

View File

@ -43,7 +43,7 @@
<a-modal
title="添加路径映射"
v-model:visible="addConfigModalVisible"
v-model:open="addConfigModalVisible"
@ok="confirmAddConfig"
class="config-modal"
>
@ -91,12 +91,14 @@ props.config && Object.entries(props.config).forEach(([key, value]) => {
//
const addConfigModalVisible = ref(false);
const isAdding = ref(false); // ref
//
const newConfig = ref({ key: '', value: '' });
//
const addConfig = () => {
isAdding.value = true; //
addConfigModalVisible.value = true;
};
@ -113,6 +115,7 @@ const confirmAddConfig = () => {
configList.push({ key: newConfig.value.key, value: newConfig.value.value });
addConfigModalVisible.value = false;
newConfig.value = { key: '', value: '' };
isAdding.value = false; //
};
//
@ -137,6 +140,13 @@ const configObject = computed(() => {
watch(configObject, (newValue) => {
emit('update:config', newValue);
}, { deep: true });
//
watch(addConfigModalVisible, (newValue) => {
if (!newValue) {
isAdding.value = false; //
}
});
</script>
<style scoped>

View File

@ -9,10 +9,12 @@ import {
BookOutlined,
BookFilled,
GithubOutlined,
DatabaseOutlined,
DatabaseFilled,
FolderOutlined,
FolderFilled,
GoldOutlined,
GoldFilled,
ToolFilled,
ToolOutlined,
BugOutlined,
ProjectFilled,
ProjectOutlined,
@ -33,6 +35,10 @@ const layoutSettings = reactive({
useTopBar: false, // 使
})
// Add state for GitHub stars
const githubStars = ref(0)
const isLoadingStars = ref(false)
const getRemoteConfig = () => {
configStore.refreshConfig()
}
@ -44,9 +50,24 @@ const getRemoteDatabase = () => {
databaseStore.refreshDatabase()
}
// Fetch GitHub stars count
const fetchGithubStars = async () => {
try {
isLoadingStars.value = true
const response = await fetch('https://api.github.com/repos/xerrors/Yuxi-Know')
const data = await response.json()
githubStars.value = data.stargazers_count
} catch (error) {
console.error('Error fetching GitHub stars:', error)
} finally {
isLoadingStars.value = false
}
}
onMounted(() => {
getRemoteConfig()
getRemoteDatabase()
fetchGithubStars() // Fetch GitHub stars on mount
})
// 使 vue3 setup composition API
@ -91,7 +112,7 @@ console.log(route)
<span class="text">对话</span>
</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/database') ? DatabaseFilled : DatabaseOutlined" />
<component class="icon" :is="route.path.startsWith('/database') ? FolderFilled : FolderOutlined" />
<span class="text">知识</span>
</RouterLink>
<RouterLink to="/graph" class="nav-item" active-class="active">
@ -99,7 +120,7 @@ console.log(route)
<span class="text">图谱</span>
</RouterLink>
<RouterLink to="/tools" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/tools') ? StarFilled: StarOutlined" />
<component class="icon" :is="route.path.startsWith('/tools') ? ToolFilled: ToolOutlined" />
<span class="text">工具</span>
</RouterLink>
<a-tooltip placement="right">
@ -112,8 +133,11 @@ console.log(route)
</div>
<div class="fill" style="flex-grow: 1;"></div>
<div class="github nav-item">
<a href="https://github.com/xerrors/ProjectAthena" target="_blank">
<GithubOutlined class="icon" style="color: #222;"/>
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank" class="github-link">
<GithubOutlined class="icon" style="color: #222;"/>
<span v-if="githubStars > 0" class="github-stars">
<span class="star-count">{{ githubStars }}</span>
</span>
</a>
</div>
<RouterLink class="nav-item setting" to="/setting" active-class="active">
@ -229,6 +253,30 @@ div.header, #app-router-view {
background-color: transparent;
border: 1px solid transparent;
}
.github-link {
display: flex;
flex-direction: column;
align-items: center;
color: inherit;
}
.github-stars {
display: flex;
align-items: center;
font-size: 12px;
margin-top: 4px;
.star-icon {
color: #f0a742;
font-size: 12px;
margin-right: 2px;
}
.star-count {
font-weight: 600;
}
}
}
&.setting {
@ -387,7 +435,6 @@ div.header, #app-router-view {
font-size: 15px;
}
&.github, &.setting {
padding: 8px 12px;
@ -404,11 +451,21 @@ div.header, #app-router-view {
&.github {
a {
display: flex;
justify-content: center;
align-items: center;
}
.github-stars {
display: flex;
align-items: center;
margin-left: 6px;
.star-icon {
color: #f0a742;
font-size: 14px;
margin-right: 2px;
}
}
}
}
}
</style>
</style>

View File

@ -7,7 +7,7 @@ const router = createRouter({
routes: [
{
path: '/',
name: 'home',
name: 'main',
component: BlankLayout,
children: [ {
path: '',
@ -24,7 +24,7 @@ const router = createRouter({
children: [
{
path: '',
name: 'Chat',
name: 'ChatComp',
component: () => import('../views/ChatView.vue'),
meta: { keepAlive: true }
}
@ -37,7 +37,7 @@ const router = createRouter({
children: [
{
path: '',
name: 'Graph',
name: 'GraphComp',
component: () => import('../views/GraphView.vue'),
meta: { keepAlive: false }
}
@ -50,13 +50,13 @@ const router = createRouter({
children: [
{
path: '',
name: 'Database',
name: 'DatabaseComp',
component: () => import('../views/DataBaseView.vue'),
meta: { keepAlive: true }
},
{
path: ':database_id',
name: 'databaseInfo',
name: 'DatabaseInfoComp',
component: () => import('../views/DataBaseInfoView.vue'),
meta: { keepAlive: false }
}
@ -69,7 +69,7 @@ const router = createRouter({
children: [
{
path: '',
name: 'Setting',
name: 'SettingComp',
component: () => import('../views/SettingView.vue'),
meta: { keepAlive: true }
}
@ -82,7 +82,7 @@ const router = createRouter({
children: [
{
path: '',
name: 'ToolsView',
name: 'ToolsComp',
component: () => import('../views/ToolsView.vue'),
meta: { keepAlive: true }
},

View File

@ -41,9 +41,16 @@ const convs = reactive(JSON.parse(localStorage.getItem('chat-convs')) || [
])
const state = reactive({
isSidebarOpen: true,
isSidebarOpen: JSON.parse(localStorage.getItem('chat-sidebar-open') || 'true'),
})
// Watch isSidebarOpen and save to localStorage
watch(
() => state.isSidebarOpen,
(newValue) => {
localStorage.setItem('chat-sidebar-open', JSON.stringify(newValue))
}
)
const curConvId = ref(0)
const generateRandomHash = (length) => {

View File

@ -3,7 +3,7 @@
<a-empty>
<template #description>
<span>
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link>
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link>
</span>
</template>
</a-empty>
@ -11,7 +11,7 @@
<div class="graph-container layout-container" v-else>
<HeaderComponent
title="图数据库"
:description="`${graphInfo?.database_name || ''} - 共 ${graphInfo?.entity_count || 0} 实体,${graphInfo?.relationship_count || 0} 个关系`"
:description="`${graphInfo?.database_name || ''} - 共 ${graphInfo?.entity_count || 0} 实体,${graphInfo?.relationship_count || 0} 个关系。向量模型:${graphInfo?.embed_model_name || '未上传文件'}`"
>
<template #actions>
<div class="status-wrapper">
@ -43,7 +43,7 @@
</div>
</div>
<div class="main" id="container" ref="container" v-show="graphData.nodes.length > 0"></div>
<a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/>
<a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/>
<a-modal
:open="state.showModal" title="上传文件"
@ -51,6 +51,11 @@
@cancel="() => state.showModal = false"
ok-text="添加到图数据库" cancel-text="取消"
:confirm-loading="state.precessing">
<div v-if="graphInfo?.embed_model_name">
<p>当前图数据库向量模型{{ graphInfo?.embed_model_name }}</p>
<p>当前所选择的向量模型是 {{ configStore.config.embed_model }}</p>
</div>
<p v-else>第一次创建之后将无法修改向量模型当前向量模型 {{ configStore.config.embed_model }}</p>
<div class="upload">
<a-upload-dragger
class="upload-dragger"
@ -58,7 +63,7 @@
name="file"
:fileList="fileList"
:max-count="1"
:disabled="state.precessing"
:disabled="state.precessing || (graphInfo?.embed_model_name && graphInfo?.embed_model_name !== configStore.config.embed_model)"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
@ -173,6 +178,8 @@ const loadSampleNodes = () => {
.then((res) => {
if (res.ok) {
return res.json();
} else if (!configStore.config.enable_knowledge_graph) {
throw new Error('请前往设置页面配置启用知识图谱')
} else {
throw new Error("加载失败");
}
@ -181,7 +188,7 @@ const loadSampleNodes = () => {
graphData.nodes = data.result.nodes
graphData.edges = data.result.edges
console.log(graphData)
randerGraph()
setTimeout(() => randerGraph(), 500)
})
.catch((error) => {
message.error(error.message);
@ -195,12 +202,6 @@ const onSearch = () => {
return
}
const cur_embed_model = configStore.config.embed_model
if (cur_embed_model !== 'zhipu-embedding-3') {
message.error('当前不支持实体检索,请在设置中选择向量模型为 zhipu-embedding-3')
return
}
state.searchLoading = true
fetch(`/api/data/graph/node?entity_name=${state.searchInput}`)
.then((res) => {

View File

@ -5,7 +5,7 @@
<p>大模型驱动的知识库管理工具</p>
<button class="home-btn" @click="goToChat">开始对话</button>
<img src="/home.png" alt="Placeholder Image" />
<footer>© 江南语析 2024 [WIP] v0.12.138</footer>
<footer>© 江南语析 2025 [WIP] v0.12.138</footer>
</div>
</template>

View File

@ -2,7 +2,7 @@
<div class="">
<HeaderComponent title="设置" class="setting-header">
<template #description>
<p>配置文件也可以在 <code>saves/config/config.yaml</code> 中修改</p>
<p>配置文件也可以在 <code>src/static/config.yaml</code> 中修改</p>
</template>
<template #actions>
<a-button type="primary" v-if="isNeedRestart" @click="sendRestart">
@ -24,7 +24,7 @@
<div class="section">
<div class="card">
<span class="label">{{ items?.embed_model.des }}</span>
<a-select style="width: 200px"
<a-select style="width: 300px"
:value="configStore.config?.embed_model"
@change="handleChange('embed_model', $event)"
>
@ -36,7 +36,7 @@
</div>
<div class="card">
<span class="label">{{ items?.reranker.des }}</span>
<a-select style="width: 200px"
<a-select style="width: 300px"
:value="configStore.config?.reranker"
@change="handleChange('reranker', $event)"
:disabled="!configStore.config.enable_reranker"
@ -64,13 +64,13 @@
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
/>
</div>
<!-- <div class="card">
<span class="label">{{ items?.enable_search_engine.des }}</span>
<div class="card">
<span class="label">{{ items?.enable_web_search.des }}</span>
<a-switch
:checked="configStore.config.enable_search_engine"
@change="handleChange('enable_search_engine', !configStore.config.enable_search_engine)"
:checked="configStore.config.enable_web_search"
@change="handleChange('enable_web_search', !configStore.config.enable_web_search)"
/>
</div> -->
</div>
<div class="card">
<span class="label">{{ items?.enable_reranker.des }}</span>
<a-switch
@ -186,6 +186,7 @@
</div>
<div class="setting" v-if="state.section ==='path'">
<h3>本地模型配置</h3>
<p>如果是 Docker 启动务必确保在环境变量中设置了 MODEL_DIR或者设置了 volumes 映射</p>
<TableConfigComponent
:config="configStore.config?.model_local_paths"
@update:config="handleModelLocalPathsUpdate"
@ -270,6 +271,7 @@ const handleChange = (key, e) => {
if (key == 'enable_reranker'
|| key == 'enable_knowledge_graph'
|| key == 'enable_knowledge_base'
|| key == 'enable_web_search'
|| key == 'model_provider'
|| key == 'model_name'
|| key == 'embed_model'

View File

@ -2,7 +2,7 @@
<div class="tools-container layout-container">
<HeaderComponent
title="工具箱"
description="这里展示了各种可用的工具。"
description="这里展示了各种可用的工具。(注:不是大模型的工具)"
>
</HeaderComponent>