Merge pull request #10 from xerrors/dev

Dev
This commit is contained in:
Wenjie Zhang 2024-09-09 17:08:15 +08:00 committed by GitHub
commit ad2e6e9f92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 553 additions and 245 deletions

3
.gitignore vendored
View File

@ -40,4 +40,5 @@ local_neo4j/data
local_neo4j/logs
local_neo4j/import
local_neo4j/plugins
local_neo4j/conf
local_neo4j/conf
graphrag

View File

@ -1,15 +1,17 @@
from neo4j import GraphDatabase
from neo4j.exceptions import ServiceUnavailable, AuthError
# sudo ln -s /snap/core22/1586/usr/sbin/iptables /usr/sbin/iptables
def check_neo4j_status(uri="bolt://localhost:7687", username="neo4j", password="0123456789"):
"""
检查 Neo4j 数据库是否可以连接并正常工作
参数:
uri (str): Neo4j URI默认为 "bolt://localhost:7687"
username (str): 数据库用户名默认为 "neo4j"
password (str): 数据库密码默认为 "0123456789"
返回:
str: "OK" 表示连接成功"UNAVAILABLE" 表示服务不可用"AUTH_FAILED" 表示认证失败
"""

View File

@ -49,7 +49,7 @@ class Config(SimpleConfig):
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
self.add_item("model_provider", default="zhipu", des="模型提供商", choices=["qianfan", "vllm", "zhipu", "deepseek", "dashscope"])
self.add_item("model_provider", default="zhipu", des="模型提供商", choices=["openai", "qianfan", "vllm", "zhipu", "deepseek", "dashscope"])
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=["bge-reranker-v2-m3"])
@ -145,6 +145,13 @@ class Config(SimpleConfig):
MODEL_NAMES = {
# https://platform.deepseek.com/api-docs/zh-cn/pricing
"openai": [
"gpt-4",
"gpt-4o",
"gpt-4-0125-preview",
"gpt-4o-mini",
],
"deepseek": [
"deepseek-chat",
"deepseek-coder"

View File

@ -15,6 +15,71 @@ logger = setup_logger("server-graphbase")
warnings.filterwarnings("ignore", category=UserWarning)
"""
from neo4j import GraphDatabase
import random
class KnowledgeGraph:
def __init__(self, uri, user, password):
self._driver = GraphDatabase.driver(uri, auth=(user, password))
def close(self):
self._driver.close()
def use_database(self, kgdb_name):
with self._driver.session() as session:
session.run(f"USE {kgdb_name}")
def get_sample_nodes(self, kgdb_name='neo4j', num=50):
self.use_database(kgdb_name)
selected_nodes = set()
nodes_to_expand = set()
result_nodes = []
with self._driver.session() as session:
while len(selected_nodes) < num:
# 如果需要扩展的节点为空,随机选择一个新节点
if not nodes_to_expand:
result = session.run("MATCH (n) RETURN n, rand() as r ORDER BY r LIMIT 1")
for record in result:
nodes_to_expand.add(record['n'].id)
result_nodes.append({'n': record['n'], 'r': None, 'm': None})
# 从需要扩展的节点中随机选择一个节点
current_node_id = random.choice(list(nodes_to_expand))
nodes_to_expand.remove(current_node_id)
# 获取当前节点的邻居节点最多5个
result = session.run(
f"MATCH (n)-[r]-(m) WHERE id(n) = {current_node_id} RETURN n, r, m LIMIT 5"
)
for record in result:
neighbor_node_id = record['m'].id
if neighbor_node_id not in selected_nodes:
selected_nodes.add(neighbor_node_id)
nodes_to_expand.add(neighbor_node_id)
result_nodes.append({'n': record['n'], 'r': record['r'], 'm': record['m']})
# 如果已经达到最大值,停止扩展
if len(selected_nodes) >= num:
break
return result_nodes[:num]
# 示例用法
uri = "bolt://localhost:7687"
user = "neo4j"
password = "password"
kg = KnowledgeGraph(uri, user, password)
sample_nodes = kg.get_sample_nodes(num=50)
for node in sample_nodes:
print(f"Node: {node['n'].id}, Relationship: {node['r']}, Neighbor: {node['m'].id}")
kg.close()
"""
UIE_MODEL = None
class GraphDatabase:
@ -38,7 +103,7 @@ class GraphDatabase:
self.driver.close()
def get_sample_nodes(self, kgdb_name='neo4j', num=50):
"""获取指定数据库的 num 个节点信息"""
"""获取指定数据库的 num 个节点信息"""
self.use_database(kgdb_name)
def query(tx, num):
result = tx.run("MATCH (n)-[r]->(m) RETURN n, r, m LIMIT $num", num=int(num))

View File

@ -38,5 +38,6 @@ class HistoryManager():
def __str__(self):
history_str = ""
for message in self.messages:
history_str += f"{message['role']}: {message['content']}"
msg = message["content"].replace('\n', ' ')
history_str += f"\n{message['role']}: {msg}"
return history_str

View File

@ -63,7 +63,7 @@ class KnowledgeBase:
"id": int(random.random() * 1e12),
"vector": vectors[i],
"text": docs[i],
"hash": hashstr(docs[i] + str(random.random())),
"hash": hashstr(docs[i], with_salt=True),
**kwargs} for i in range(len(vectors))]
res = self.client.insert(collection_name=collection_name, data=data)

