sync
This commit is contained in:
parent
def68e8a3e
commit
0b2dd0179b
@ -51,7 +51,7 @@ class Config(SimpleConfig):
|
||||
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
|
||||
self.add_item("model_provider", default="zhipu", des="模型提供商", choices=["qianfan", "vllm", "zhipu", "deepseek", "dashscope"])
|
||||
self.add_item("model_name", default=None, des="模型名称")
|
||||
self.add_item("embed_model", default="bge-large-zh-v1.5", des="Embedding 模型", choices=["bge-large-zh-v1.5", "zhipu"])
|
||||
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"])
|
||||
self.add_item("model_local_paths", default={}, des="本地模型路径")
|
||||
### <<< 默认配置结束
|
||||
@ -92,22 +92,29 @@ class Config(SimpleConfig):
|
||||
"""根据传入的文件覆盖掉默认配置"""
|
||||
logger.info(f"Loading config from {self.filename}")
|
||||
if self.filename is not None and os.path.exists(self.filename):
|
||||
|
||||
if self.filename.endswith(".json"):
|
||||
with open(self.filename, 'r') as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
self.update(json.loads(content))
|
||||
local_config = json.loads(content)
|
||||
local_config.pop("_config_items")
|
||||
self.update(local_config)
|
||||
else:
|
||||
print(f"{self.filename} is empty.")
|
||||
|
||||
elif self.filename.endswith(".yaml"):
|
||||
with open(self.filename, 'r') as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
self.update(yaml.safe_load(content))
|
||||
local_config = yaml.safe_load(content)
|
||||
local_config.pop("_config_items")
|
||||
self.update(local_config)
|
||||
else:
|
||||
print(f"{self.filename} is empty.")
|
||||
else:
|
||||
logger.warning(f"Unknown config file type {self.filename}")
|
||||
|
||||
else:
|
||||
logger.warning(f"\n\n{'='*70}\n{'Config file not found':^70}\n{'You can custum your config in `' + self.filename + '`':^70}\n{'='*70}\n\n")
|
||||
|
||||
@ -170,7 +177,30 @@ MODEL_NAMES = {
|
||||
"llama3.1-8b-instruct",
|
||||
"llama3-8b-instruct",
|
||||
"llama3.1-405b-instruct",
|
||||
"baichuan2-7b-chat-v1",
|
||||
"qwen2-0.5b-instruct"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
EMBED_MODEL_INFO = {
|
||||
"bge-large-zh-v1.5": SimpleConfig({
|
||||
"name": "bge-large-zh-v1.5",
|
||||
"default_path": "BAAI/bge-large-zh-v1.5",
|
||||
"dimension": 1024,
|
||||
"query_instruction": "为这个句子生成表示以用于检索相关文章:",
|
||||
}),
|
||||
"zhipu-embedding-2": SimpleConfig({
|
||||
"name": "zhipu-embedding-2",
|
||||
"default_path": "embedding-2",
|
||||
"dimension": 1024,
|
||||
}),
|
||||
"zhipu-embedding-3": SimpleConfig({
|
||||
"name": "zhipu-embedding-3",
|
||||
"default_path": "embedding-3",
|
||||
"dimension": 2048,
|
||||
}),
|
||||
}
|
||||
|
||||
RERANKER_LIST = {
|
||||
"bge-reranker-v2-m3": "BAAI/bge-reranker-v2-m3",
|
||||
}
|
||||
@ -9,10 +9,11 @@ logger = setup_logger("DataBaseManager")
|
||||
|
||||
|
||||
class DataBaseLite:
|
||||
def __init__(self, name, description, db_type, **kwargs) -> None:
|
||||
def __init__(self, name, description, db_type, dimension=None, **kwargs) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.db_type = db_type
|
||||
self.dimension = dimension
|
||||
self.db_id = kwargs.get("db_id", hashstr(name))
|
||||
self.metaname = kwargs.get("metaname", f"{db_type[:1]}{hashstr(name)}")
|
||||
self.metadata = kwargs.get("metaname", {})
|
||||
@ -37,7 +38,8 @@ class DataBaseLite:
|
||||
"embed_model": self.embed_model,
|
||||
"metaname": self.metaname,
|
||||
"metadata": self.metadata,
|
||||
"files": self.files
|
||||
"files": self.files,
|
||||
"dimension": self.dimension
|
||||
}
|
||||
|
||||
def to_json(self):
|
||||
@ -115,10 +117,14 @@ class DataBaseManager:
|
||||
else:
|
||||
return {"message": "Graph base not enabled", "graph": {}}
|
||||
|
||||
def create_database(self, database_name, description, db_type):
|
||||
new_database = DataBaseLite(database_name, description, db_type, embed_model=self.config.embed_model)
|
||||
def create_database(self, database_name, description, db_type, dimension):
|
||||
new_database = DataBaseLite(database_name,
|
||||
description,
|
||||
db_type,
|
||||
embed_model=self.config.embed_model,
|
||||
dimension=dimension)
|
||||
|
||||
self.knowledge_base.add_collection(new_database.metaname)
|
||||
self.knowledge_base.add_collection(new_database.metaname, dimension)
|
||||
self.data["databases"].append(new_database)
|
||||
self._save_databases()
|
||||
return self.get_databases()
|
||||
|
||||
@ -18,7 +18,6 @@ class KnowledgeBase:
|
||||
self.client = MilvusClient(self.milvus_path)
|
||||
|
||||
def _init_config(self, config):
|
||||
self.vector_dim = 1024 # 暂时不知道这个和 embedding model 的 embedding 大小有什么关系
|
||||
self.milvus_path = os.path.join(config.save_dir, "data/vector_base/milvus.db")
|
||||
os.makedirs(os.path.dirname(self.milvus_path), exist_ok=True)
|
||||
|
||||
@ -40,14 +39,14 @@ class KnowledgeBase:
|
||||
# collection["id"] = hashstr(collection_name)
|
||||
return collection
|
||||
|
||||
def add_collection(self, collection_name):
|
||||
def add_collection(self, collection_name, dimension=None):
|
||||
if self.client.has_collection(collection_name=collection_name):
|
||||
logger.warning(f"Collection {collection_name} already exists, drop it")
|
||||
self.client.drop_collection(collection_name=collection_name)
|
||||
|
||||
self.client.create_collection(
|
||||
collection_name=collection_name,
|
||||
dimension=self.vector_dim, # The vectors we will use in this demo has 768 dimensions
|
||||
dimension= dimension, # The vectors we will use in this demo has 768 dimensions
|
||||
)
|
||||
|
||||
def add_documents(self, docs, collection_name, **kwargs):
|
||||
@ -55,8 +54,8 @@ class KnowledgeBase:
|
||||
# 检查 collection 是否存在
|
||||
import random
|
||||
if not self.client.has_collection(collection_name=collection_name):
|
||||
logger.warning(f"Collection {collection_name} not found, create it")
|
||||
self.add_collection(collection_name)
|
||||
logger.error(f"Collection {collection_name} not found, create it")
|
||||
# self.add_collection(collection_name)
|
||||
|
||||
vectors = self.embed_model.encode(docs)
|
||||
|
||||
|
||||
@ -66,29 +66,31 @@ class Retriever:
|
||||
|
||||
def query_knowledgebase(self, query, history, refs):
|
||||
"""查询知识库"""
|
||||
rw_query = self.rewrite_query(query, history, refs)
|
||||
|
||||
kb_res = []
|
||||
final_res = []
|
||||
if refs["meta"].get("db_name") and self.config.enable_knowledge_base:
|
||||
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"}
|
||||
|
||||
db_name = refs["meta"]["db_name"]
|
||||
kb = self.dbm.metaname2db[db_name]
|
||||
limit = refs["meta"].get("queryCount", 10)
|
||||
rw_query = self.rewrite_query(query, history, refs)
|
||||
|
||||
kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=limit)
|
||||
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:
|
||||
r["file"] = kb.id2file(r["entity"]["file_id"])
|
||||
|
||||
if self.config.enable_reranker:
|
||||
RERANK_THRESHOLD = 0.1
|
||||
for r in kb_res:
|
||||
r["file"] = kb.id2file(r["entity"]["file_id"])
|
||||
r["rerank_score"] = self.reranker.compute_score([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]
|
||||
|
||||
if self.config.enable_reranker:
|
||||
RERANK_THRESHOLD = 0.1
|
||||
for r in kb_res:
|
||||
r["rerank_score"] = self.reranker.compute_score([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]
|
||||
|
||||
else:
|
||||
final_res = kb_res[:5]
|
||||
else:
|
||||
final_res = kb_res[:5]
|
||||
|
||||
return {"results": final_res, "all_results": kb_res, "rw_query": rw_query}
|
||||
|
||||
|
||||
@ -11,8 +11,6 @@ class OpenAIBase():
|
||||
self.model_name = model_name
|
||||
|
||||
def predict(self, message, stream=False):
|
||||
|
||||
logger.debug(message)
|
||||
if isinstance(message, str):
|
||||
messages=[{"role": "user", "content": message}]
|
||||
else:
|
||||
|
||||
@ -1,37 +1,24 @@
|
||||
import os
|
||||
from FlagEmbedding import FlagModel, FlagReranker
|
||||
|
||||
from src.config import EMBED_MODEL_INFO, RERANKER_LIST
|
||||
from src.utils.logging_config import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger("EmbeddingModel")
|
||||
|
||||
SUPPORT_LIST = {
|
||||
"bge-large-zh-v1.5": "BAAI/bge-large-zh-v1.5",
|
||||
"zhipu": "embedding-3",
|
||||
}
|
||||
|
||||
RERANKER_LIST = {
|
||||
"bge-reranker-v2-m3": "BAAI/bge-reranker-v2-m3",
|
||||
}
|
||||
|
||||
QUERY_INSTRUCTION = {
|
||||
"bge-large-zh-v1.5": "为这个句子生成表示以用于检索相关文章:",
|
||||
}
|
||||
|
||||
class EmbeddingModel(FlagModel):
|
||||
def __init__(self, config, **kwargs):
|
||||
|
||||
assert config.embed_model in SUPPORT_LIST.keys(), f"Unsupported embed model: {config.embed_model}, only support {SUPPORT_LIST}"
|
||||
|
||||
model_name_or_path = config.model_local_paths.get(config.embed_model, SUPPORT_LIST[config.embed_model])
|
||||
logger.info(f"Loading embedding model {config.embed_model} from {model_name_or_path}")
|
||||
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.default_path)
|
||||
logger.info(f"Loading embedding model {model_info.name} from {model_name_or_path}")
|
||||
|
||||
super().__init__(model_name_or_path,
|
||||
query_instruction_for_retrieval=QUERY_INSTRUCTION[config.embed_model],
|
||||
query_instruction_for_retrieval=model_info.get("query_instruction", None),
|
||||
use_fp16=False, **kwargs)
|
||||
|
||||
logger.info(f"Embedding model {config.embed_model} loaded")
|
||||
logger.info(f"Embedding model {model_info.name} loaded")
|
||||
|
||||
|
||||
class Reranker(FlagReranker):
|
||||
@ -50,8 +37,9 @@ from zhipuai import ZhipuAI
|
||||
|
||||
class ZhipuEmbedding:
|
||||
|
||||
def __init__(self, config) -> None:
|
||||
def __init__(self, model_info, config) -> None:
|
||||
self.config = config
|
||||
self.model_info = model_info
|
||||
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAPI"))
|
||||
logger.info("Zhipu Embedding model loaded")
|
||||
self.query_instruction_for_retrieval = "为这个句子生成表示以用于检索相关文章:"
|
||||
@ -63,8 +51,8 @@ class ZhipuEmbedding:
|
||||
for i in range(0, len(message), 10):
|
||||
group_msg = message[i:i+10]
|
||||
response = self.client.embeddings.create(
|
||||
model=SUPPORT_LIST[self.config.embed_model],
|
||||
input=group_msg
|
||||
model=self.model_info.default_path,
|
||||
input=group_msg,
|
||||
)
|
||||
|
||||
data.extend([a.embedding for a in response.data])
|
||||
@ -83,7 +71,12 @@ def get_embedding_model(config):
|
||||
if not config.enable_knowledge_base:
|
||||
return None
|
||||
|
||||
if config.embed_model == "zhipu":
|
||||
return ZhipuEmbedding(config)
|
||||
else:
|
||||
return EmbeddingModel(config)
|
||||
assert config.embed_model in EMBED_MODEL_INFO.keys(), f"Unsupported embed model: {config.embed_model}, only support {EMBED_MODEL_INFO.keys()}"
|
||||
|
||||
if config.embed_model in ["bge-large-zh-v1.5"]:
|
||||
model = EmbeddingModel(EMBED_MODEL_INFO[config.embed_model], config)
|
||||
|
||||
if config.embed_model in ["zhipu-embedding-2", "zhipu-embedding-3"]:
|
||||
model = ZhipuEmbedding(EMBED_MODEL_INFO[config.embed_model], config)
|
||||
|
||||
return model
|
||||
@ -3,21 +3,23 @@ import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
|
||||
DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
|
||||
DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
|
||||
# DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
|
||||
LOG_FILE = f'log/project-{DATETIME}.log'
|
||||
|
||||
def setup_logger(name, log_file=None, level=logging.DEBUG, console=False):
|
||||
|
||||
if log_file is None:
|
||||
log_file = f'log/project-{DATETIME}.log'
|
||||
def setup_logger(name, level=logging.DEBUG, console=False):
|
||||
os.makedirs("log", exist_ok=True)
|
||||
|
||||
"""Function to setup logger with the given name and log file."""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# 清除已有的 Handler,防止重复添加
|
||||
if logger.hasHandlers():
|
||||
logger.handlers.clear()
|
||||
|
||||
# File handler for logging to a file
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler = logging.FileHandler(LOG_FILE)
|
||||
file_handler.setLevel(level)
|
||||
|
||||
# Formatter for the logs
|
||||
@ -34,6 +36,7 @@ def setup_logger(name, log_file=None, level=logging.DEBUG, console=False):
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# Setup the root logger
|
||||
logger = setup_logger('Athena')
|
||||
|
||||
|
||||
@ -29,7 +29,6 @@ def chat():
|
||||
request_data = json.loads(request.data)
|
||||
query = request_data['query']
|
||||
meta = request_data.get('meta')
|
||||
logger.debug(f"Web query: {query}")
|
||||
history_manager = HistoryManager(request_data['history'])
|
||||
|
||||
new_query, refs = startup.retriever(query, history_manager.messages, meta)
|
||||
@ -63,7 +62,7 @@ def call():
|
||||
request_data = json.loads(request.data)
|
||||
query = request_data['query']
|
||||
response = startup.model.predict(query)
|
||||
logger.debug(f"\n\n\nCall query: \n{query} \n\nResponse: \n{response.content}\n\n")
|
||||
logger.debug({"query": query, "response": response.content})
|
||||
|
||||
return jsonify({
|
||||
"response": response.content,
|
||||
@ -83,4 +82,13 @@ def update_config():
|
||||
@common.route('/restart', methods=['POST'])
|
||||
def restart():
|
||||
startup.restart()
|
||||
return jsonify({"message": "Restarted!"})
|
||||
return jsonify({"message": "Restarted!"})
|
||||
|
||||
|
||||
@common.route('/log', methods=['GET'])
|
||||
def get_log():
|
||||
from src.utils.logging_config import LOG_FILE
|
||||
with open(LOG_FILE, 'r') as f:
|
||||
log = f.read()
|
||||
|
||||
return jsonify({"log": log})
|
||||
@ -27,8 +27,9 @@ def create_database():
|
||||
database_name = data.get('database_name')
|
||||
description = data.get('description')
|
||||
db_type = data.get('db_type')
|
||||
dimension = data.get('dimension')
|
||||
logger.debug(f"Create database {database_name}")
|
||||
database = startup.dbm.create_database(database_name, description, db_type)
|
||||
database = startup.dbm.create_database(database_name, description, db_type, dimension=dimension)
|
||||
return jsonify(database)
|
||||
|
||||
@db.route('/', methods=['DELETE'])
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
--main-200: #8CC6E1;
|
||||
--main-100: #ABE0F7;
|
||||
--main-50: #CDF5FF;
|
||||
--main-25: #E6FAFF;
|
||||
|
||||
--c-white: #ffffff;
|
||||
--c-white-soft: #f8f8f8;
|
||||
|
||||
@ -117,49 +117,7 @@
|
||||
class="message-md"
|
||||
@click="consoleMsg(message)"></p>
|
||||
|
||||
<div class="refs" v-if="message.role=='received' && message.groupedResults && message.status=='finished'">
|
||||
<a-tag
|
||||
class="filetag"
|
||||
v-for="(results, filename) in message.groupedResults"
|
||||
:key="filename"
|
||||
@click="opts.openDetail = true"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ filename }}
|
||||
<a-drawer
|
||||
v-model:open="opts.openDetail"
|
||||
title="检索详情"
|
||||
width="800"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
class="retrieval-detail"
|
||||
rootClassName="root"
|
||||
>
|
||||
<div class="fileinfo">
|
||||
<p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
|
||||
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
|
||||
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
|
||||
</div>
|
||||
<div v-for="(res, idx) in results" :key="idx" class="result-item">
|
||||
<p class="result-id"><strong>ID:</strong> #{{ res.id }}</p>
|
||||
<p class="result-distance">
|
||||
<strong>相似度距离:</strong>
|
||||
<div class=scorebar>
|
||||
<a-progress :percent="(res.distance * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||
</div>
|
||||
</p>
|
||||
<p class="result-rerank-score">
|
||||
<strong>重排序分数:</strong>
|
||||
<div class=scorebar>
|
||||
<a-progress :percent="(res.rerank_score * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||
</div>
|
||||
</p>
|
||||
<a-divider />
|
||||
<p class="result-text">{{ res.entity.text }}</p>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</a-tag>
|
||||
</div>
|
||||
<RefsComponent v-if="message.role=='received' && message.status=='finished'" :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
@ -196,11 +154,14 @@ import {
|
||||
PlusCircleOutlined,
|
||||
FolderOutlined,
|
||||
FolderOpenOutlined,
|
||||
GlobalOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { Marked } from 'marked';
|
||||
import { markedHighlight } from 'marked-highlight';
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import RefsComponent from '@/components/RefsComponent.vue'
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
@ -663,57 +624,9 @@ watch(
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.refs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filetag:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.retrieval-detail {
|
||||
.fileinfo {
|
||||
margin-bottom: 20px;
|
||||
padding: 1rem;
|
||||
background: var(--main-50);
|
||||
color: var(--main-800);
|
||||
border-radius: 8px;
|
||||
// border: 1px solid var(--main-100);
|
||||
p {
|
||||
margin: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 24px 16px 10px 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
background: var(--main-light-6);
|
||||
|
||||
.result-id,
|
||||
.result-distance,
|
||||
.result-rerank-score,
|
||||
.result-text-label,
|
||||
.result-text {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.scorebar {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
padding-bottom: 2px;
|
||||
|
||||
|
||||
& > * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
85
web/src/components/DebugComponent.vue
Normal file
85
web/src/components/DebugComponent.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="log-viewer">
|
||||
<a-button @click="fetchLogs">刷新</a-button>
|
||||
<div ref="logContainer" class="log-container">
|
||||
<pre v-if="logs">{{ logs }}</pre>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onActivated, nextTick } from 'vue';
|
||||
|
||||
// 定义一个 ref 来存储日志数据和错误信息
|
||||
const logs = ref('');
|
||||
const error = ref('');
|
||||
|
||||
// 定义一个 ref 来获取日志容器的 DOM 元素
|
||||
const logContainer = ref(null);
|
||||
|
||||
// 定义一个方法来获取日志数据
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
// 清空之前的错误信息
|
||||
error.value = '';
|
||||
|
||||
// 发送请求获取日志数据
|
||||
const response = await fetch('/api/log'); // 替换为你的 API 路径
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch logs');
|
||||
}
|
||||
|
||||
// 解析 JSON 数据
|
||||
const data = await response.json();
|
||||
|
||||
// 将日志信息赋值给 logs 变量
|
||||
logs.value = data.log;
|
||||
|
||||
// 等待 DOM 更新完成后自动滚动到最底部
|
||||
await nextTick();
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight;
|
||||
}
|
||||
} catch (err) {
|
||||
// 处理请求错误
|
||||
error.value = `Error: ${err.message}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件第一次挂载时自动获取日志并滚动到底部
|
||||
onMounted(() => {
|
||||
fetchLogs();
|
||||
setInterval(fetchLogs, 5000); // 每 5 秒自动刷新一次日志
|
||||
});
|
||||
|
||||
// 组件重新激活时(从 keep-alive 缓存恢复)自动获取日志
|
||||
onActivated(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-viewer {}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
max-height: 80vh; /* 设置最大高度 */
|
||||
overflow-y: auto; /* 启用垂直滚动 */
|
||||
background-color: #f0f0f0;
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap; /* 使日志内容自动换行 */
|
||||
word-wrap: break-word;
|
||||
|
||||
background: #0C0C0C;
|
||||
color: #D1D1D1;
|
||||
}
|
||||
</style>
|
||||
143
web/src/components/RefsComponent.vue
Normal file
143
web/src/components/RefsComponent.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<!-- RefsComponent.vue -->
|
||||
<template>
|
||||
<div class="refs" v-if="showRefs">
|
||||
<span class="item"><GlobalOutlined /> {{ message.model_name }}</span>
|
||||
<div class="tags">
|
||||
<span class="filetag item"
|
||||
v-for="(results, filename) in message.groupedResults"
|
||||
:key="filename"
|
||||
@click="openDetail = true"
|
||||
>
|
||||
<FileTextOutlined /> {{ filename }}
|
||||
<a-drawer
|
||||
v-model:open="openDetail"
|
||||
title="检索详情"
|
||||
width="800"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
class="retrieval-detail"
|
||||
rootClassName="root"
|
||||
>
|
||||
<div class="fileinfo">
|
||||
<p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
|
||||
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
|
||||
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
|
||||
</div>
|
||||
<div v-for="(res, idx) in results" :key="idx" class="result-item">
|
||||
<p class="result-id"><strong>ID:</strong> #{{ res.id }}</p>
|
||||
<p class="result-distance">
|
||||
<strong>相似度距离:</strong>
|
||||
<div class="scorebar">
|
||||
<a-progress :percent="(res.distance * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||
</div>
|
||||
</p>
|
||||
<p class="result-rerank-score">
|
||||
<strong>重排序分数:</strong>
|
||||
<div class="scorebar">
|
||||
<a-progress :percent="(res.rerank_score * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||
</div>
|
||||
</p>
|
||||
<a-divider />
|
||||
<p class="result-text">{{ res.entity.text }}</p>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
GlobalOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
message: Object,
|
||||
})
|
||||
|
||||
const message = ref(props.message)
|
||||
|
||||
const openDetail = ref(false)
|
||||
const showRefs = computed(() => message.value.role=='received' && message.value.status=='finished')
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.refs {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
color: var(--c-text-light-4);
|
||||
font-size: 14px;
|
||||
gap: 10px;
|
||||
|
||||
.item {
|
||||
background: var(--main-25);
|
||||
color: var(--main-800);
|
||||
border: 1px solid var(--main-100);
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.filetag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--main-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.retrieval-detail {
|
||||
.fileinfo {
|
||||
margin-bottom: 20px;
|
||||
padding: 1rem;
|
||||
background: var(--main-25);
|
||||
color: var(--main-800);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--main-100);
|
||||
p {
|
||||
margin: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 24px 16px 10px 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
background: var(--main-light-6);
|
||||
|
||||
.result-id,
|
||||
.result-distance,
|
||||
.result-rerank-score,
|
||||
.result-text-label,
|
||||
.result-text {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.scorebar {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
padding-bottom: 2px;
|
||||
|
||||
|
||||
& > * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { KeepAlive, onMounted } from 'vue'
|
||||
import { ref, KeepAlive, onMounted } from 'vue'
|
||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||
import {
|
||||
MessageOutlined,
|
||||
@ -15,10 +15,13 @@ import {
|
||||
import { themeConfig } from '@/assets/theme'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useDatabaseStore } from '@/stores/database'
|
||||
import DebugComponent from '@/components/DebugComponent.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const databaseStore = useDatabaseStore()
|
||||
|
||||
const showDebug = ref(false)
|
||||
|
||||
const getRemoteConfig = () => {
|
||||
fetch('/api/config').then(res => res.json()).then(data => {
|
||||
console.log(data)
|
||||
@ -48,6 +51,18 @@ console.log(route)
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<div class="debug-panel">
|
||||
<div class="shown-btn" @click="showDebug=!showDebug">Debug</div>
|
||||
<a-drawer
|
||||
v-model:open="showDebug"
|
||||
title="调试面板"
|
||||
width="800"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
>
|
||||
<DebugComponent />
|
||||
</a-drawer>
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<router-link to="/"><img src="/jnu.png"> </router-link>
|
||||
@ -98,6 +113,19 @@ console.log(route)
|
||||
.header-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 0;
|
||||
top: 50px;
|
||||
border-radius: 16px 0 0 16px;
|
||||
background-color: var(--main-light-3);
|
||||
padding: 8px 8px 8px 16px;
|
||||
box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: right 0.3s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
div.header, #app-router-view {
|
||||
|
||||
@ -67,6 +67,19 @@ const router = createRouter({
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/monitor',
|
||||
// name: 'monitor',
|
||||
// component: AppLayout,
|
||||
// children: [
|
||||
// {
|
||||
// path: '',
|
||||
// name: 'monitor',
|
||||
// component: () => import('../views/MonitorView.vue'),
|
||||
// meta: { keepAlive: true }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
||||
@ -11,6 +11,9 @@
|
||||
placeholder="新建数据库描述"
|
||||
:auto-size="{ minRows: 2, maxRows: 5 }"
|
||||
/>
|
||||
<h3 style="margin-top: 20px;">向量维度</h3>
|
||||
<p>必须与向量模型 {{ configStore.config.embed_model }} 一致</p>
|
||||
<a-input v-model:value="newDatabase.dimension" placeholder="向量维度 (e.g. 768, 1024)" />
|
||||
<template #footer>
|
||||
<a-button key="back" @click="newDatabase.open=false">取消</a-button>
|
||||
<a-button key="submit" type="primary" :loading="newDatabase.loading" @click="createDatabase">创建</a-button>
|
||||
@ -40,7 +43,8 @@
|
||||
</div>
|
||||
<p class="description">{{ database.description }}</p>
|
||||
<div class="tags">
|
||||
<a-tag color="blue" v-if="database.embed_model">Embed: {{ database.embed_model }}</a-tag>
|
||||
<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>
|
||||
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
|
||||
</div>
|
||||
@ -94,6 +98,7 @@ const configStore = useConfigStore()
|
||||
const newDatabase = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
dimension: '',
|
||||
loading: false,
|
||||
})
|
||||
|
||||
@ -123,7 +128,8 @@ const createDatabase = () => {
|
||||
body: JSON.stringify({
|
||||
database_name: newDatabase.name,
|
||||
description: newDatabase.description,
|
||||
db_type: "knowledge"
|
||||
db_type: "knowledge",
|
||||
dimension: newDatabase.dimension,
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
@ -132,7 +138,8 @@ const createDatabase = () => {
|
||||
loadDatabases()
|
||||
newDatabase.open = false
|
||||
newDatabase.name = ''
|
||||
newDatabase.description = ''
|
||||
newDatabase.description = '',
|
||||
newDatabase.dimension = ''
|
||||
})
|
||||
.finally(() => {
|
||||
newDatabase.loading = false
|
||||
@ -238,6 +245,7 @@ onMounted(() => {
|
||||
background-color: #F5F8FF;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0EAFF;
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.info {
|
||||
@ -246,6 +254,11 @@ onMounted(() => {
|
||||
color: black;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--c-text-light-1);
|
||||
font-size: small;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 160px"
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_provider"
|
||||
@change="handleChange('model_provider', $event)"
|
||||
>
|
||||
@ -29,7 +29,7 @@
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 160px"
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_name"
|
||||
@change="handleChange('model_name', $event)"
|
||||
v-if="configStore.config?.model_names && configStore.config?.model_provider && configStore.config?.model_names[configStore.config?.model_provider]"
|
||||
@ -47,7 +47,7 @@
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 160px"
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.embed_model"
|
||||
@change="handleChange('embed_model', $event)"
|
||||
>
|
||||
@ -63,7 +63,7 @@
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 160px"
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.reranker"
|
||||
@change="handleChange('reranker', $event)"
|
||||
:disabled="!configStore.config.enable_reranker"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user