Merge branch 'main' into dev
This commit is contained in:
commit
c4ea78f0e5
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
||||
2
LICENSE
2
LICENSE
@ -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
119
README.md
@ -1,39 +1,57 @@
|
||||
<h1 align="center">语析 (基于大模型的知识图谱问答平台)</h1>
|
||||
<h1 align="center">语析 (基于大模型的知识库+知识图谱问答平台)</h1>
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> 当前项目还处于开发的早期,还存在一些 BUG,有问题随时提 issue。
|
||||
|
||||
|
||||
## 概述
|
||||
|
||||
基于大模型 RAG 知识库与知识图谱的问答平台。Llamaindex + VueJS + Flask + Neo4j。大模型适配 OpenAI、国内主流大模型平台的模型调用、本地 vllm 部署。只需要配置对应服务平台的 `API_KEY` 即可使用。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
> [!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) 的独立部署。
|
||||
|
||||

|
||||
|
||||
|
||||
## 快速上手
|
||||
|
||||
在启动之前,提供 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 运行本地模型时。
|
||||
|
||||

|
||||
|
||||
### 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) 的独立部署。
|
||||
|
||||
## 相关问题
|
||||
|
||||
### 镜像下载问题
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
BIN
images/custom_models.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
BIN
images/reasoning.png
Normal file
BIN
images/reasoning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 891 KiB |
@ -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
|
||||
|
||||
@ -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
10
scripts/run_docker.sh
Normal 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 "$@"
|
||||
@ -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
|
||||
@ -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}")
|
||||
|
||||
@ -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: {}
|
||||
@ -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))
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
70
src/models/rerank_model.py
Normal file
70
src/models/rerank_model.py
Normal 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)
|
||||
|
||||
@ -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
0
src/static/config.yaml
Normal 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
|
||||
@ -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
121
test/test_concurrency.py
Normal 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)
|
||||
@ -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" /> -->
|
||||
<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>
|
||||
</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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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 }
|
||||
},
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="tools-container layout-container">
|
||||
<HeaderComponent
|
||||
title="工具箱"
|
||||
description="这里展示了各种可用的工具。"
|
||||
description="这里展示了各种可用的工具。(注:不是大模型的工具)"
|
||||
>
|
||||
</HeaderComponent>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user