View File

@ -1,5 +1,6 @@
from src.models.embedding import Reranker
from src.utils.logging_config import setup_logger
logger = setup_logger("server-common")
@ -37,12 +38,14 @@ class Retriever:
# 解析图数据库的结果
db_res = refs.get("graph_base").get("results", [])
if len(db_res["nodes"]) > 0:
db_text = '\n'.join([f"{edge['source_name']}{edge['target_name']}的关系是{edge['type']}" for edge in db_res['edges']])
db_text = "\n".join(
[f"{edge['source_name']}{edge['target_name']}的关系是{edge['type']}" for edge in db_res["edges"]]
)
external += f"图数据库信息: \n\n{db_text}"
# 构造查询
if len(external) > 0:
query = f"以下是参考资料:\n\n\n{external}\n\n\n请根据前面的知识回答{query}"
query = f"参考资料:\n\n\n{external}\n\n\n请根据前面的知识回答问题。\n\n问题{query}\n\n回答:"
return query
@ -70,42 +73,53 @@ class Retriever:
kb_res = []
final_res = []
if not refs["meta"].get("db_name") or not self.config.enable_knowledge_base:
return {"results": final_res, "all_results": kb_res, "rw_query": query, "message": "Knowledge base is disabled"}
return {
"results": final_res,
"all_results": kb_res,
"rw_query": query,
"message": "Knowledge base is disabled",
}
rw_query = self.rewrite_query(query, history, refs)
db_name = refs["meta"]["db_name"]
kb = self.dbm.metaname2db[db_name]
limit = refs["meta"].get("queryCount", 10)
kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=limit)
for r in kb_res:
max_query_count = refs["meta"].get("maxQueryCount", 10)
rerank_threshold = refs["meta"].get("rerankThreshold", 0.1)
distance_threshold = refs["meta"].get("distanceThreshold", 0)
top_k = refs["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"])
# use distance threshold to filter results
kb_res = [r for r in all_kb_res if r["distance"] > distance_threshold]
if self.config.enable_reranker:
RERANK_THRESHOLD = 0.001
for r in kb_res:
r["rerank_score"] = self.reranker.compute_score([query, r["entity"]["text"]], normalize=True)
r["rerank_score"] = self.reranker.compute_score([rw_query, r["entity"]["text"]], normalize=True)
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
final_res = [_res for _res in kb_res if _res["rerank_score"] > RERANK_THRESHOLD]
kb_res = [_res for _res in kb_res if _res["rerank_score"] > rerank_threshold]
else:
final_res = kb_res[:5]
kb_res = kb_res[:top_k]
return {"results": final_res, "all_results": kb_res, "rw_query": rw_query}
return {"results": kb_res, "all_results": all_kb_res, "rw_query": rw_query}
def rewrite_query(self, query, history, refs):
"""重写查询"""
rewrite_query_span = refs["meta"].get("rewrite_query", None)
if rewrite_query_span is None or rewrite_query_span == "OFF":
rewrite_query_span = refs["meta"].get("rewriteQuery", "off")
if rewrite_query_span == "off":
rewritten_query = query
else:
from src.utils.prompts import rewritten_query_prompt_template
history_query = [entry['content'] for entry in history if entry['role'] == 'user'] if history else ""
history_query = [entry["content"] for entry in history if entry["role"] == "user"] if history else ""
rewritten_query_prompt = rewritten_query_prompt_template.format(history=history_query, query=query)
rewritten_query = self.model.predict(rewritten_query_prompt).content
if rewrite_query_span == "HyDE":
if rewrite_query_span == "hyde":
hy_doc = self.model.predict(rewritten_query).content
rewritten_query = f"{rewritten_query} {hy_doc}"
@ -118,54 +132,68 @@ class Retriever:
entities = []
if refs["meta"].get("use_graph"):
from src.utils.prompts import entity_extraction_prompt_template
entity_extraction_prompt = entity_extraction_prompt_template.format(text=query)
entities = self.model.predict(entity_extraction_prompt).content.split(",")
entities = [entity for entity in entities if all(char.isalnum() or char in '汉字' for char in entity)]
entities = [entity for entity in entities if all(char.isalnum() or char in "汉字" for char in entity)]
return entities
def foramt_general_results(self, results):
logger.debug(f"Formatting general results: {results}")
def _extract_relationship_info(self, relationship, source_name, target_name):
"""
提取关系信息并返回格式化的节点和边信息
"""
rel_id = relationship.element_id
nodes = relationship.nodes
if len(nodes) != 2:
return None, None
source, target = nodes
source_id = source.element_id
target_id = target.element_id
relationship_type = relationship._properties.get("type", "unknown")
if relationship_type == "unknown":
relationship_type = relationship.type
edge_info = {
"id": rel_id,
"type": relationship_type,
"source_id": source_id,
"target_id": target_id,
"source_name": source_name,
"target_name": target_name,
}
node_info = [
{"id": source_id, "name": source_name},
{"id": target_id, "name": target_name},
]
return node_info, edge_info
def format_general_results(self, results):
formatted_results = {"nodes": [], "edges": []}
for item in results:
relationship = item[1]
rel_id = relationship.element_id
nodes = relationship.nodes
if len(nodes) != 2:
source_name = item[0]._properties.get("name", "unknown")
target_name = item[2]._properties.get("name", "unknown") if len(item) > 2 else "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
source, target = nodes
for node in node_info:
if node["id"] not in [n["id"] for n in formatted_results["nodes"]]:
formatted_results["nodes"].append(node)
source_id = source.element_id
target_id = target.element_id
source_name = source._properties.get('name', 'unknown')
target_name = target._properties.get('name', 'unknown')
if source_id not in formatted_results["nodes"]:
formatted_results["nodes"].append({"id": source_id, "name": source_name})
if target_id not in formatted_results["nodes"]:
formatted_results["nodes"].append({"id": target_id, "name": target_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": source_id,
"target_id": target_id,
"source_name": source_name,
"target_name": target_name
})
formatted_results["edges"].append(edge_info)
return formatted_results
def format_query_results(self, results):
logger.debug(f"Formatting query results: {results}")
formatted_results = {"nodes": [], "edges": []}
node_dict = {}
for item in results:
@ -173,35 +201,18 @@ class Retriever:
continue
relationship = item[1][0]
rel_id = relationship.element_id
nodes = relationship.nodes
if len(nodes) != 2:
source_name = item[0]
target_name = item[2] if len(item) > 2 else "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
source, target = nodes
for node in node_info:
if node["id"] not in node_dict:
node_dict[node["id"]] = node
source_id = source.element_id
target_id = target.element_id
source_name = item[0]
target_name = item[2] if len(item) > 2 else 'unknown'
if source_id not in node_dict:
node_dict[source_id] = {"id": source_id, "name": source_name}
if target_id not in node_dict:
node_dict[target_id] = {"id": target_id, "name": target_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": source_id,
"target_id": target_id,
"source_name": source_name,
"target_name": target_name
})
formatted_results["edges"].append(edge_info)
formatted_results["nodes"] = list(node_dict.values())
@ -210,4 +221,4 @@ class Retriever:
def __call__(self, query, history, meta):
refs = self.retrieval(query, history, meta)
query = self.construct_query(query, refs, meta)
return query, refs
return query, refs

