commit
56444740b9
29
README.md
29
README.md
@ -10,7 +10,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## 概述
|
||||
|
||||
基于大模型 RAG 知识库与知识图谱的问答平台。Llamaindex + VueJS + Flask + Neo4j。大模型适配 OpenAI、国内主流大模型平台的模型调用、本地 vllm 部署。只需要配置对应服务平台的 `API_KEY` 即可使用。
|
||||
@ -18,11 +17,12 @@
|
||||

|
||||
|
||||
|
||||
代办清单
|
||||
待办清单
|
||||
|
||||
- [ ] Ollma Embedding 支持(Open-like Embedding 支持)
|
||||
- [x] 知识图谱索引支持自定义 Embedding 模型
|
||||
- [x] DeepSeek-R1 支持
|
||||
- [ ] embedding 过程添加进度显示
|
||||
- [X] 知识图谱索引支持自定义 Embedding 模型
|
||||
- [X] DeepSeek-R1 支持
|
||||
|
||||
## 更新日志
|
||||
|
||||
@ -39,7 +39,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
## 快速上手
|
||||
|
||||
在启动之前,提供 API 服务商的 API_KEY,并放置在 `src/.env` 文件中。默认使用的是硅基流动。因此务必需要配置 `SILICONFLOW_API_KEY=<SILICONFLOW_API_KEY>`。其余模型的配置可以参考 [src/static/models.yaml](src/static/models.yaml) 中的 env。
|
||||
@ -103,15 +102,15 @@ docker compose -f docker/docker-compose.yml --env-file src/.env up --build
|
||||
|
||||
模型仅支持通过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` |
|
||||
| `deepseek` | `deepseek-chat` | `DEEPSEEK_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` |
|
||||
| 模型供应商 | 默认模型 | 配置项目 |
|
||||
| :--------------------- | :---------------------------------- | :--------------------------------------------- |
|
||||
| `siliconflow` (默认) | `Qwen/Qwen2.5-7B-Instruct` (免费) | `SILICONFLOW_API_KEY` |
|
||||
| `openai` | `gpt-4o` | `OPENAI_API_KEY` |
|
||||
| `deepseek` | `deepseek-chat` | `DEEPSEEK_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 调用的方法之后,只需要在 [src/static/models.yaml](src/static/models.yaml) 中添加对应的模型配置即可。配置示例如下:
|
||||
|
||||
@ -140,14 +139,12 @@ docker compose -f docker/docker-compose.yml --env-file src/.env up --build
|
||||
> [!Warning]
|
||||
> 需要注意,由于知识库和图数据库的构建都依赖于向量模型,如果中途更改向量模型,会导致知识库不可用。此外,知识图谱的向量索引的建立默认使用 embedding-3 构建,因此检索的时候必须使用 embedding-3(现阶段还不支持修改)
|
||||
|
||||
|
||||
对于**语言模型**,并不支持直接运行本地语言模型,请使用 vllm 或者 ollama 转成 API 服务之后使用。
|
||||
|
||||
对于**向量模型**和**重排序模型**,选择以 `local` 前缀开头的模型,可以不做修改会自动下载模型,如果下载过程中出现问题,请参考 [HF-Mirror](https://hf-mirror.com/) 配置相关内容。(但请注意,如果是 Docker 运行,模型仅会缓存到 Docker 里面)
|
||||
|
||||
如果想要使用本地已经下载好的模型,可以在网页的 settings 里面做映射。或者修改 `saves/config/base.yaml` 来配置映射关系。但请记得,本地模型的路径要在 docker-compose 的文件中映射 volumes。
|
||||
|
||||
|
||||
## 知识库支持
|
||||
|
||||
本项目支持多种格式的知识库,目前支持的格式有:pdf、txt、md。文件上传后,会首先转换为纯文本,然后使用向量模型将文本转换为向量,然后存储到向量数据库中。这个过程可能会花费不少时间。
|
||||
|
||||
@ -6,8 +6,8 @@ WORKDIR /app
|
||||
COPY ./web/package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm install --verbose --force
|
||||
# RUN npm install --registry http://mirrors.cloud.tencent.com/npm/ --verbose --force
|
||||
# RUN npm install --verbose --force
|
||||
RUN npm install --registry http://mirrors.cloud.tencent.com/npm/ --verbose --force
|
||||
|
||||
# 复制源代码
|
||||
COPY ./web .
|
||||
@ -22,8 +22,8 @@ FROM node:latest AS build-stage
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./web/package*.json ./
|
||||
RUN npm install --force
|
||||
# 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
|
||||
|
||||
@ -2,11 +2,9 @@ import os
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from src.utils.logging_config import setup_logger
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
logger = setup_logger("Config")
|
||||
|
||||
with open(Path("src/static/models.yaml"), "r") as f:
|
||||
with open(Path("src/static/models.yaml"), 'r', encoding='utf-8') as f:
|
||||
_models = yaml.safe_load(f)
|
||||
|
||||
MODEL_NAMES = _models["MODEL_NAMES"]
|
||||
@ -53,7 +51,6 @@ class Config(SimpleConfig):
|
||||
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="是否开启网页搜索(需配置 TAVILY_API_KEY)")
|
||||
# 模型配置
|
||||
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
|
||||
@ -90,6 +87,9 @@ class Config(SimpleConfig):
|
||||
|
||||
def handle_self(self):
|
||||
self.model_names = MODEL_NAMES
|
||||
self.embed_model_names = EMBED_MODEL_INFO
|
||||
self.reranker_names = RERANKER_LIST
|
||||
|
||||
model_provider_info = self.model_names.get(self.model_provider, {})
|
||||
self.model_dir = os.environ.get("MODEL_DIR", "")
|
||||
|
||||
|
||||
@ -2,11 +2,9 @@ import os
|
||||
import json
|
||||
import time
|
||||
from src.plugins import pdf2txt
|
||||
from src.utils import hashstr, setup_logger, is_text_pdf
|
||||
from src.utils import hashstr, logger, is_text_pdf
|
||||
from src.models.embedding import get_embedding_model
|
||||
|
||||
logger = setup_logger("DataBaseManager")
|
||||
|
||||
|
||||
class DataBaseManager:
|
||||
|
||||
@ -21,7 +19,6 @@ class DataBaseManager:
|
||||
if self.config.enable_knowledge_graph:
|
||||
from src.core.graphbase import GraphDatabase
|
||||
self.graph_base = GraphDatabase(self.config, self.embed_model)
|
||||
self.graph_base.start()
|
||||
else:
|
||||
self.graph_base = None
|
||||
|
||||
|
||||
@ -9,9 +9,7 @@ import warnings
|
||||
|
||||
from src.plugins import pdf2txt
|
||||
from src.plugins.oneke import OneKE
|
||||
from src.utils import setup_logger
|
||||
|
||||
logger = setup_logger("server-graphbase")
|
||||
from src.utils import logger
|
||||
|
||||
warnings.filterwarnings("ignore", category=UserWarning)
|
||||
|
||||
@ -28,6 +26,16 @@ class GraphDatabase:
|
||||
assert embed_model, "embed_model=None"
|
||||
self.embed_model = embed_model
|
||||
self.embed_model_name = None
|
||||
self.work_dir = os.path.join(config.save_dir, "knowledge_graph", kgdb_name)
|
||||
os.makedirs(self.work_dir, exist_ok=True)
|
||||
|
||||
# 尝试加载已保存的图数据库信息
|
||||
if self.load_graph_info():
|
||||
logger.info(f"已加载图数据库信息:{self.kgdb_name}")
|
||||
else:
|
||||
logger.info(f"未找到已保存的图数据库信息,将创建新的配置")
|
||||
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
||||
@ -38,11 +46,12 @@ class GraphDatabase:
|
||||
self.driver = GD.driver(f"{uri}/{self.kgdb_name}", auth=(username, password))
|
||||
self.status = "open"
|
||||
logger.info(f"Connected to Neo4j at {uri}/{self.kgdb_name}, {self.get_database_info()}")
|
||||
# 连接成功后保存图数据库信息
|
||||
self.save_graph_info()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Neo4j: {e}, {uri}, {self.kgdb_name}, {username}, {password}")
|
||||
self.config.enable_knowledge_graph = False
|
||||
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
self.driver.close()
|
||||
@ -119,33 +128,39 @@ class GraphDatabase:
|
||||
with self.driver.session() as session:
|
||||
session.execute_write(create, triples)
|
||||
|
||||
def pdf_file_add_entity(self, file_path, output_path, kgdb_name='neo4j'):
|
||||
self.use_database(kgdb_name) # 切换到指定数据库
|
||||
text_path = pdf2txt(file_path)
|
||||
global UIE_MODEL
|
||||
if UIE_MODEL is None:
|
||||
UIE_MODEL = OneKE()
|
||||
triples_path = UIE_MODEL.processing_text_to_kg(text_path, output_path)
|
||||
self.jsonl_file_add_entity(triples_path)
|
||||
return kgdb_name
|
||||
# def pdf_file_add_entity(self, file_path, output_path, kgdb_name='neo4j'):
|
||||
# self.use_database(kgdb_name) # 切换到指定数据库
|
||||
# text_path = pdf2txt(file_path)
|
||||
# global UIE_MODEL
|
||||
# if UIE_MODEL is None:
|
||||
# UIE_MODEL = OneKE()
|
||||
# triples_path = UIE_MODEL.processing_text_to_kg(text_path, output_path)
|
||||
# self.jsonl_file_add_entity(triples_path)
|
||||
# return kgdb_name
|
||||
|
||||
def txt_add_vector_entity(self, triples, kgdb_name='neo4j'):
|
||||
"""添加实体三元组"""
|
||||
self.use_database(kgdb_name)
|
||||
def _index_exists(tx, index_name):
|
||||
"""检查索引是否存在"""
|
||||
result = tx.run("SHOW INDEXES")
|
||||
for record in result:
|
||||
if record["name"] == index_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _create_graph(tx, data):
|
||||
"""添加一个三元组"""
|
||||
for entry in data:
|
||||
tx.run("""
|
||||
MERGE (h:Entity {name: $h})
|
||||
MERGE (t:Entity {name: $t})
|
||||
MERGE (h)-[r:RELATION {type: $r}]->(t)
|
||||
""", h=entry['h'], t=entry['t'], r=entry['r'])
|
||||
|
||||
def _create_vector_index(tx, dim):
|
||||
"""创建向量索引"""
|
||||
# NOTE 这里是否是会重复构建索引?
|
||||
index_name = "entityEmbeddings"
|
||||
if not _index_exists(tx, index_name):
|
||||
tx.run(f"""
|
||||
@ -157,24 +172,34 @@ class GraphDatabase:
|
||||
}} }};
|
||||
""")
|
||||
|
||||
# 判断模型名称是否匹配
|
||||
from src.config import EMBED_MODEL_INFO
|
||||
embed_info = EMBED_MODEL_INFO[self.config.embed_model]
|
||||
with self.driver.session() as session:
|
||||
session.execute_write(_create_graph, triples)
|
||||
session.execute_write(_create_vector_index, embed_info.get('dimension'))
|
||||
for i, entry in enumerate(triples):
|
||||
logger.info(f"Adding entity {i+1}/{len(triples)}")
|
||||
embedding_h = self.get_embedding(entry['h'])
|
||||
session.execute_write(self.set_embedding, entry['h'], embedding_h)
|
||||
cur_embed_info = EMBED_MODEL_INFO[self.config.embed_model]
|
||||
self.embed_model_name = self.embed_model_name or cur_embed_info.get('name')
|
||||
assert self.embed_model_name == cur_embed_info.get('name') or self.embed_model_name is None, \
|
||||
f"embed_model_name={self.embed_model_name}, {cur_embed_info.get('name')=}"
|
||||
|
||||
with self.driver.session() as session:
|
||||
logger.info(f"Adding entity to {kgdb_name}")
|
||||
session.execute_write(_create_graph, triples)
|
||||
logger.info(f"Creating vector index for {kgdb_name} with {self.config.embed_model}")
|
||||
session.execute_write(_create_vector_index, cur_embed_info['dimension'])
|
||||
# NOTE 这里需要异步处理
|
||||
for i, entry in enumerate(triples):
|
||||
logger.debug(f"Adding entity {i+1}/{len(triples)}")
|
||||
embedding_h = self.get_embedding(entry['h'])
|
||||
embedding_t = self.get_embedding(entry['t'])
|
||||
session.execute_write(self.set_embedding, entry['h'], embedding_h)
|
||||
session.execute_write(self.set_embedding, entry['t'], embedding_t)
|
||||
|
||||
# 数据添加完成后保存图信息
|
||||
self.save_graph_info()
|
||||
|
||||
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) # 切换到指定数据库
|
||||
logger.info(f"Start adding entity to {kgdb_name} with {file_path}")
|
||||
|
||||
def read_triples(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
@ -186,6 +211,8 @@ class GraphDatabase:
|
||||
self.txt_add_vector_entity(triples, kgdb_name)
|
||||
|
||||
self.status = "open"
|
||||
# 更新并保存图数据库信息
|
||||
self.save_graph_info()
|
||||
return kgdb_name
|
||||
|
||||
def delete_entity(self, entity_name=None, kgdb_name="neo4j"):
|
||||
@ -213,27 +240,14 @@ class GraphDatabase:
|
||||
|
||||
def query_node(self, entity_name, hops=2, **kwargs):
|
||||
# TODO 添加判断节点数量为 0 停止检索
|
||||
|
||||
logger.debug(f"Query graph node {entity_name} with {hops=}")
|
||||
if kwargs.get("exact_match"):
|
||||
raise NotImplemented("not implement for `exact_match`")
|
||||
else:
|
||||
return self.query_by_vector(entity_name=entity_name, **kwargs)
|
||||
|
||||
def query_by_vector(self, entity_name, threshold=0.9, kgdb_name='neo4j', hops=2, num_of_res=5):
|
||||
results = self.query_by_vector_tep(entity_name=entity_name)
|
||||
|
||||
# 筛选出分数高于阈值的实体
|
||||
qualified_entities = [result[0] for result in results[:num_of_res] if result[1] > threshold]
|
||||
|
||||
# 对每个合格的实体进行查询
|
||||
all_query_results = []
|
||||
for entity in qualified_entities:
|
||||
query_result = self.query_specific_entity(entity_name=entity, hops=hops, kgdb_name=kgdb_name)
|
||||
all_query_results.extend(query_result)
|
||||
|
||||
return all_query_results
|
||||
|
||||
def query_by_vector_tep(self, entity_name, kgdb_name='neo4j'):
|
||||
"""向量查询"""
|
||||
self.use_database(kgdb_name)
|
||||
def query(tx, text):
|
||||
embedding = self.get_embedding(text)
|
||||
@ -245,15 +259,28 @@ class GraphDatabase:
|
||||
return result.values()
|
||||
|
||||
with self.driver.session() as session:
|
||||
return session.execute_read(query, entity_name)
|
||||
results = session.execute_read(query, entity_name)
|
||||
|
||||
|
||||
# 筛选出分数高于阈值的实体
|
||||
qualified_entities = [result[0] for result in results[:num_of_res] if result[1] > threshold]
|
||||
logger.debug(f"Graph Query Entities: {entity_name}, {qualified_entities=}")
|
||||
|
||||
# 对每个合格的实体进行查询
|
||||
all_query_results = []
|
||||
for entity in qualified_entities:
|
||||
query_result = self.query_specific_entity(entity_name=entity, hops=hops, kgdb_name=kgdb_name)
|
||||
all_query_results.extend(query_result)
|
||||
|
||||
return all_query_results
|
||||
|
||||
def query_specific_entity(self, entity_name, kgdb_name='neo4j', hops=2):
|
||||
"""查询指定实体三元组信息"""
|
||||
"""查询指定实体三元组信息(无向关系)"""
|
||||
self.use_database(kgdb_name)
|
||||
def query(tx, entity_name, hops):
|
||||
result = tx.run(f"""
|
||||
MATCH (n {{name: $entity_name}})-[r*1..{hops}]->(m)
|
||||
RETURN n.name AS node_name, r, m.name AS neighbor_name
|
||||
MATCH (n {{name: $entity_name}})-[r*1..{hops}]-(m)
|
||||
RETURN n, r, m
|
||||
""", entity_name=entity_name)
|
||||
return result.values()
|
||||
|
||||
@ -316,11 +343,9 @@ class GraphDatabase:
|
||||
return session.execute_read(query, node_name, hops)
|
||||
|
||||
def get_embedding(self, text):
|
||||
inputs = [text]
|
||||
with torch.no_grad():
|
||||
outputs = self.embed_model.encode(inputs)
|
||||
embeddings = outputs[0] # 假设取平均作为文本的嵌入向量
|
||||
return embeddings
|
||||
outputs = self.embed_model.encode([text])[0]
|
||||
return outputs
|
||||
|
||||
def set_embedding(self, tx, entity_name, embedding):
|
||||
tx.run("""
|
||||
@ -328,160 +353,71 @@ class GraphDatabase:
|
||||
CALL db.create.setNodeVectorProperty(e, 'embedding', $embedding)
|
||||
""", name=entity_name, embedding=embedding)
|
||||
|
||||
# def format_query_results(self, results):
|
||||
# formatted_results = []
|
||||
# for row in results:
|
||||
# n, rs, m = row
|
||||
# entity_a = n['name']
|
||||
# entity_b = m['name']
|
||||
# for rel in rs:
|
||||
# relationship = rel.type
|
||||
# formatted_results.append(f"实体 {entity_a} 和 实体 {entity_b} 的关系是 {relationship}")
|
||||
# return formatted_results
|
||||
def save_graph_info(self):
|
||||
"""
|
||||
将图数据库的基本信息保存到工作目录中的JSON文件
|
||||
保存的信息包括:数据库名称、状态、嵌入模型名称等
|
||||
"""
|
||||
try:
|
||||
# 获取数据库信息
|
||||
db_info = None
|
||||
if self.status == "open" and self.driver:
|
||||
try:
|
||||
db_info = self.get_database_info(self.kgdb_name)
|
||||
except Exception as e:
|
||||
logger.warning(f"无法获取数据库信息:{e}")
|
||||
|
||||
# 构建要保存的信息字典
|
||||
graph_info = {
|
||||
"kgdb_name": self.kgdb_name,
|
||||
"status": self.status,
|
||||
"embed_model_name": self.embed_model_name,
|
||||
"last_updated": None, # 这里可以添加时间戳
|
||||
"database_info": db_info
|
||||
}
|
||||
|
||||
# 添加时间戳
|
||||
from datetime import datetime
|
||||
graph_info["last_updated"] = datetime.now().isoformat()
|
||||
|
||||
# 保存到JSON文件
|
||||
info_file_path = os.path.join(self.work_dir, "graph_info.json")
|
||||
with open(info_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(graph_info, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"图数据库信息已保存到:{info_file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存图数据库信息失败:{e}")
|
||||
return False
|
||||
|
||||
def load_graph_info(self):
|
||||
"""
|
||||
从工作目录中的JSON文件加载图数据库的基本信息
|
||||
返回True表示加载成功,False表示加载失败
|
||||
"""
|
||||
try:
|
||||
info_file_path = os.path.join(self.work_dir, "graph_info.json")
|
||||
if not os.path.exists(info_file_path):
|
||||
logger.warning(f"图数据库信息文件不存在:{info_file_path}")
|
||||
return False
|
||||
|
||||
with open(info_file_path, 'r', encoding='utf-8') as f:
|
||||
graph_info = json.load(f)
|
||||
|
||||
# 更新对象属性
|
||||
if graph_info.get("embed_model_name"):
|
||||
self.embed_model_name = graph_info["embed_model_name"]
|
||||
|
||||
# 如果需要,可以加载更多信息
|
||||
# 注意:这里不更新self.kgdb_name,因为它是在初始化时设置的
|
||||
|
||||
logger.info(f"已加载图数据库信息,最后更新时间:{graph_info.get('last_updated')}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"加载图数据库信息失败:{e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = None
|
||||
|
||||
kgdb_name = "neo4j"
|
||||
|
||||
class EmbeddingModel(FlagModel):
|
||||
def __init__(self, config, **kwargs):
|
||||
|
||||
model_name_or_path = "/data2024/yyyl/model/BAAI/bge-large-zh-v1.5/"
|
||||
|
||||
super().__init__(model_name_or_path, use_fp16=False, **kwargs)
|
||||
|
||||
|
||||
model = EmbeddingModel(config)
|
||||
# 初始化知识图谱数据库
|
||||
kgdb = GraphDatabase(config, model)
|
||||
# 创建新的数据库
|
||||
# kgdb.create_graph_database("db2")
|
||||
|
||||
# 返回指定数据库信息
|
||||
# info = kgdb.get_database_info(kgdb_name)
|
||||
# print(info)
|
||||
|
||||
# triples = [
|
||||
# {
|
||||
# "h": "CCC",
|
||||
# "t": "EE",
|
||||
# "r": "同学"
|
||||
# },
|
||||
# {
|
||||
# "h": "EE",
|
||||
# "t": "RR",
|
||||
# "r": "同事"
|
||||
# }
|
||||
# ]
|
||||
|
||||
# kgdb.query_by_vector("z")
|
||||
# def format_query_results(results):
|
||||
# formatted_results = {"nodes": [], "edges": []}
|
||||
|
||||
# # 用于存储所有唯一的节点信息
|
||||
# node_dict = {}
|
||||
|
||||
# for item in results:
|
||||
# # 确保item[1]是一个非空的列表
|
||||
# if isinstance(item[1], list) and len(item[1]) > 0:
|
||||
# relationship = item[1][0]
|
||||
# rel_id = relationship.element_id
|
||||
# nodes = relationship.nodes
|
||||
# if len(nodes) == 2:
|
||||
# node1, node2 = nodes
|
||||
|
||||
# # 提取源节点和目标节点信息
|
||||
# node1_id = node1.element_id
|
||||
# node2_id = node2.element_id
|
||||
# node1_name = item[0] # 假设节点名称和列表中的第一个元素相同
|
||||
# node2_name = item[2] if len(item) > 2 else 'unknown'
|
||||
|
||||
# # 记录节点信息
|
||||
# if node1_id not in node_dict:
|
||||
# node_dict[node1_id] = {"id": node1_id, "name": node1_name}
|
||||
# if node2_id not in node_dict:
|
||||
# node_dict[node2_id] = {"id": node2_id, "name": node2_name}
|
||||
|
||||
# # 确定关系类型
|
||||
|
||||
# relationship_type = relationship._properties.get('type', 'unknown')
|
||||
# if relationship_type == 'unknown':
|
||||
# relationship_type = relationship.type
|
||||
|
||||
# # 记录边的信息
|
||||
# formatted_results["edges"].append({
|
||||
# "id": rel_id,
|
||||
# "type": relationship_type,
|
||||
# "source_id": node1_id,
|
||||
# "target_id": node2_id,
|
||||
# "source_name": node1_name,
|
||||
# "target_name": node2_name
|
||||
# })
|
||||
|
||||
# # 将唯一的节点信息添加到结果中
|
||||
# formatted_results["nodes"] = list(node_dict.values())
|
||||
|
||||
# return formatted_results
|
||||
|
||||
# entities = ['jqy', '维c']
|
||||
# results = []
|
||||
# for entitie in entities:
|
||||
# result = kgdb.query_by_vector(entitie)
|
||||
# if result != []:
|
||||
# results.extend(result)
|
||||
# print(format_query_results(results))
|
||||
|
||||
|
||||
# kgdb.txt_add_vector_entity(triples, model)
|
||||
# print("Extend the Graph data base")
|
||||
|
||||
# kgdb.jsonl_file_add_entity("/data2024/yyyl/ProjectAthena/tep.jsonl", kgdb_name)
|
||||
# print("Extend the Graph data base")
|
||||
|
||||
# triples_path = "output.jsonl"
|
||||
# def read_triples(file_path):
|
||||
# with open(file_path, 'r', encoding='utf-8') as file:
|
||||
# for line in file:
|
||||
# item = json.loads(line.strip())
|
||||
# yield [item]
|
||||
# for trio in read_triples(triples_path):
|
||||
# kgdb.txt_add_entity(trio)
|
||||
|
||||
# 通过文件添加三元组数据
|
||||
# kgdb.file_add_entity("/data2024/yyyl/ProjectAthena/test.pdf", "/data2024/yyyl/ProjectAthena/output.jsonl", kgdb_name)
|
||||
# print("Extend the Graph data base")
|
||||
|
||||
# 删除数据库信息
|
||||
# kgdb.delete_entity()
|
||||
# print("Clear the Graph data base")
|
||||
|
||||
# 查询所有节点和关系
|
||||
# results = kgdb.query_all_nodes_and_relationships(kgdb_name)
|
||||
# print(results)
|
||||
|
||||
# 查询特定实体及其关系
|
||||
# results = kgdb.query_specific_entity("kllll", kgdb_name)
|
||||
# print(results)
|
||||
|
||||
# 查询特定实体及其关系
|
||||
# results = kgdb.query_by_vector_tep("z", model, tokenizer)
|
||||
# print(results)
|
||||
|
||||
# 查询特定关系类型的所有节点
|
||||
# results = kgdb.query_by_relationship_type("作用或食用效果", kgdb_name)
|
||||
# print(results)
|
||||
|
||||
# 模糊查询
|
||||
# results = kgdb.query_entity_like("三七", kgdb_name)
|
||||
# print(results)
|
||||
|
||||
# 查询节点信息
|
||||
# results = kgdb.query_entity_like("三七提取物", kgdb_name)
|
||||
# print(results)
|
||||
|
||||
# 关闭数据库连接
|
||||
kgdb.close()
|
||||
|
||||
pass
|
||||
@ -1,8 +1,7 @@
|
||||
import os
|
||||
|
||||
from pymilvus import MilvusClient, MilvusException
|
||||
from src.utils import setup_logger, hashstr
|
||||
logger = setup_logger("KnowledgeBase")
|
||||
from src.utils import logger, hashstr
|
||||
|
||||
|
||||
class KnowledgeBase:
|
||||
@ -68,7 +67,7 @@ class KnowledgeBase:
|
||||
logger.error(f"Collection {collection_name} not found, create it")
|
||||
# self.add_collection(collection_name)
|
||||
|
||||
vectors = self.embed_model.encode(docs)
|
||||
vectors = self.embed_model.batch_encode(docs)
|
||||
|
||||
data = [{
|
||||
"id": int(random.random() * 1e12),
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
from src.models.rerank_model import get_reranker
|
||||
from src.utils.logging_config import setup_logger
|
||||
|
||||
logger = setup_logger("server-common")
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
|
||||
class Retriever:
|
||||
@ -19,7 +17,6 @@ class Retriever:
|
||||
self.web_searcher = WebSearcher()
|
||||
|
||||
def retrieval(self, query, history, meta):
|
||||
|
||||
refs = {"query": query, "history": history, "meta": meta}
|
||||
refs["model_name"] = self.config.model_name
|
||||
refs["entities"] = self.reco_entities(query, history, refs)
|
||||
@ -66,8 +63,8 @@ class Retriever:
|
||||
|
||||
def query_classification(self, query):
|
||||
"""判断是否需要查询
|
||||
- 对于完全基于用户给定信息的任务,称之为“足够”“sufficient”,不需要检索;
|
||||
- 否则,称之为“不足”“insufficient”,可能需要检索,
|
||||
- 对于完全基于用户给定信息的任务,称之为"足够""sufficient",不需要检索;
|
||||
- 否则,称之为"不足""insufficient",可能需要检索,
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -107,12 +104,14 @@ class Retriever:
|
||||
distance_threshold = meta.get("distanceThreshold", 0)
|
||||
top_k = meta.get("topK", 5)
|
||||
|
||||
# 检索
|
||||
all_kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=max_query_count)
|
||||
for r in all_kb_res:
|
||||
r["file"] = kb.id2file(r["entity"]["file_id"])
|
||||
|
||||
kb_res = [r for r in all_kb_res if r["distance"] > distance_threshold]
|
||||
|
||||
# 重排序
|
||||
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)
|
||||
@ -176,7 +175,7 @@ class Retriever:
|
||||
|
||||
return entities
|
||||
|
||||
def _extract_relationship_info(self, relationship, source_name, target_name):
|
||||
def _extract_relationship_info(self, relationship, source_name=None, target_name=None, node_dict=None):
|
||||
"""
|
||||
提取关系信息并返回格式化的节点和边信息
|
||||
"""
|
||||
@ -189,6 +188,9 @@ class Retriever:
|
||||
source_id = source.element_id
|
||||
target_id = target.element_id
|
||||
|
||||
source_name = node_dict[source_id]["name"] if source_name is None else source_name
|
||||
target_name = node_dict[target_id]["name"] if target_name is None else target_name
|
||||
|
||||
relationship_type = relationship._properties.get("type", "unknown")
|
||||
if relationship_type == "unknown":
|
||||
relationship_type = relationship.type
|
||||
@ -230,24 +232,33 @@ class Retriever:
|
||||
return formatted_results
|
||||
|
||||
def format_query_results(self, results):
|
||||
logger.debug(f"Graph Query Results: {results}")
|
||||
formatted_results = {"nodes": [], "edges": []}
|
||||
node_dict = {}
|
||||
|
||||
for item in results:
|
||||
if not isinstance(item[1], list) or not item[1]:
|
||||
# 检查数据格式
|
||||
if len(item) < 2 or not isinstance(item[1], list):
|
||||
continue
|
||||
|
||||
relationship = item[1][0]
|
||||
source_name = item[0]
|
||||
target_name = item[2] if len(item) > 2 else "unknown"
|
||||
node_dict[item[0].element_id] = dict(id=item[0].element_id, name=item[0]._properties.get("name", "Unknown"))
|
||||
node_dict[item[2].element_id] = dict(id=item[2].element_id, name=item[2]._properties.get("name", "Unknown"))
|
||||
|
||||
node_info, edge_info = self._extract_relationship_info(relationship, source_name, target_name)
|
||||
if node_info is None or edge_info is None:
|
||||
continue
|
||||
# 处理关系列表中的每个关系
|
||||
for i, relationship in enumerate(item[1]):
|
||||
try:
|
||||
# 提取关系信息
|
||||
node_info, edge_info = self._extract_relationship_info(relationship, node_dict=node_dict)
|
||||
if node_info is None or edge_info is None:
|
||||
continue
|
||||
|
||||
node_dict.update({node["id"]: node for node in node_info})
|
||||
formatted_results["edges"].append(edge_info)
|
||||
# 添加边
|
||||
formatted_results["edges"].append(edge_info)
|
||||
except Exception as e:
|
||||
logger.error(f"处理关系时出错: {e}, 关系: {relationship}")
|
||||
continue
|
||||
|
||||
# 将节点字典转换为列表
|
||||
formatted_results["nodes"] = list(node_dict.values())
|
||||
|
||||
return formatted_results
|
||||
|
||||
@ -3,9 +3,7 @@ from src.core import DataBaseManager
|
||||
from src.core.retriever import Retriever
|
||||
from src.models import select_model
|
||||
from src.config import Config
|
||||
from src.utils import setup_logger
|
||||
|
||||
logger = setup_logger("Startup")
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
class Startup:
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv("src/.env")
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from src.routers import router
|
||||
from src.utils.logging_config import setup_logger
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
@ -20,9 +22,6 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
logger = setup_logger("server:main")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=5000, threads=10, workers=10)
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ def select_model(config, model_provider=None, model_name=None):
|
||||
"siliconflow",
|
||||
"zhipu",
|
||||
"lingyiwanwu",
|
||||
"together.ai",
|
||||
]:
|
||||
return OpenAIBase(
|
||||
api_key=os.getenv(model_info["env"][0]),
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import os
|
||||
from openai import OpenAI
|
||||
from src.utils.logging_config import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
from src.utils import logger
|
||||
|
||||
class OpenAIBase():
|
||||
def __init__(self, api_key, base_url, model_name):
|
||||
|
||||
@ -4,12 +4,9 @@ import requests
|
||||
from FlagEmbedding import FlagModel
|
||||
|
||||
from src.config import EMBED_MODEL_INFO
|
||||
from src.utils.logging_config import setup_logger
|
||||
from src.utils import hashstr
|
||||
from src.utils import hashstr, logger
|
||||
|
||||
|
||||
logger = setup_logger("EmbeddingModel")
|
||||
|
||||
class LocalEmbeddingModel(FlagModel):
|
||||
def __init__(self, config, **kwargs):
|
||||
info = EMBED_MODEL_INFO[config.embed_model]
|
||||
@ -31,6 +28,7 @@ class RemoteEmbeddingModel:
|
||||
embed_state = {}
|
||||
|
||||
def batch_encode(self, messages, batch_size=20):
|
||||
logger.info(f"Batch encoding {len(messages)} messages")
|
||||
data = []
|
||||
|
||||
if len(messages) > batch_size:
|
||||
|
||||
@ -4,9 +4,7 @@ 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")
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
class OllamaEmbedding(RemoteEmbeddingModel):
|
||||
"""
|
||||
|
||||
@ -5,10 +5,7 @@ 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")
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
|
||||
class LocalReranker(FlagReranker):
|
||||
|
||||
@ -11,8 +11,7 @@ from transformers import (
|
||||
BitsAndBytesConfig
|
||||
)
|
||||
|
||||
from src.utils import setup_logger
|
||||
logger = setup_logger("OneKE")
|
||||
from src.utils import logger
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
@ -5,11 +5,11 @@ base = APIRouter()
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import Request, Body
|
||||
from src.core import HistoryManager
|
||||
from src.utils.logging_config import setup_logger
|
||||
from src.core.startup import startup
|
||||
|
||||
logger = setup_logger("server-base")
|
||||
from src.core import HistoryManager
|
||||
from src.core.startup import startup
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
@base.get("/")
|
||||
async def route_index():
|
||||
@ -39,6 +39,6 @@ def get_log():
|
||||
last_lines = deque(f, maxlen=1000)
|
||||
|
||||
log = ''.join(last_lines)
|
||||
return {"log": log}
|
||||
return {"log": log, "message": "success", "log_file": LOG_FILE}
|
||||
|
||||
|
||||
|
||||
@ -5,14 +5,12 @@ from fastapi.responses import StreamingResponse, Response
|
||||
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.logging_config import logger
|
||||
|
||||
chat = APIRouter(prefix="/chat")
|
||||
logger = setup_logger("server-chat")
|
||||
# 创建线程池
|
||||
executor = ThreadPoolExecutor()
|
||||
|
||||
refs_pool = {}
|
||||
|
||||
@chat.get("/")
|
||||
async def chat_get():
|
||||
@ -26,6 +24,7 @@ def chat_post(
|
||||
cur_res_id: str = Body(...)):
|
||||
|
||||
history_manager = HistoryManager(history)
|
||||
logger.debug(f"Received query: {query} with meta: {meta}")
|
||||
|
||||
def make_chunk(content=None, **kwargs):
|
||||
return json.dumps({
|
||||
@ -112,10 +111,4 @@ async def call(query: str = Body(...), meta: dict = Body(None)):
|
||||
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
|
||||
refs = refs_pool.pop(cur_res_id, None)
|
||||
return {"refs": refs}
|
||||
return {"response": response.content}
|
||||
@ -2,12 +2,11 @@ import os
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends, Body
|
||||
|
||||
from src.utils import setup_logger, hashstr
|
||||
from src.utils import logger, hashstr
|
||||
from src.core.startup import startup
|
||||
|
||||
data = APIRouter(prefix="/data")
|
||||
|
||||
logger = setup_logger("server-database")
|
||||
|
||||
@data.get("/")
|
||||
def get_databases():
|
||||
@ -100,7 +99,6 @@ async def get_graph_info():
|
||||
|
||||
@data.get("/graph/node")
|
||||
async def get_graph_node(entity_name: str):
|
||||
logger.debug(f"Get graph node {entity_name}")
|
||||
result = startup.dbm.graph_base.query_node(entity_name=entity_name)
|
||||
return {"result": startup.retriever.format_query_results(result), "message": "success"}
|
||||
|
||||
@ -113,7 +111,7 @@ async def get_graph_nodes(kgdb_name: str, num: int):
|
||||
result = startup.dbm.graph_base.get_sample_nodes(kgdb_name, num)
|
||||
return {"result": startup.retriever.format_general_results(result), "message": "success"}
|
||||
|
||||
@data.post("/graph/add")
|
||||
@data.post("/graph/add-by-jsonl")
|
||||
async def add_graph_entity(file_path: str = Body(...), kgdb_name: Optional[str] = Body(None)):
|
||||
if not startup.config.enable_knowledge_graph:
|
||||
raise HTTPException(status_code=400, detail="Knowledge graph is not enabled")
|
||||
|
||||
@ -3,11 +3,10 @@ from fastapi import APIRouter, Body
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from src.utils import setup_logger
|
||||
from src.utils import logger
|
||||
|
||||
tool = APIRouter(prefix="/tool")
|
||||
|
||||
logger = setup_logger("server-tools")
|
||||
|
||||
class Tool(BaseModel):
|
||||
name: str
|
||||
|
||||
@ -50,6 +50,18 @@ MODEL_NAMES:
|
||||
- deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
||||
- Qwen/Qwen2.5-72B-Instruct
|
||||
- Qwen/Qwen2.5-7B-Instruct
|
||||
together.ai:
|
||||
name: Together.ai
|
||||
url: https://api.together.ai/models
|
||||
base_url: https://api.together.xyz/v1/
|
||||
default: meta-llama/Llama-3.3-70B-Instruct-Turbo-Free
|
||||
env:
|
||||
- TOGETHER_API_KEY
|
||||
models:
|
||||
- meta-llama/Llama-3.3-70B-Instruct-Turbo
|
||||
- meta-llama/Llama-3.3-70B-Instruct-Turbo-Free
|
||||
- deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free
|
||||
|
||||
qianfan:
|
||||
name: 百度千帆 (QianFan)
|
||||
url: https://open.bigmodel.cn/dev/api
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import time
|
||||
import random
|
||||
from src.utils.logging_config import setup_logger, logger
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
def is_text_pdf(pdf_path):
|
||||
import fitz
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import os
|
||||
from typing import List, Dict
|
||||
from tavily import TavilyClient
|
||||
from src.utils.logging_config import setup_logger
|
||||
|
||||
logger = setup_logger("web-search")
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
class WebSearcher:
|
||||
def __init__(self):
|
||||
|
||||
5
test/data/A_Dream_of_Red_Mansions_tiny.jsonl
Normal file
5
test/data/A_Dream_of_Red_Mansions_tiny.jsonl
Normal file
@ -0,0 +1,5 @@
|
||||
{"h": "贾代善", "t": "贾源", "r": "子"}
|
||||
{"h": "娄氏", "t": "贾源", "r": "重孙媳妇"}
|
||||
{"h": "贾母", "t": "贾代善", "r": "妻"}
|
||||
{"h": "老姨奶奶", "t": "贾代善", "r": "妾"}
|
||||
{"h": "贾敏", "t": "贾代善", "r": "女"}
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="chat" ref="chatContainer">
|
||||
<div class="header">
|
||||
<div class="chat-header">
|
||||
<div class="header__left">
|
||||
<div
|
||||
v-if="!state.isSidebarOpen"
|
||||
@ -154,7 +154,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted, toRefs, nextTick, computed, watch } from 'vue'
|
||||
import { reactive, ref, onMounted, toRefs, nextTick, onUnmounted, watch } from 'vue'
|
||||
import {
|
||||
SendOutlined,
|
||||
MenuOutlined,
|
||||
@ -197,7 +197,11 @@ const configStore = useConfigStore()
|
||||
|
||||
const { conv, state } = toRefs(props)
|
||||
const chatContainer = ref(null)
|
||||
|
||||
const isStreaming = ref(false)
|
||||
const userIsScrolling = ref(false);
|
||||
const shouldAutoScroll = ref(true);
|
||||
|
||||
const panel = ref(null)
|
||||
const modelCard = ref(null)
|
||||
const examples = ref([
|
||||
@ -295,10 +299,23 @@ const renameTitle = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserScroll = () => {
|
||||
// 计算我们是否接近底部(100像素以内)
|
||||
const isNearBottom = chatContainer.value.scrollHeight - chatContainer.value.scrollTop - chatContainer.value.clientHeight < 100;
|
||||
|
||||
// 如果用户不在底部,则仅将其标记为用户滚动
|
||||
userIsScrolling.value = !isNearBottom;
|
||||
|
||||
// 如果用户再次滚动到底部,请恢复自动滚动
|
||||
shouldAutoScroll.value = isNearBottom;
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight
|
||||
}, 10)
|
||||
if (shouldAutoScroll.value) {
|
||||
setTimeout(() => {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
const generateRandomHash = (length) => {
|
||||
@ -502,22 +519,6 @@ const fetchChatResponse = (user_input, cur_res_id) => {
|
||||
});
|
||||
}
|
||||
|
||||
const fetchRefs = (cur_res_id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`/api/chat/refs?cur_res_id=${cur_res_id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
resolve(data.refs)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 更新后的 sendMessage 函数
|
||||
const sendMessage = () => {
|
||||
@ -531,6 +532,8 @@ const sendMessage = () => {
|
||||
isStreaming.value = true;
|
||||
appendUserMessage(user_input);
|
||||
appendAiMessage("", null);
|
||||
forceScrollToBottom();
|
||||
|
||||
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id;
|
||||
conv.value.inputText = '';
|
||||
meta.db_name = dbName;
|
||||
@ -553,15 +556,15 @@ const retryMessage = (id) => {
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
const autoSend = (msg) => {
|
||||
conv.value.inputText = msg
|
||||
sendMessage()
|
||||
}
|
||||
|
||||
// 从本地存储加载数据
|
||||
onMounted(() => {
|
||||
scrollToBottom()
|
||||
loadDatabases()
|
||||
|
||||
chatContainer.value.addEventListener('scroll', handleUserScroll);
|
||||
|
||||
// 从本地存储加载数据
|
||||
const storedMeta = localStorage.getItem('meta');
|
||||
if (storedMeta) {
|
||||
const parsedMeta = JSON.parse(storedMeta);
|
||||
@ -569,6 +572,20 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.removeEventListener('scroll', handleUserScroll);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加新函数来处理特定的滚动行为
|
||||
const forceScrollToBottom = () => {
|
||||
shouldAutoScroll.value = true;
|
||||
setTimeout(() => {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight;
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 监听 meta 对象的变化,并保存到本地存储
|
||||
watch(
|
||||
() => meta,
|
||||
@ -593,7 +610,7 @@ watch(
|
||||
flex: 5 5 200px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.header {
|
||||
.chat-header {
|
||||
user-select: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@ -728,7 +745,7 @@ watch(
|
||||
|
||||
.chat-box {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
flex-grow: 1;
|
||||
padding: 1rem 2rem;
|
||||
@ -819,7 +836,7 @@ watch(
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 900px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 2px solid var(--gray-200);
|
||||
@ -1008,14 +1025,14 @@ watch(
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.chat-container .chat .header {
|
||||
.chat-container .chat .chat-header {
|
||||
background: var(--main-light-4);
|
||||
.header__left, .header__right {
|
||||
gap: 20px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.3rem;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
@ -1057,19 +1074,29 @@ watch(
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.message-box pre {
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--main-light-3);
|
||||
padding: 1rem;
|
||||
|
||||
&:has(code.hljs) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-md {
|
||||
color: var(--gray-900);
|
||||
max-width: 100%;
|
||||
|
||||
pre {
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--main-light-3);
|
||||
padding: 1rem;
|
||||
|
||||
&:has(code.hljs) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@ -1083,6 +1110,15 @@ watch(
|
||||
ol, ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--main-800);
|
||||
margin: auto 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -337,8 +337,12 @@ div.header, #app-router-view {
|
||||
div.header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
bottom: 10rem;
|
||||
}
|
||||
|
||||
}
|
||||
.app-layout div.header-mobile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -348,6 +352,7 @@ div.header, #app-router-view {
|
||||
align-items: center;
|
||||
flex: 0 0 60px;
|
||||
border-right: none;
|
||||
height: 40px;
|
||||
|
||||
.nav-item {
|
||||
text-decoration: none;
|
||||
|
||||
@ -256,10 +256,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.conversation {
|
||||
.conversations {
|
||||
position: absolute;
|
||||
z-index: 101;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
border-radius: 0 16px 16px 0;
|
||||
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@ -59,7 +59,9 @@
|
||||
<template v-if="column.key === 'filename'">
|
||||
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text }}</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'"><span :class="text">{{ text.toUpperCase() }}</span></template>
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<span :class="['span-type', text]">{{ text.toUpperCase() }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'done'">
|
||||
<CheckCircleFilled style="color: #41A317;"/>
|
||||
</template>
|
||||
@ -813,7 +815,7 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pdf, .txt, .md {
|
||||
.span-type {
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
@ -821,6 +823,7 @@ onMounted(() => {
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
user-select: none;
|
||||
background: #005F77;
|
||||
}
|
||||
|
||||
.pdf {
|
||||
@ -831,6 +834,16 @@ onMounted(() => {
|
||||
background: #068033;
|
||||
}
|
||||
|
||||
.docx, .doc {
|
||||
background: #2C59B7;
|
||||
}
|
||||
|
||||
.md {
|
||||
background: #020817;
|
||||
}
|
||||
|
||||
|
||||
|
||||
button.main-btn {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
|
||||
@ -50,12 +50,13 @@
|
||||
@ok="addDocumentByFile"
|
||||
@cancel="() => state.showModal = false"
|
||||
ok-text="添加到图数据库" cancel-text="取消"
|
||||
:ok-button-props="{ disabled: disabled }"
|
||||
:confirm-loading="state.precessing">
|
||||
<div v-if="graphInfo?.embed_model_name">
|
||||
<p>当前图数据库向量模型:{{ graphInfo?.embed_model_name }}</p>
|
||||
<p>当前所选择的向量模型是 {{ configStore.config.embed_model }}</p>
|
||||
<p>当前所选择的向量模型是 {{ cur_embed_model }}</p>
|
||||
</div>
|
||||
<p v-else>第一次创建之后将无法修改向量模型,当前向量模型 {{ configStore.config.embed_model }}</p>
|
||||
<p v-else>第一次创建之后将无法修改向量模型,当前向量模型 {{ cur_embed_model }}</p>
|
||||
<div class="upload">
|
||||
<a-upload-dragger
|
||||
class="upload-dragger"
|
||||
@ -63,7 +64,7 @@
|
||||
name="file"
|
||||
:fileList="fileList"
|
||||
:max-count="1"
|
||||
:disabled="state.precessing || (graphInfo?.embed_model_name && graphInfo?.embed_model_name !== configStore.config.embed_model)"
|
||||
:disabled="disabled"
|
||||
action="/api/data/upload"
|
||||
@change="handleFileUpload"
|
||||
@drop="handleDrop"
|
||||
@ -87,6 +88,12 @@ import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const cur_embed_model = computed(() => configStore.config.embed_model_names[configStore.config.embed_model].name)
|
||||
const disabled = computed(() => {
|
||||
if (state.precessing) return true
|
||||
if (graphInfo?.value?.embed_model_name !== cur_embed_model.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
let graphInstance
|
||||
const graphInfo = ref(null)
|
||||
@ -152,7 +159,7 @@ const getGraphData = () => {
|
||||
const addDocumentByFile = () => {
|
||||
state.precessing = true
|
||||
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
|
||||
fetch('/api/data/graph/add', {
|
||||
fetch('/api/data/graph/add-by-jsonl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json" // 添加 Content-Type 头
|
||||
@ -178,7 +185,7 @@ const loadSampleNodes = () => {
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else if (!configStore.config.enable_knowledge_graph) {
|
||||
} else if (configStore?.config && !configStore?.config.enable_knowledge_graph) {
|
||||
throw new Error('请前往设置页面配置启用知识图谱')
|
||||
} else {
|
||||
throw new Error("加载失败");
|
||||
|
||||
@ -14,15 +14,15 @@
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
<div class="setting-container layout-container">
|
||||
<div class="sider">
|
||||
<div class="sider" v-if="state.windowWidth > 520">
|
||||
<a-button type="text" :class="{ activesec: state.section === 'base'}" @click="state.section='base'" :icon="h(SettingOutlined)"> 基本设置 </a-button>
|
||||
<a-button type="text" :class="{ activesec: state.section === 'model'}" @click="state.section='model'" :icon="h(CodeOutlined)"> 模型配置 </a-button>
|
||||
<a-button type="text" :class="{ activesec: state.section === 'path'}" @click="state.section='path'" :icon="h(FolderOutlined)"> 路径配置 </a-button>
|
||||
</div>
|
||||
<div class="setting" v-if="state.section === 'base'">
|
||||
<div class="setting" v-if="state.windowWidth <= 520 || state.section === 'base'">
|
||||
<h3>基础模型配置</h3>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<div class="card card-select">
|
||||
<span class="label">{{ items?.embed_model.des }}</span>
|
||||
<a-select style="width: 300px"
|
||||
:value="configStore.config?.embed_model"
|
||||
@ -34,7 +34,7 @@
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card card-select">
|
||||
<span class="label">{{ items?.reranker.des }}</span>
|
||||
<a-select style="width: 300px"
|
||||
:value="configStore.config?.reranker"
|
||||
@ -81,7 +81,7 @@
|
||||
</div>
|
||||
<h3>检索配置</h3>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<div class="card card-select">
|
||||
<span class="label">{{ items?.use_rewrite_query.des }}</span>
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.use_rewrite_query"
|
||||
@ -95,7 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting" v-if="state.section === 'model'">
|
||||
<div class="setting" v-if="state.windowWidth <= 520 || state.section === 'model'">
|
||||
<h3>模型配置</h3>
|
||||
<p>请在 <code>src/.env</code> 文件中配置对应的 APIKEY</p>
|
||||
<div class="model-provider-card">
|
||||
@ -184,7 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting" v-if="state.section ==='path'">
|
||||
<div class="setting" v-if="state.windowWidth <= 520 || state.section ==='path'">
|
||||
<h3>本地模型配置</h3>
|
||||
<p>如果是 Docker 启动,务必确保在环境变量中设置了 MODEL_DIR,或者设置了 volumes 映射</p>
|
||||
<TableConfigComponent
|
||||
@ -198,7 +198,7 @@
|
||||
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, reactive, ref, h, watch } from 'vue'
|
||||
import { computed, reactive, ref, h, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
@ -231,16 +231,17 @@ const customModel = reactive({
|
||||
})
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
section: 'base'
|
||||
section: 'base',
|
||||
windowWidth: window?.innerWidth || 0
|
||||
})
|
||||
|
||||
// 筛选 modelStatus 中为真的key
|
||||
const modelKeys = computed(() => {
|
||||
return Object.keys(modelStatus.value).filter(key => modelStatus.value[key])
|
||||
return Object.keys(modelStatus.value || {}).filter(key => modelStatus.value?.[key])
|
||||
})
|
||||
|
||||
const notModelKeys = computed(() => {
|
||||
return Object.keys(modelStatus.value).filter(key => !modelStatus.value[key])
|
||||
return Object.keys(modelStatus.value || {}).filter(key => !modelStatus.value?.[key])
|
||||
})
|
||||
|
||||
const generateRandomHash = (length) => {
|
||||
@ -360,6 +361,19 @@ const handleCancelCustomModel = () => {
|
||||
customModel.visible = false
|
||||
}
|
||||
|
||||
const updateWindowWidth = () => {
|
||||
state.windowWidth = window?.innerWidth || 0
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateWindowWidth()
|
||||
window.addEventListener('resize', updateWindowWidth)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateWindowWidth)
|
||||
})
|
||||
|
||||
const sendRestart = () => {
|
||||
console.log('Restarting...')
|
||||
message.loading({ content: '重新加载模型中', key: "restart", duration: 0 });
|
||||
@ -618,7 +632,18 @@ const sendRestart = () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.setting-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card.card-select {
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: env.VITE_API_URL || 'http://localhost:5000',
|
||||
target: env.VITE_API_URL || 'http://localhost:5050',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user