View File

@ -38,7 +38,7 @@ python -m vllm.entrypoints.openai.api_server \
|`bge-large-zh-v1.5`|`BAAI/bge-large-zh-v1.5`|`bge-large-zh-v1.5`*修改为本地路径)|
|`zhipu`|`embedding-2`|`ZHIPUAPI` (`.env`)|
### 3. 重排序模型支持
例如(`config/base.yaml`
@ -52,3 +52,7 @@ model_name: null # for default
model_local_paths:
bge-large-zh-v1.5: /models/bge-large-zh-v1.5
```
### 3. 重排序模型支持
目前仅支持 `BAAI/bge-reranker-v2-m3`

View File

@ -28,6 +28,10 @@ def select_model(config):
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 is None:
raise ValueError("Model provider not specified, please modify `model_provider` in `src/config/base.yaml`")
else:

View File

@ -39,6 +39,14 @@ class OpenAIBase():
return response.choices[0].message
class OpenModel(OpenAIBase):
def __init__(self, model_name=None):
model_name = model_name or "gpt-4o-mini"
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_API_BASE")
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"

View File

@ -1,12 +1,16 @@
import os
import uuid
from FlagEmbedding import FlagModel, FlagReranker
from src.config import EMBED_MODEL_INFO, RERANKER_LIST
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):
@ -47,9 +51,21 @@ class ZhipuEmbedding:
def predict(self, message):
data = []
if len(message) > 10:
global GLOBAL_EMBED_STATE
task_id = hashstr(message)
logger.info(f"Creating new state for process {task_id}")
GLOBAL_EMBED_STATE[task_id] = {
'status': 'in-progress',
'total': len(message),
'progress': 0
}
for i in range(0, len(message), 10):
if len(message) > 10:
logger.info(f"Encoding {i} to {i+10} with {len(message)} messages")
GLOBAL_EMBED_STATE[task_id]['progress'] = i
group_msg = message[i:i+10]
response = self.client.embeddings.create(
model=self.model_info.default_path,
@ -58,6 +74,10 @@ class ZhipuEmbedding:
data.extend([a.embedding for a in response.data])
if len(message) > 10:
GLOBAL_EMBED_STATE[task_id]['progress'] = len(message)
GLOBAL_EMBED_STATE[task_id]['status'] = 'completed'
return data
def encode(self, message):

View File

@ -16,7 +16,7 @@ logger = setup_logger("OneKE")
dotenv.load_dotenv()
MODEL_NAME_OR_PATH = os.path.join(os.getenv('MODEL_ROOT_DIR'), 'OneKE')
MODEL_NAME_OR_PATH = os.path.join(os.getenv('MODEL_ROOT_DIR', './'), 'OneKE')
instruction_mapper = {
'NERzh': "你是专门进行实体抽取的专家。请从input中抽取出符合schema定义的实体不存在的实体类型返回空列表。请按照JSON字符串的格式回答。",

View File

@ -1,25 +1,53 @@
import os
import uuid
import fitz # fitz就是pip install PyMuPDF
import cv2
from copy import deepcopy
from tqdm import tqdm
from argparse import ArgumentParser
from src.utils import logger
GOLBAL_STATE = {}
def pdf2txt(pdf_path, return_text=False):
if not os.path.exists(pdf_path):
raise FileNotFoundError(f"File not found: {pdf_path}")
# Importing these modules here to avoid unnecessary imports in other files
from paddleocr import PPStructure, save_structure_res
from paddleocr.ppstructure.recovery.recovery_to_doc import sorted_layout_boxes, convert_info_docx
output_dir = os.path.join('tmp', 'pdf2txt', os.path.basename(pdf_path).split('.')[0])
filename = os.path.basename(pdf_path).split('.')[0]
output_dir = os.path.join('saves', 'data', 'pdf2txt', filename)
os.makedirs(output_dir, exist_ok=True)
table_engine = PPStructure(recovery=True, lang='ch')
imgs = convert_imgs(pdf_path, output_dir)
respath = os.path.join(output_dir, 'res.txt')
respath = os.path.join(output_dir, f'{filename}.txt')
global GOLBAL_STATE
task_id = str(uuid.uuid4())
if task_id in GOLBAL_STATE:
logger.info(f"Reusing previous state for process {task_id}")
return GOLBAL_STATE[task_id]
else:
logger.info(f"Creating new state for process {task_id}")
GOLBAL_STATE[task_id] = {
'pdf_path': pdf_path,
'return_text': return_text,
'output_dir': output_dir,
'status': 'in-progress',
'total': len(imgs),
'progress': 0
}
text = []
for img_name in tqdm(imgs, desc='to txt', ncols=100):
img = cv2.imread(img_name)
result = table_engine(img)
GOLBAL_STATE[task_id]['progress'] += 1
save_structure_res(result, output_dir, "structure_result")
@ -43,6 +71,9 @@ def pdf2txt(pdf_path, return_text=False):
whole_text = ''.join(text)
with open(respath, 'w', encoding='utf-8') as f:
f.write(whole_text)
logger.info(f"Extracted text saved to {respath}")
GOLBAL_STATE[task_id]['status'] = 'completed'
if return_text:
return whole_text
@ -72,6 +103,13 @@ def convert_imgs(pdf_path, output_dir):
return imgs
def get_state(task_id):
return GOLBAL_STATE.get(task_id, {})
if __name__ == "__main__":
pdf_path = r'saves/data/uploads/2e04d5_保健食品.pdf'
print(pdf2txt(pdf_path))
parser = ArgumentParser()
parser.add_argument('--pdf-path', type=str, required=True, help='Path to the PDF file')
parser.add_argument('--return-text', action='store_true', help='Return the extracted text')
args = parser.parse_args()
pdf2txt(args.pdf_path, args.return_text)

View File

@ -1,4 +1,5 @@
import time
import random
from src.utils.logging_config import setup_logger, logger
def is_text_pdf(pdf_path):
@ -15,7 +16,7 @@ def hashstr(input_string, length=8, with_salt=False):
import hashlib
# 添加时间戳作为干扰
if with_salt:
input_string += str(time.time())
input_string += str(time.time() + random.random())
hash = hashlib.md5(str(input_string).encode()).hexdigest()
return hash[:length]

View File

@ -5,10 +5,10 @@ from datetime import datetime
DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
# DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
LOG_FILE = f'log/project-{DATETIME}.log'
LOG_FILE = f'saves/log/project-{DATETIME}.log'
def setup_logger(name, level=logging.DEBUG, console=False):
os.makedirs("log", exist_ok=True)
os.makedirs("saves/log", exist_ok=True)
"""Function to setup logger with the given name and log file."""
logger = logging.getLogger(name)

View File

@ -1,3 +1,4 @@
import itertools
import json
from flask import Blueprint, jsonify, request, Response
@ -89,6 +90,7 @@ def restart():
def get_log():
from src.utils.logging_config import LOG_FILE
with open(LOG_FILE, 'r') as f:
log = f.read()
log_lines = itertools.islice(f, 1000)
log = ''.join(log_lines)
return jsonify({"log": log})

View File

@ -135,7 +135,7 @@ def get_graph_nodes():
logger.debug(f"Get graph nodes in {kgdb_name} with {num} nodes")
result = startup.dbm.graph_base.get_sample_nodes(kgdb_name, num)
return jsonify({'result': startup.retriever.foramt_general_results(result), 'message': 'success'}), 200
return jsonify({'result': startup.retriever.format_general_results(result), 'message': 'success'}), 200
@db.route('/graph/add', methods=['POST'])
def add_graph_entity():

BIN
web/public/icon-narrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
web/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -30,10 +30,10 @@
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="meta.selectedKB=index">
<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="meta.selectedKB = null">
<a-menu-item @click="useDatabase(null)">
<a href="javascript:;">不使用</a>
</a-menu-item>
</a-menu>
@ -54,10 +54,10 @@
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="meta.selectedKB=index">
<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="meta.selectedKB = null">
<a-menu-item @click="useDatabase(null)">
<a href="javascript:;">不使用</a>
</a-menu-item>
</a-menu>
@ -71,15 +71,15 @@
<div class="flex-center" @click="meta.use_web = !meta.use_web" v-if="configStore.config.enable_search_engine">
搜索引擎Bing <div @click.stop><a-switch v-model:checked="meta.use_web" /></div>
</div>
<div class="flex-center" @click="meta.rewrite_query = !meta.rewrite_query" v-if="configStore.config.enable_reranker">
重写查询 <div @click.stop><a-switch v-model:checked="meta.rewrite_query" /></div>
</div>
<div class="flex-center" @click="meta.rewrite_query = !meta.rewrite_query">
<div class="flex-center" @click="meta.stream = !meta.stream">
流式输出 <div @click.stop><a-switch v-model:checked="meta.stream" /></div>
</div>
<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" v-if="configStore.config.enable_knowledge_base">
重写查询 <a-segmented v-model:value="meta.rewriteQuery" :options="['off', 'on', 'hyde']"/>
</div>
<div class="flex-center">
最大历史轮数 <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
</div>
@ -162,6 +162,7 @@ import { onClickOutside } from '@vueuse/core'
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import { useConfigStore } from '@/stores/config'
import { message } from 'ant-design-vue'
import RefsComponent from '@/components/RefsComponent.vue'
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
@ -197,7 +198,7 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
use_graph: false,
use_web: false,
graph_name: "neo4j",
rewrite_query: true,
rewriteQuery: "off",
selectedKB: null,
stream: true,
summary_title: true,
@ -229,6 +230,17 @@ const renderMarkdown = (message) => {
}
}
const useDatabase = (index) => {
const selected = opts.databases[index]
console.log(selected)
if (index != null && configStore.config.embed_model != selected.embed_model) {
console.log(selected.embed_model, configStore.config.embed_model)
message.error(`所选知识库的向量模型(${selected.embed_model})与当前向量模型(${configStore.config.embed_model}) 不匹配,请重新选择`)
} else {
meta.selectedKB = index
}
}
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
@ -517,7 +529,7 @@ watch(
border-radius: 12px;
padding: 12px;
z-index: 11;
width: 250px;
width: 280px;
.flex-center {
display: flex;

View File

@ -1,6 +1,6 @@
<template>
<div class="log-viewer">
<a-button @click="fetchLogs">刷新</a-button>
<a-button @click="fetchLogs" :loading="state.fetching">刷新</a-button>
<div ref="logContainer" class="log-container">
<pre v-if="logs">{{ logs }}</pre>
</div>
@ -9,17 +9,21 @@
</template>
<script setup>
import { ref, onMounted, onActivated, nextTick } from 'vue';
import { ref, onMounted, onActivated, nextTick, reactive } from 'vue';
// ref
const logs = ref('');
const error = ref('');
const state = reactive({
fetching: false,
})
// ref DOM
const logContainer = ref(null);
//
const fetchLogs = async () => {
state.fetching = true;
try {
//
error.value = '';
@ -45,41 +49,51 @@
} catch (err) {
//
error.value = `Error: ${err.message}`;
} finally {
state.fetching = false;
}
};
//
onMounted(() => {
fetchLogs();
setInterval(fetchLogs, 5000); // 5
// setInterval(fetchLogs, 5000); // 5
});
// keep-alive
onActivated(() => {
fetchLogs();
setTimeout(fetchLogs, 1000);
});
</script>
<style scoped>
.log-viewer {}
.log-viewer {
}
.error {
color: red;
}
.log-container {
max-height: 80vh; /* 设置最大高度 */
height: calc(100vh - 150px); /* 设置最大高度 */
overflow-y: auto; /* 启用垂直滚动 */
background-color: #f0f0f0;
margin: 20px 0;
padding: 10px;
background-color: #f6f7f7;
padding-bottom: 0;
border-radius: 5px;
margin-top: 10px;
white-space: pre-wrap; /* 使日志内容自动换行 */
word-wrap: break-word;
font-size: small;
background: #0C0C0C;
color: #D1D1D1;
pre {
margin: 0;
padding: 10px;
height: 100%;
}
}
</style>

View File

@ -6,11 +6,11 @@
<span class="filetag item"
v-for="(results, filename) in message.groupedResults"
:key="filename"
@click="openDetail = true"
@click="toggleDrawer(filename)"
>
<FileTextOutlined /> {{ filename }}
<a-drawer
v-model:open="openDetail"
v-model:open="openDetail[filename]"
title="检索详情"
width="800"
:contentWrapperStyle="{ maxWidth: '100%'}"
@ -19,7 +19,7 @@
rootClassName="root"
>
<div class="fileinfo">
<p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
<p><strong>文件名:</strong> {{ filename }}</p>
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
</div>
@ -47,7 +47,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, reactive } from 'vue'
import {
GlobalOutlined,
FileTextOutlined,
@ -59,7 +59,18 @@ const props = defineProps({
const message = ref(props.message)
const openDetail = ref(false)
// 使 reactive
const openDetail = reactive({})
// openDetail
for (const filename in message.value.groupedResults) {
openDetail[filename] = false
}
const toggleDrawer = (filename) => {
openDetail[filename] = !openDetail[filename]
}
const showRefs = computed(() => message.value.role=='received' && message.value.status=='finished')
</script>

View File

@ -54,8 +54,8 @@ console.log(route)
<template>
<div class="app-layout">
<div class="debug-panel">
<div class="shown-btn" @click="showDebug=!showDebug"><BugOutlined /></div>
<div class="debug-panel" @click="showDebug=!showDebug">
<div class="shown-btn"><BugOutlined /></div>
<a-drawer
v-model:open="showDebug"
title="调试面板"

View File

@ -16,22 +16,41 @@
<p class="description">{{ database.description }}</p>
<div class="tags">
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
</div>
<a-divider/>
<div class="query-params" v-if="state.curPage == 'query-test'">
<p style="text-align: center; margin: 0;"><strong>参数配置</strong></p>
<!-- <p style="text-align: center; margin: 0;"><strong>参数配置</strong></p> -->
<div class="params-item">
<p>检索数量</p>
<a-input-number size="small" v-model:value="meta.queryCount" :min="1" :max="20" />
<a-input-number size="small" v-model:value="meta.maxQueryCount" :min="1" :max="20" />
</div>
<div class="params-item">
<p>TopK</p>
<a-input-number size="small" v-model:value="meta.topK" :min="1" :max="meta.maxQueryCount" />
</div>
<div class="params-item">
<p>过滤低质量</p>
<a-switch v-model:checked="meta.filter" />
</div>
<div class="params-item">
<p>重写查询</p>
<a-segmented v-model:value="meta.rewrite_query" :options="['OFF', 'ON', 'HyDE']" />
<div class="params-item w100" v-if="configStore.config.enable_reranker">
<p>重排序阈值</p>
<a-slider v-model:value="meta.rerankThreshold" :min="0" :max="1" :step="0.01" />
</div>
<div class="params-item w100">
<p>距离阈值</p>
<a-slider v-model:value="meta.distanceThreshold" :min="0" :max="1" :step="0.01" />
</div>
<div class="params-item col">
<p>重写查询<small>修改后需重新检索</small></p>
<a-segmented v-model:value="meta.rewriteQuery" :options="rewriteQueryOptions" >
<template #label="{ payload }">
<div style="padding: 4px 4px">
<p>{{ payload.title }}</p>
<small>{{ payload.subTitle }}</small>
</div>
</template>
</a-segmented>
</div>
</div>
</div>
@ -42,7 +61,7 @@
<a-tab-pane key="add">
<template #tab><span><CloudUploadOutlined />添加文件</span></template>
<div class="db-info-container">
<h3>向知识库中添加文件</h3>
<!-- <h3>向知识库中添加文件</h3> -->
<div class="upload">
<a-upload-dragger
class="upload-dragger"
@ -117,7 +136,7 @@
<a-tab-pane key="query-test" force-render>
<template #tab><span><SearchOutlined />检索测试</span></template>
<div class="db-info-container">
<h3>检索测试</h3>
<!-- <h3>检索测试</h3> -->
<div class="query-action">
<a-textarea
v-model:value="queryText"
@ -131,7 +150,11 @@
</a-button>
</div>
<div class="query-test" v-if="queryResult">
<div class="query-card" v-for="(result, idx) in (meta.filter ? queryResult.results : queryResult.all_results)" :key="idx">
<div class="results-overview">
<p>总数{{ queryResult.all_results.length }}过滤后{{ filteredResults.length }} TopK{{ meta.topK }}</p>
<p>重写后查询{{ queryResult.rw_query }}</p>
</div>
<div class="query-card" v-for="(result, idx) in (filteredResults)" :key="idx">
<p>
<strong>#{{ idx + 1 }}&nbsp;&nbsp;&nbsp;</strong>
<span>{{ result.file.filename }}&nbsp;&nbsp;&nbsp;</span>
@ -149,9 +172,10 @@
</template>
<script setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { onMounted, reactive, ref, watch, toRaw } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useRoute, useRouter } from 'vue-router';
import { useConfigStore } from '@/stores/config'
import {
ReadFilled,
LeftOutlined,
@ -177,6 +201,8 @@ const selectedFile = ref(null);
//
const queryText = ref('');
const queryResult = ref(null)
const filteredResults = ref([])
const configStore = useConfigStore()
const state = reactive({
loading: false,
@ -189,11 +215,44 @@ const state = reactive({
});
const meta = reactive({
queryCount: 10,
filter: false,
rewrite_query: 'OFF',
maxQueryCount: 30,
filter: true,
rewriteQuery: 'off',
rerankThreshold: 0.1,
distanceThreshold: 0.5,
topK: 10,
});
const rewriteQueryOptions = ref([
{ value: 'off', payload: { title: 'off', subTitle: '不启用' } },
{ value: 'on', payload: { title: 'on', subTitle: '启用重写' } },
{ value: 'hyde', payload: { title: 'hyde', subTitle: '伪文档生成' } },
])
const filterQueryResults = () => {
if (!queryResult.value || !queryResult.value.all_results) {
return;
}
let results = toRaw(queryResult.value.all_results);
console.log("results", results);
if (meta.filter) {
results = results.filter(r => r.distance >= meta.distanceThreshold);
console.log("before", results);
if (configStore.config.enable_reranker) {
results = results
.filter(r => r.rerank_score >= meta.rerankThreshold)
.sort((a, b) => b.rerank_score - a.rerank_score);
}
console.log("after", results);
results = results.slice(0, meta.topK);
}
filteredResults.value = results;
}
const onQuery = () => {
console.log(queryText.value)
state.searchLoading = true
@ -214,6 +273,7 @@ const onQuery = () => {
.then(data => {
console.log(data)
queryResult.value = data
filterQueryResults()
})
.catch(error => {
console.error(error)
@ -348,7 +408,6 @@ const getDatabaseInfo = () => {
})
}
const deleteFile = (fileId) => {
console.log(fileId)
state.lock = true
@ -410,36 +469,12 @@ const addDocumentByFile = () => {
}
const columns = [
{
title: '文件ID',
dataIndex: 'file_id',
key: 'file_id',
},
{
title: '文件名',
dataIndex: 'filename',
key: 'filename',
},
{
title: '上传时间',
dataIndex: 'created_at',
key: 'created_at',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '操作',
key: 'action',
dataIndex: 'file_id',
}
{ title: '文件ID', dataIndex: 'file_id', key: 'file_id' },
{ title: '文件名', dataIndex: 'filename', key: 'filename' },
{ title: '上传时间', dataIndex: 'created_at', key: 'created_at' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: '操作', key: 'action', dataIndex: 'file_id' }
];
watch(() => route.params.database_id, (newId) => {
@ -450,6 +485,12 @@ watch(() => route.params.database_id, (newId) => {
}
);
// meta
watch(() => meta, () => {
filterQueryResults()
}, { deep: true })
onMounted(() => {
getDatabaseInfo();
// const refreshInterval = setInterval(() => {
@ -457,6 +498,8 @@ onMounted(() => {
// }, 10000);
})
</script>
<style lang="less" scoped>
@ -546,15 +589,27 @@ onMounted(() => {
}
}
.params-item.col {
align-items: flex-start;
flex-direction: column;
width: 100%;
height: auto;
}
.params-item.w100 > *{
width: 100%;
}
.ant-slider {
margin: 6px 0px;
}
}
}
.atab-container {
padding: 12px 16px;
padding: 0;
width: 100%;
max-height: 100%;
overflow: auto;
}
.db-info-container {
@ -675,8 +730,10 @@ onMounted(() => {
color: white;
padding: 2px 4px;
border-radius: 4px;
font-size: small;
font-size: 10px;
font-weight: bold;
opacity: 0.8;
user-select: none;
}
.pdf {
@ -711,3 +768,24 @@ onMounted(() => {
}
</style>
<style lang="less">
.atab-container div.ant-tabs-nav {
background: var(--main-light-5);
padding: 1rem;
padding-bottom: 0;
}
.atab-container .ant-tabs-content-holder {
padding: 0 1rem;
}
.params-item.col .ant-segmented {
width: 100%;
div.ant-segmented-group {
display: flex;
justify-content: space-around;
}
}
</style>

View File

@ -10,18 +10,18 @@
</div>
<div class="graph-container layout-container" v-else>
<div class="info">
<h2>图数据库 {{ graph?.database_name }}</h2>
<h2>图数据库 {{ graphInfo?.database_name }}</h2>
<p>
<span v-if="state.graphloading">加载中</span>
<span class="green-dot" v-if="graph?.status == 'open'"></span>
<span v-if="state.loadingGraphInfo">加载中</span>
<span class="green-dot" v-if="graphInfo?.status == 'open'"></span>
<span class="red-dot" v-else></span>
<span>{{ graph?.status }}</span> ·
<span> {{ graph?.entity_count }} 实体{{ graph?.relationship_count }} 个关系</span>
<span>{{ graphInfo?.status }}</span> ·
<span> {{ graphInfo?.entity_count }} 实体{{ graphInfo?.relationship_count }} 个关系</span>
</p>
</div>
<div class="actions">
<div class="actions-left">
<a-button @click="state.showModal = true">上传文件</a-button>
<a-button @click="state.showModal = true"><UploadOutlined /> 上传文件</a-button>
<a-modal
:open="state.showModal" title="上传文件"
@ok="addDocumentByFile"
@ -42,15 +42,15 @@
>
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
<p class="ant-upload-hint">
目前仅支持上传文本文件 .pdf, .txt, .md且同名文件无法重复添加
目前仅支持上传 jsonl 文件且同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
</a-modal>
<input v-model="sampleNodeCount">
<a-button @click="loadSampleNodes">确定</a-button>
<a-button @click="loadSampleNodes" :loading="state.fetching">获取节点</a-button>
</div>
<div class="action-right">
<div class="actions-right">
<input
v-model="state.searchInput"
placeholder="输入要查询的实体"
@ -66,7 +66,8 @@
</a-button>
</div>
</div>
<div class="main" id="container" ref="container"></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;"/>
</div>
</template>
@ -75,21 +76,23 @@ import { Graph } from "@antv/g6";
import { computed, onMounted, reactive, ref } from 'vue';
import { message } from "ant-design-vue";
import { useConfigStore } from '@/stores/config';
import { UploadOutlined } from '@ant-design/icons-vue';
const configStore = useConfigStore()
let graphInstance
const graph = ref(null)
const graphInfo = ref(null)
const container = ref(null);
const fileList = ref([]);
const sampleNodeCount = ref(100);
const subgraph = reactive({
const graphData = reactive({
nodes: [],
edges: [],
});
const state = reactive({
graphloading: false,
fetching: false,
loadingGraphInfo: false,
searchInput: '',
searchLoading: false,
showModal: false,
@ -98,27 +101,27 @@ const state = reactive({
})
const loadGraph = () => {
state.graphloading = true
const loadGraphInfo = () => {
state.loadingGraphInfo = true
fetch('/api/database/graph', {
method: "GET",
})
.then(response => response.json())
.then(data => {
console.log(data)
graph.value = data.graph
state.graphloading = false
graphInfo.value = data.graph
state.loadingGraphInfo = false
})
.catch(error => {
console.error(error)
message.error(error.message)
state.graphloading = false
state.loadingGraphInfo = false
})
}
const graphData = computed(() => {
const getGraphData = () => {
return {
nodes: subgraph.nodes.map(node => {
nodes: graphData.nodes.map(node => {
return {
id: node.id,
data: {
@ -126,7 +129,7 @@ const graphData = computed(() => {
},
}
}),
edges: subgraph.edges.map(edge => {
edges: graphData.edges.map(edge => {
return {
source: edge.source_id,
target: edge.target_id,
@ -136,7 +139,7 @@ const graphData = computed(() => {
}
}),
}
})
}
const addDocumentByFile = () => {
state.precessing = true
@ -159,6 +162,7 @@ const addDocumentByFile = () => {
};
const loadSampleNodes = () => {
state.fetching = true
fetch(`/api/database/graph/nodes?kgdb_name=neo4j&num=${sampleNodeCount.value}`)
.then((res) => {
if (res.ok) {
@ -168,17 +172,15 @@ const loadSampleNodes = () => {
}
})
.then((data) => {
subgraph.nodes = data.result.nodes
subgraph.edges = data.result.edges
console.log(data)
console.log(subgraph)
setTimeout(() => {
randerGraph()
}, 500)
graphData.nodes = data.result.nodes
graphData.edges = data.result.edges
console.log(graphData)
randerGraph()
})
.catch((error) => {
message.error(error.message);
})
.finally(() => state.fetching = false)
}
const onSearch = () => {
@ -187,6 +189,12 @@ 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/database/graph/node?entity_name=${state.searchInput}`)
.then((res) => {
@ -197,10 +205,13 @@ const onSearch = () => {
}
})
.then((data) => {
subgraph.nodes = data.result.nodes
subgraph.edges = data.result.edges
graphData.nodes = data.result.nodes
graphData.edges = data.result.edges
if (graphData.nodes.length === 0) {
message.info('未找到相关实体')
}
console.log(data)
console.log(subgraph)
console.log(graphData)
randerGraph()
})
.catch((error) => {
@ -210,54 +221,58 @@ const onSearch = () => {
};
const randerGraph = () => {
graphInstance.setData(graphData.value);
if (graphInstance) {
graphInstance.destroy();
}
initGraph();
graphInstance.setData(getGraphData());
graphInstance.render();
}
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'],
});
window.addEventListener('resize', randerGraph);
}
onMounted(() => {
loadGraph();
loadGraphInfo();
loadSampleNodes();
setTimeout(() => {
if (state.showPage) {
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: 100,
collide: {
strength: 0.5,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 40,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
graphInstance.setData(graphData.value);
graphInstance.render();
window.addEventListener('resize', randerGraph);
}
}, 400)
});
@ -303,7 +318,7 @@ const handleDrop = (event) => {
justify-content: space-between;
margin-bottom: 20px;
.actions-left {
.actions-left, .actions-right {
display: flex;
align-items: center;
gap: 10px;
@ -311,7 +326,6 @@ const handleDrop = (event) => {
input {
width: 100px;
margin-right: 10px;
border-radius: 8px;
padding: 4px 12px;
border: 2px solid var(--main-300);
@ -324,6 +338,7 @@ const handleDrop = (event) => {
}
button {
border-width: 2px;
height: 40px;
box-shadow: none;
}
@ -343,8 +358,9 @@ const handleDrop = (event) => {
margin: 20px 0;
border-radius: 16px;
width: 100%;
height: 800px;
height: calc(100vh - 200px);
resize: horizontal;
overflow: hidden;
}
.database-empty {

View File

@ -42,7 +42,8 @@
</a-select>
</div>
<div class="card">
<span class="label">{{ items?.embed_model.des }} &nbsp;
<span class="label">
{{ items?.embed_model.des }} &nbsp;
<a-button small v-if="needRestart.embed_model" @click="sendRestart">
<ReloadOutlined />需要重启
</a-button>
@ -58,7 +59,8 @@
</a-select>
</div>
<div class="card">
<span class="label">{{ items?.reranker.des }} &nbsp;
<span class="label">
{{ items?.reranker.des }} &nbsp;
<a-button small v-if="needRestart.reranker" @click="sendRestart">
<ReloadOutlined />需要重启
</a-button>
@ -89,7 +91,11 @@
/>
</div>
<div class="card">
<span class="label">{{ items?.enable_knowledge_graph.des }}</span>
<span class="label">{{ items?.enable_knowledge_graph.des }}
<a-button small v-if="needRestart.enable_knowledge_graph" @click="sendRestart">
<ReloadOutlined />需要重启
</a-button>
</span>
<a-switch
:checked="configStore.config.enable_knowledge_graph"
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
@ -194,6 +200,13 @@ const sendRestart = () => {
.label {
margin-right: 20px;
button {
margin-left: 10px;
height: 24px;
padding: 0 8px;
font-size: smaller;
}
}
}