commit
10a2f036c0
8
.gitignore
vendored
8
.gitignore
vendored
@ -25,13 +25,19 @@ cache
|
|||||||
|
|
||||||
### IDE
|
### IDE
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
*.nogit.*
|
*.nogit.*
|
||||||
|
|
||||||
*.pdf
|
*.pdf
|
||||||
|
*.yaml
|
||||||
src/data
|
src/data
|
||||||
neo4j*
|
neo4j*
|
||||||
*/package-lock.json
|
*/package-lock.json
|
||||||
web/package-lock.json
|
web/package-lock.json
|
||||||
saves
|
saves
|
||||||
notebooks
|
notebooks
|
||||||
*.yaml
|
local_neo4j/data
|
||||||
|
local_neo4j/logs
|
||||||
|
local_neo4j/import
|
||||||
|
local_neo4j/plugins
|
||||||
|
local_neo4j/conf
|
||||||
34
README.md
34
README.md
@ -3,19 +3,39 @@
|
|||||||
|
|
||||||
<img src="web/public/home.png" style="border-radius: 16px; margin: 0 auto; max-height: 400px; display: block;"/>
|
<img src="web/public/home.png" style="border-radius: 16px; margin: 0 auto; max-height: 400px; display: block;"/>
|
||||||
|
|
||||||
### 准备
|
## 准备
|
||||||
|
|
||||||
1. 提供 API 服务商的 API_KEY,并放置在 `src/.env` 文件中,参考 `src/.env.template`。默认使用的是智谱AI。
|
1. 提供 API 服务商的 API_KEY,并放置在 `src/.env` 文件中,参考 `src/.env.template`。默认使用的是智谱AI。
|
||||||
2. 配置 python 环境 `pip install -r src/requirements.txt`
|
2. 配置 python 环境 `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
**如果不启用知识库,可以仅安装下面的依赖**
|
||||||
|
|
||||||
### 启动命令行模式
|
```
|
||||||
|
FlagEmbedding==1.2.10
|
||||||
```bash
|
Flask==3.0.3
|
||||||
python -m src.cli
|
Flask_Cors==4.0.1
|
||||||
|
openai==1.35.10
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
PyYAML==6.0.1
|
||||||
|
zhipuai
|
||||||
```
|
```
|
||||||
|
|
||||||
### 启动网页模式
|
### 【可选】配置图数据库 neo4j
|
||||||
|
|
||||||
|
使用 docker 部署 neo4j 服务,配置文件见 [local_neo4j/docker-compose.yml](local_neo4j/docker-compose.yml).
|
||||||
|
默认账号密码见最后一行,可以使用 `http://localhost:7474/` 在浏览器可视化访问。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd local_neo4j
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
可以使用 `python test_neo4j.py` 来测试是否正常启动。使用 `docker compose down` 可停止服务。
|
||||||
|
如果想要管理 neo4j,也可以使用 `docker ps` 查看容器 id,然后使用 `docker exec -it <CONTAINER_ID> /bin/bash` 进入容器。
|
||||||
|
如果想要删除数据库中的文件,可以进入容器并停止 neo4j 后,执行 `rm -rf /data/databases`。
|
||||||
|
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m src.api
|
python -m src.api
|
||||||
|
|||||||
17
local_neo4j/docker-compose.yml
Normal file
17
local_neo4j/docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: '3.9'
|
||||||
|
services:
|
||||||
|
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:latest
|
||||||
|
volumes:
|
||||||
|
- ./conf:/var/lib/neo4j/conf
|
||||||
|
- ./import:/var/lib/neo4j/import
|
||||||
|
- ./plugins:/plugins
|
||||||
|
- ./data:/data
|
||||||
|
- ./logs:/var/lib/neo4j/logs
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 7474:7474
|
||||||
|
- 7687:7687
|
||||||
|
environment:
|
||||||
|
- NEO4J_AUTH=neo4j/0123456789
|
||||||
34
local_neo4j/test_neo4j.py
Normal file
34
local_neo4j/test_neo4j.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from neo4j import GraphDatabase
|
||||||
|
from neo4j.exceptions import ServiceUnavailable, AuthError
|
||||||
|
|
||||||
|
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" 表示认证失败。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
driver = GraphDatabase.driver(uri, auth=(username, password))
|
||||||
|
with driver.session() as session:
|
||||||
|
# 简单的查询来测试连接
|
||||||
|
result = session.run("RETURN 1")
|
||||||
|
if result.single()[0] == 1:
|
||||||
|
return "OK"
|
||||||
|
except ServiceUnavailable:
|
||||||
|
return "UNAVAILABLE"
|
||||||
|
except AuthError:
|
||||||
|
return "AUTH_FAILED"
|
||||||
|
finally:
|
||||||
|
# 确保关闭驱动
|
||||||
|
driver.close()
|
||||||
|
|
||||||
|
# 测试函数
|
||||||
|
status = check_neo4j_status()
|
||||||
|
print(f"Neo4j status: {status}")
|
||||||
|
|
||||||
18
requirements.txt
Normal file
18
requirements.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
dashscope==1.20.5
|
||||||
|
FlagEmbedding==1.2.11
|
||||||
|
Flask==3.0.3
|
||||||
|
Flask_Cors==4.0.1
|
||||||
|
llama_index==0.11.1
|
||||||
|
neo4j==5.23.1
|
||||||
|
openai==1.42.0
|
||||||
|
paddleocr==2.8.1
|
||||||
|
pymilvus==2.4.5
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
PyYAML==6.0.2
|
||||||
|
qianfan==0.4.6
|
||||||
|
torch==2.4.0
|
||||||
|
tqdm==4.66.5
|
||||||
|
zhipuai==2.1.4.20230814
|
||||||
|
PyMuPDF
|
||||||
|
llama-index-readers-file
|
||||||
|
peft
|
||||||
@ -38,12 +38,12 @@ class Config(SimpleConfig):
|
|||||||
|
|
||||||
### >>> 默认配置
|
### >>> 默认配置
|
||||||
# 可以在 config/base.yaml 中覆盖
|
# 可以在 config/base.yaml 中覆盖
|
||||||
self.add_item("mode", default="cli", des="运行模式", choices=["cli", "api"])
|
|
||||||
self.add_item("stream", default=True, des="是否开启流式输出")
|
self.add_item("stream", default=True, des="是否开启流式输出")
|
||||||
self.add_item("save_dir", default="saves", des="保存目录")
|
self.add_item("save_dir", default="saves", des="保存目录")
|
||||||
# 功能选项
|
# 功能选项
|
||||||
self.add_item("enable_reranker", default=False, des="是否开启重排序")
|
self.add_item("enable_reranker", default=False, des="是否开启重排序")
|
||||||
self.add_item("enable_knowledge_base", 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_search_engine", default=False, des="是否开启搜索引擎")
|
||||||
|
|
||||||
# 模型配置
|
# 模型配置
|
||||||
@ -70,6 +70,13 @@ class Config(SimpleConfig):
|
|||||||
"choices": choices
|
"choices": choices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __dict__(self):
|
||||||
|
blocklist = [
|
||||||
|
"_config_items",
|
||||||
|
"model_names",
|
||||||
|
]
|
||||||
|
return {k: v for k, v in self.items() if k not in blocklist}
|
||||||
|
|
||||||
def handle_self(self):
|
def handle_self(self):
|
||||||
### handle local model
|
### handle local model
|
||||||
model_root_dir = os.getenv("MODEL_ROOT_DIR", "pretrained_models")
|
model_root_dir = os.getenv("MODEL_ROOT_DIR", "pretrained_models")
|
||||||
@ -98,7 +105,6 @@ class Config(SimpleConfig):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
if content:
|
if content:
|
||||||
local_config = json.loads(content)
|
local_config = json.loads(content)
|
||||||
local_config.pop("_config_items")
|
|
||||||
self.update(local_config)
|
self.update(local_config)
|
||||||
else:
|
else:
|
||||||
print(f"{self.filename} is empty.")
|
print(f"{self.filename} is empty.")
|
||||||
@ -108,7 +114,6 @@ class Config(SimpleConfig):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
if content:
|
if content:
|
||||||
local_config = yaml.safe_load(content)
|
local_config = yaml.safe_load(content)
|
||||||
local_config.pop("_config_items")
|
|
||||||
self.update(local_config)
|
self.update(local_config)
|
||||||
else:
|
else:
|
||||||
print(f"{self.filename} is empty.")
|
print(f"{self.filename} is empty.")
|
||||||
|
|||||||
@ -8,46 +8,6 @@ from src.models.embedding import get_embedding_model
|
|||||||
logger = setup_logger("DataBaseManager")
|
logger = setup_logger("DataBaseManager")
|
||||||
|
|
||||||
|
|
||||||
class DataBaseLite:
|
|
||||||
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", {})
|
|
||||||
self.files = kwargs.get("files", [])
|
|
||||||
self.embed_model = kwargs.get("embed_model", None)
|
|
||||||
|
|
||||||
def id2file(self, file_id):
|
|
||||||
for f in self.files:
|
|
||||||
if f["file_id"] == file_id:
|
|
||||||
return f
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, metadata):
|
|
||||||
self.metadata = metadata
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"name": self.name,
|
|
||||||
"description": self.description,
|
|
||||||
"db_type": self.db_type,
|
|
||||||
"db_id": self.db_id,
|
|
||||||
"embed_model": self.embed_model,
|
|
||||||
"metaname": self.metaname,
|
|
||||||
"metadata": self.metadata,
|
|
||||||
"files": self.files,
|
|
||||||
"dimension": self.dimension
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
return json.dumps(self.to_dict(), ensure_ascii=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.to_json()
|
|
||||||
|
|
||||||
class DataBaseManager:
|
class DataBaseManager:
|
||||||
|
|
||||||
def __init__(self, config=None) -> None:
|
def __init__(self, config=None) -> None:
|
||||||
@ -111,13 +71,16 @@ class DataBaseManager:
|
|||||||
return {"databases": [db.to_dict() for db in self.data["databases"]]}
|
return {"databases": [db.to_dict() for db in self.data["databases"]]}
|
||||||
|
|
||||||
def get_graph(self):
|
def get_graph(self):
|
||||||
if self.config.enable_graph_base:
|
if self.config.enable_knowledge_graph:
|
||||||
self.data["graph"].update(self.graph_base.get_database_info("neo4j"))
|
self.data["graph"].update(self.graph_base.get_database_info("neo4j"))
|
||||||
return {"graph": self.data["graph"]}
|
return {"graph": self.data["graph"]}
|
||||||
else:
|
else:
|
||||||
return {"message": "Graph base not enabled", "graph": {}}
|
return {"message": "Graph base not enabled", "graph": {}}
|
||||||
|
|
||||||
def create_database(self, database_name, description, db_type, dimension):
|
def create_database(self, database_name, description, db_type, dimension):
|
||||||
|
from src.config import EMBED_MODEL_INFO
|
||||||
|
dimension = dimension or EMBED_MODEL_INFO[self.config.embed_model]["dimension"]
|
||||||
|
|
||||||
new_database = DataBaseLite(database_name,
|
new_database = DataBaseLite(database_name,
|
||||||
description,
|
description,
|
||||||
db_type,
|
db_type,
|
||||||
@ -134,7 +97,7 @@ class DataBaseManager:
|
|||||||
|
|
||||||
if db.embed_model != self.config.embed_model:
|
if db.embed_model != self.config.embed_model:
|
||||||
logger.error(f"Embed model not match, {db.embed_model} != {self.config.embed_model}")
|
logger.error(f"Embed model not match, {db.embed_model} != {self.config.embed_model}")
|
||||||
return {"message": "Embed model not match", "status": "failed"}
|
return {"message": f"Embed model not match, cur: {self.config.embed_model}", "status": "failed"}
|
||||||
|
|
||||||
new_files = []
|
new_files = []
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -208,7 +171,6 @@ class DataBaseManager:
|
|||||||
logger.error(f"File format not supported, only support {support_format}")
|
logger.error(f"File format not supported, only support {support_format}")
|
||||||
raise Exception(f"File format not supported, only support {support_format}")
|
raise Exception(f"File format not supported, only support {support_format}")
|
||||||
|
|
||||||
|
|
||||||
def delete_file(self, db_id, file_id):
|
def delete_file(self, db_id, file_id):
|
||||||
db = self.get_kb_by_id(db_id)
|
db = self.get_kb_by_id(db_id)
|
||||||
file_idx_to_delete = [idx for idx, f in enumerate(db.files) if f["file_id"] == file_id][0]
|
file_idx_to_delete = [idx for idx, f in enumerate(db.files) if f["file_id"] == file_id][0]
|
||||||
@ -253,3 +215,44 @@ class DataBaseManager:
|
|||||||
if db.db_id == db_id:
|
if db.db_id == db_id:
|
||||||
return db
|
return db
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DataBaseLite:
|
||||||
|
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", {})
|
||||||
|
self.files = kwargs.get("files", [])
|
||||||
|
self.embed_model = kwargs.get("embed_model", None)
|
||||||
|
|
||||||
|
def id2file(self, file_id):
|
||||||
|
for f in self.files:
|
||||||
|
if f["file_id"] == file_id:
|
||||||
|
return f
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update(self, metadata):
|
||||||
|
self.metadata = metadata
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"db_type": self.db_type,
|
||||||
|
"db_id": self.db_id,
|
||||||
|
"embed_model": self.embed_model,
|
||||||
|
"metaname": self.metaname,
|
||||||
|
"metadata": self.metadata,
|
||||||
|
"files": self.files,
|
||||||
|
"dimension": self.dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self.to_dict(), ensure_ascii=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.to_json()
|
||||||
@ -9,11 +9,12 @@ import warnings
|
|||||||
|
|
||||||
from src.plugins import pdf2txt
|
from src.plugins import pdf2txt
|
||||||
from src.plugins.oneke import OneKE
|
from src.plugins.oneke import OneKE
|
||||||
|
from src.utils import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger("server-graphbase")
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=UserWarning)
|
warnings.filterwarnings("ignore", category=UserWarning)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
UIE_MODEL = None
|
UIE_MODEL = None
|
||||||
|
|
||||||
class GraphDatabase:
|
class GraphDatabase:
|
||||||
@ -36,6 +37,16 @@ class GraphDatabase:
|
|||||||
"""关闭数据库连接"""
|
"""关闭数据库连接"""
|
||||||
self.driver.close()
|
self.driver.close()
|
||||||
|
|
||||||
|
def get_sample_nodes(self, kgdb_name='neo4j', num=50):
|
||||||
|
"""获取指定数据库的前 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))
|
||||||
|
return result.values()
|
||||||
|
|
||||||
|
with self.driver.session() as session:
|
||||||
|
return session.execute_read(query, num)
|
||||||
|
|
||||||
def create_graph_database(self, kgdb_name):
|
def create_graph_database(self, kgdb_name):
|
||||||
"""创建新的数据库,如果已存在则返回已有数据库的名称"""
|
"""创建新的数据库,如果已存在则返回已有数据库的名称"""
|
||||||
with self.driver.session() as session:
|
with self.driver.session() as session:
|
||||||
@ -116,21 +127,25 @@ class GraphDatabase:
|
|||||||
MERGE (t:Entity {name: $t})
|
MERGE (t:Entity {name: $t})
|
||||||
MERGE (h)-[r:RELATION {type: $r}]->(t)
|
MERGE (h)-[r:RELATION {type: $r}]->(t)
|
||||||
""", h=entry['h'], t=entry['t'], r=entry['r'])
|
""", h=entry['h'], t=entry['t'], r=entry['r'])
|
||||||
def _create_vector_index(tx):
|
def _create_vector_index(tx, dim):
|
||||||
index_name = "entity-embeddings"
|
index_name = "entityEmbeddings"
|
||||||
if not _index_exists(tx, index_name):
|
if not _index_exists(tx, index_name):
|
||||||
tx.run(f"""
|
tx.run(f"""
|
||||||
CREATE VECTOR INDEX {index_name}
|
CREATE VECTOR INDEX {index_name}
|
||||||
FOR (n: Entity) ON (n.embedding)
|
FOR (n: Entity) ON (n.embedding)
|
||||||
OPTIONS {{indexConfig: {{
|
OPTIONS {{indexConfig: {{
|
||||||
`vector.dimensions`: 1024,
|
`vector.dimensions`: {dim},
|
||||||
`vector.similarity_function`: 'cosine'
|
`vector.similarity_function`: 'cosine'
|
||||||
}} }};
|
}} }};
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
from src.config import EMBED_MODEL_INFO
|
||||||
|
embed_info = EMBED_MODEL_INFO[self.config.embed_model]
|
||||||
with self.driver.session() as session:
|
with self.driver.session() as session:
|
||||||
session.execute_write(_create_graph, triples)
|
session.execute_write(_create_graph, triples)
|
||||||
session.execute_write(_create_vector_index)
|
session.execute_write(_create_vector_index, embed_info.dimension)
|
||||||
for entry in triples:
|
for i, entry in enumerate(triples):
|
||||||
|
logger.info(f"Adding entity {i+1}/{len(triples)}")
|
||||||
embedding_h = self.get_embedding(entry['h'])
|
embedding_h = self.get_embedding(entry['h'])
|
||||||
session.execute_write(self.set_embedding, entry['h'], embedding_h)
|
session.execute_write(self.set_embedding, entry['h'], embedding_h)
|
||||||
|
|
||||||
@ -148,37 +163,39 @@ class GraphDatabase:
|
|||||||
|
|
||||||
triples = list(read_triples(file_path))
|
triples = list(read_triples(file_path))
|
||||||
|
|
||||||
def batch_create(tx, triples):
|
self.txt_add_vector_entity(triples, kgdb_name)
|
||||||
query = """
|
|
||||||
UNWIND $triples AS triple
|
|
||||||
MERGE (a:Entity {name: triple.h})
|
|
||||||
MERGE (b:Entity {name: triple.t})
|
|
||||||
MERGE (a)-[r:RELATION {type: triple.r}]->(b)
|
|
||||||
"""
|
|
||||||
tx.run(query, triples=triples)
|
|
||||||
|
|
||||||
def batch_add_embeddings(tx, embeddings):
|
# def batch_create(tx, triples):
|
||||||
query = """
|
# query = """
|
||||||
UNWIND $embeddings AS embedding
|
# UNWIND $triples AS triple
|
||||||
MATCH (e:Entity {name: embedding.name})
|
# MERGE (a:Entity {name: triple.h})
|
||||||
SET e.embedding = embedding.vector
|
# MERGE (b:Entity {name: triple.t})
|
||||||
"""
|
# MERGE (a)-[r:RELATION {type: triple.r}]->(b)
|
||||||
tx.run(query, embeddings=embeddings)
|
# """
|
||||||
|
# tx.run(query, triples=triples)
|
||||||
with self.driver.session() as session:
|
#
|
||||||
session.execute_write(batch_create, triples)
|
# def batch_add_embeddings(tx, embeddings):
|
||||||
|
# query = """
|
||||||
# 获取embedding并批量添加
|
# UNWIND $embeddings AS embedding
|
||||||
embeddings = []
|
# MATCH (e:Entity {name: embedding.name})
|
||||||
for triple in triples:
|
# SET e.embedding = embedding.vector
|
||||||
h = triple['h']
|
# """
|
||||||
t = triple['t']
|
# tx.run(query, embeddings=embeddings)
|
||||||
embedding_h = self.get_embedding(h)
|
#
|
||||||
embedding_t = self.get_embedding(t)
|
# with self.driver.session() as session:
|
||||||
embeddings.append({"name": h, "vector": embedding_h})
|
# session.execute_write(batch_create, triples)
|
||||||
embeddings.append({"name": t, "vector": embedding_t})
|
#
|
||||||
|
# # 获取embedding并批量添加
|
||||||
session.execute_write(batch_add_embeddings, embeddings)
|
# embeddings = []
|
||||||
|
# for triple in triples:
|
||||||
|
# h = triple['h']
|
||||||
|
# t = triple['t']
|
||||||
|
# embedding_h = self.get_embedding(h)
|
||||||
|
# embedding_t = self.get_embedding(t)
|
||||||
|
# embeddings.append({"name": h, "vector": embedding_h})
|
||||||
|
# embeddings.append({"name": t, "vector": embedding_t})
|
||||||
|
#
|
||||||
|
# session.execute_write(batch_add_embeddings, embeddings)
|
||||||
|
|
||||||
self.status = "open"
|
self.status = "open"
|
||||||
return kgdb_name
|
return kgdb_name
|
||||||
@ -260,13 +277,21 @@ class GraphDatabase:
|
|||||||
with self.driver.session() as session:
|
with self.driver.session() as session:
|
||||||
return session.execute_read(query, keyword, hops)
|
return session.execute_read(query, keyword, hops)
|
||||||
|
|
||||||
|
def query_node(self, entity_name, args):
|
||||||
|
# TODO 添加判断节点数量为 0 停止检索
|
||||||
|
|
||||||
|
if args.get("exact_match"):
|
||||||
|
raise NotImplemented("not implement for `exact_match`")
|
||||||
|
else:
|
||||||
|
return self.query_by_vector(entity_name, kgdb_name=args.get("kgdb_name"), hops=args.get("hops"))
|
||||||
|
|
||||||
def query_by_vector_tep(self, keyword, kgdb_name='neo4j'):
|
def query_by_vector_tep(self, keyword, kgdb_name='neo4j'):
|
||||||
"""向量查询"""
|
"""向量查询"""
|
||||||
self.use_database(kgdb_name)
|
self.use_database(kgdb_name)
|
||||||
def query(tx, text):
|
def query(tx, text):
|
||||||
embedding = self.get_embedding(text)
|
embedding = self.get_embedding(text)
|
||||||
result = tx.run("""
|
result = tx.run("""
|
||||||
CALL db.index.vector.queryNodes('entity-embeddings', 10, $embedding)
|
CALL db.index.vector.queryNodes('entityEmbeddings', 10, $embedding)
|
||||||
YIELD node AS similarEntity, score
|
YIELD node AS similarEntity, score
|
||||||
RETURN similarEntity.name AS name, score
|
RETURN similarEntity.name AS name, score
|
||||||
""", embedding=embedding)
|
""", embedding=embedding)
|
||||||
@ -277,7 +302,7 @@ class GraphDatabase:
|
|||||||
with self.driver.session() as session:
|
with self.driver.session() as session:
|
||||||
return session.execute_read(query, keyword)
|
return session.execute_read(query, keyword)
|
||||||
|
|
||||||
def query_by_vector(self, entity_name, threshold=0.9,kgdb_name='neo4j', hops=2, num_of_res=2):
|
def query_by_vector(self, entity_name, threshold=0.9, kgdb_name='neo4j', hops=2, num_of_res=2):
|
||||||
self.use_database(kgdb_name)
|
self.use_database(kgdb_name)
|
||||||
result = self.query_by_vector_tep(entity_name)
|
result = self.query_by_vector_tep(entity_name)
|
||||||
querys = []
|
querys = []
|
||||||
|
|||||||
@ -83,7 +83,7 @@ class Retriever:
|
|||||||
r["file"] = kb.id2file(r["entity"]["file_id"])
|
r["file"] = kb.id2file(r["entity"]["file_id"])
|
||||||
|
|
||||||
if self.config.enable_reranker:
|
if self.config.enable_reranker:
|
||||||
RERANK_THRESHOLD = 0.1
|
RERANK_THRESHOLD = 0.001
|
||||||
for r in kb_res:
|
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([query, r["entity"]["text"]], normalize=True)
|
||||||
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
|
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
|
||||||
@ -124,7 +124,46 @@ class Retriever:
|
|||||||
|
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
|
def foramt_general_results(self, results):
|
||||||
|
logger.debug(f"Formatting general results: {results}")
|
||||||
|
formatted_results = {"nodes": [], "edges": []}
|
||||||
|
|
||||||
|
for item in results:
|
||||||
|
relationship = item[1]
|
||||||
|
rel_id = relationship.element_id
|
||||||
|
nodes = relationship.nodes
|
||||||
|
if len(nodes) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source, target = nodes
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
return formatted_results
|
||||||
|
|
||||||
def format_query_results(self, results):
|
def format_query_results(self, results):
|
||||||
|
logger.debug(f"Formatting query results: {results}")
|
||||||
formatted_results = {"nodes": [], "edges": []}
|
formatted_results = {"nodes": [], "edges": []}
|
||||||
|
|
||||||
node_dict = {}
|
node_dict = {}
|
||||||
|
|||||||
@ -45,10 +45,11 @@ class ZhipuEmbedding:
|
|||||||
self.query_instruction_for_retrieval = "为这个句子生成表示以用于检索相关文章:"
|
self.query_instruction_for_retrieval = "为这个句子生成表示以用于检索相关文章:"
|
||||||
|
|
||||||
def predict(self, message):
|
def predict(self, message):
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for i in range(0, len(message), 10):
|
for i in range(0, len(message), 10):
|
||||||
|
if len(message) > 10:
|
||||||
|
logger.info(f"Encoding {i} to {i+10} with {len(message)} messages")
|
||||||
group_msg = message[i:i+10]
|
group_msg = message[i:i+10]
|
||||||
response = self.client.embeddings.create(
|
response = self.client.embeddings.create(
|
||||||
model=self.model_info.default_path,
|
model=self.model_info.default_path,
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
FlagEmbedding==1.2.10
|
|
||||||
Flask==3.0.3
|
|
||||||
Flask_Cors==4.0.1
|
|
||||||
openai==1.35.10
|
|
||||||
python-dotenv==1.0.1
|
|
||||||
PyYAML==6.0.1
|
|
||||||
@ -123,9 +123,20 @@ def get_graph_node():
|
|||||||
return jsonify({'message': 'entity_name and kgdb_name are required'}), 400
|
return jsonify({'message': 'entity_name and kgdb_name are required'}), 400
|
||||||
|
|
||||||
logger.debug(f"Get graph node {entity_name} in {kgdb_name} with {hops} hops")
|
logger.debug(f"Get graph node {entity_name} in {kgdb_name} with {hops} hops")
|
||||||
result = startup.dbm.graph_base.query_by_vector(entity_name, kgdb_name=kgdb_name, hops=hops)
|
result = startup.dbm.graph_base.query_node(entity_name, request.args)
|
||||||
return jsonify({'result': startup.retriever.format_query_results(result), 'message': 'success'}), 200
|
return jsonify({'result': startup.retriever.format_query_results(result), 'message': 'success'}), 200
|
||||||
|
|
||||||
|
@db.route('/graph/nodes', methods=['GET'])
|
||||||
|
def get_graph_nodes():
|
||||||
|
kgdb_name = request.args.get('kgdb_name')
|
||||||
|
num = request.args.get('num')
|
||||||
|
if not kgdb_name:
|
||||||
|
return jsonify({'message': 'kgdb_name is required'}), 400
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@db.route('/graph/add', methods=['POST'])
|
@db.route('/graph/add', methods=['POST'])
|
||||||
def add_graph_entity():
|
def add_graph_entity():
|
||||||
data = json.loads(request.data)
|
data = json.loads(request.data)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.svg">
|
<link rel="icon" href="/favicon.svg">
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^6.1.0",
|
"@ant-design/icons-vue": "^6.1.0",
|
||||||
"@antv/g6": "^5.0.9",
|
"@antv/g6": "^5.0.17",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"ant-design-vue": "^4.2.3",
|
"ant-design-vue": "^4.2.3",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
--main-100: #ABE0F7;
|
--main-100: #ABE0F7;
|
||||||
--main-50: #CDF5FF;
|
--main-50: #CDF5FF;
|
||||||
--main-25: #E6FAFF;
|
--main-25: #E6FAFF;
|
||||||
|
--main-10: #F5FDFF;
|
||||||
|
|
||||||
--c-white: #ffffff;
|
--c-white: #ffffff;
|
||||||
--c-white-soft: #f8f8f8;
|
--c-white-soft: #f8f8f8;
|
||||||
|
|||||||
@ -3,3 +3,15 @@
|
|||||||
:root {
|
:root {
|
||||||
--header-height: 60px;
|
--header-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* layout */
|
||||||
|
|
||||||
|
.layout-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px 30px;
|
||||||
|
background-color: #FCFEFF;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
class="newchat nav-btn"
|
class="newchat nav-btn"
|
||||||
@click="$emit('newconv')"
|
@click="$emit('newconv')"
|
||||||
>
|
>
|
||||||
<PlusCircleOutlined /> <span class="text">新对话 {{ configStore.config?.model_name }}</span>
|
<PlusCircleOutlined /> <span class="text">新对话:{{ configStore.config?.model_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header__right">
|
<div class="header__right">
|
||||||
@ -107,11 +107,12 @@
|
|||||||
:class="message.role"
|
:class="message.role"
|
||||||
>
|
>
|
||||||
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
|
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
|
||||||
<div v-else-if="message.text.length == 0" class="loading-dots">
|
<div v-else-if="message.text.length == 0 && message.status=='querying'" class="loading-dots">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="message.text.length == 0 || message.status == 'error'" class="err-msg">请求错误,请重试</div>
|
||||||
<p v-else
|
<p v-else
|
||||||
v-html="renderMarkdown(message)"
|
v-html="renderMarkdown(message)"
|
||||||
class="message-md"
|
class="message-md"
|
||||||
@ -182,7 +183,8 @@ const examples = ref([
|
|||||||
'肉碱的分子量是多少?直接回答',
|
'肉碱的分子量是多少?直接回答',
|
||||||
'简述大蒜的功效是什么?',
|
'简述大蒜的功效是什么?',
|
||||||
'A大于B,B小于C,A和C哪个大?',
|
'A大于B,B小于C,A和C哪个大?',
|
||||||
'今天天气怎么样?'
|
'今天天气怎么样?',
|
||||||
|
'吃饭吃出苍蝇可以索赔吗?',
|
||||||
])
|
])
|
||||||
|
|
||||||
const opts = reactive({
|
const opts = reactive({
|
||||||
@ -329,6 +331,7 @@ const updateStatus = (id, status) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
const simpleCall = (message) => {
|
const simpleCall = (message) => {
|
||||||
@ -403,6 +406,11 @@ const sendMessage = () => {
|
|||||||
}
|
}
|
||||||
return readChunk()
|
return readChunk()
|
||||||
})
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
updateStatus(cur_res_id, "error")
|
||||||
|
isStreaming.value = false
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log('请输入消息')
|
console.log('请输入消息')
|
||||||
}
|
}
|
||||||
@ -573,7 +581,7 @@ watch(
|
|||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1rem;
|
padding: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -591,6 +599,15 @@ watch(
|
|||||||
color: black;
|
color: black;
|
||||||
/* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
|
/* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
|
||||||
/* animation: slideInUp 0.1s ease-in; */
|
/* animation: slideInUp 0.1s ease-in; */
|
||||||
|
|
||||||
|
.err-msg {
|
||||||
|
color: red;
|
||||||
|
border: 1px solid red;
|
||||||
|
padding: 0.2rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background: #FFEBEE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-box.sent {
|
.message-box.sent {
|
||||||
@ -623,8 +640,6 @@ watch(
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -773,7 +788,7 @@ button:disabled {
|
|||||||
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
|
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
|
||||||
|
|
||||||
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
|
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
|
||||||
.swing-in-top-fwd {-webkit-animation: swing-in-top-fwd 0.2s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;}
|
.swing-in-top-fwd{-webkit-animation:swing-in-top-fwd .2s ease-out both;animation:swing-in-top-fwd .2s ease-out both}
|
||||||
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}
|
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}
|
||||||
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}
|
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<!-- RefsComponent.vue -->
|
<!-- RefsComponent.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="refs" v-if="showRefs">
|
<div class="refs" v-if="showRefs">
|
||||||
<span class="item"><GlobalOutlined /> {{ message.model_name }}</span>
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
|
<span class="item"><GlobalOutlined /> {{ message.model_name }}</span>
|
||||||
<span class="filetag item"
|
<span class="filetag item"
|
||||||
v-for="(results, filename) in message.groupedResults"
|
v-for="(results, filename) in message.groupedResults"
|
||||||
:key="filename"
|
:key="filename"
|
||||||
@ -28,13 +28,13 @@
|
|||||||
<p class="result-distance">
|
<p class="result-distance">
|
||||||
<strong>相似度距离:</strong>
|
<strong>相似度距离:</strong>
|
||||||
<div class="scorebar">
|
<div class="scorebar">
|
||||||
<a-progress :percent="(res.distance * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
<a-progress :percent="parseFloat((res.distance * 100).toFixed(2))" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
<p class="result-rerank-score">
|
<p class="result-rerank-score" v-if="res.rerank_score">
|
||||||
<strong>重排序分数:</strong>
|
<strong>重排序分数:</strong>
|
||||||
<div class="scorebar">
|
<div class="scorebar">
|
||||||
<a-progress :percent="(res.rerank_score * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
|
<a-progress :percent="parseFloat((res.rerank_score * 100).toFixed(2))" stroke-color="#1677FF" :size="[200, 10]"/>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
<a-divider />
|
<a-divider />
|
||||||
@ -72,9 +72,8 @@ const showRefs = computed(() => message.value.role=='received' && message.value.
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
background: var(--main-25);
|
background: var(--main-10);
|
||||||
color: var(--main-800);
|
color: var(--main-600);
|
||||||
border: 1px solid var(--main-100);
|
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -82,6 +81,7 @@ const showRefs = computed(() => message.value.role=='received' && message.value.
|
|||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
.filetag {
|
.filetag {
|
||||||
@ -91,7 +91,7 @@ const showRefs = computed(() => message.value.role=='received' && message.value.
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--main-100);
|
background: var(--main-25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import {
|
|||||||
GithubOutlined,
|
GithubOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
DatabaseFilled,
|
DatabaseFilled,
|
||||||
|
GoldOutlined,
|
||||||
|
GoldFilled,
|
||||||
|
BugOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { themeConfig } from '@/assets/theme'
|
import { themeConfig } from '@/assets/theme'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
@ -52,7 +55,7 @@ console.log(route)
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
<div class="debug-panel">
|
<div class="debug-panel">
|
||||||
<div class="shown-btn" @click="showDebug=!showDebug">Debug</div>
|
<div class="shown-btn" @click="showDebug=!showDebug"><BugOutlined /></div>
|
||||||
<a-drawer
|
<a-drawer
|
||||||
v-model:open="showDebug"
|
v-model:open="showDebug"
|
||||||
title="调试面板"
|
title="调试面板"
|
||||||
@ -74,6 +77,9 @@ console.log(route)
|
|||||||
<RouterLink to="/database" class="nav-item" active-class="active">
|
<RouterLink to="/database" class="nav-item" active-class="active">
|
||||||
<component class="icon" :is="route.path.startsWith('/database') ? DatabaseFilled : DatabaseOutlined" />
|
<component class="icon" :is="route.path.startsWith('/database') ? DatabaseFilled : DatabaseOutlined" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink to="/graph" class="nav-item" active-class="active">
|
||||||
|
<component class="icon" :is="route.path.startsWith('/graph') ? GoldFilled: GoldOutlined" />
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="fill" style="flex-grow: 1;"></div>
|
<div class="fill" style="flex-grow: 1;"></div>
|
||||||
<div class="github nav-item">
|
<div class="github nav-item">
|
||||||
@ -118,11 +124,13 @@ console.log(route)
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 50px;
|
bottom: 50px;
|
||||||
border-radius: 16px 0 0 16px;
|
border-radius: 20px 0 0 20px;
|
||||||
background-color: var(--main-light-3);
|
background-color: var(--main-light-4);
|
||||||
padding: 8px 8px 8px 16px;
|
padding: 6px;
|
||||||
box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.1);
|
padding-left: 12px;
|
||||||
|
box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid var(--c-black-soft);
|
||||||
transition: right 0.3s ease-in-out;
|
transition: right 0.3s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -181,11 +189,11 @@ div.header, #app-router-view {
|
|||||||
&.active {
|
&.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--main-600);
|
color: var(--main-600);
|
||||||
background-color: #E6E8E9;
|
background-color: rgba( 0, 93, 125, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #E6E8E9;
|
background-color: rgba( 0, 93, 125, 0.1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,19 @@ const router = createRouter({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/graph',
|
||||||
|
name: 'graph',
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Graph',
|
||||||
|
component: () => import('../views/GraphView.vue'),
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/database',
|
path: '/database',
|
||||||
name: 'database',
|
name: 'database',
|
||||||
@ -45,12 +58,6 @@ const router = createRouter({
|
|||||||
path: ':database_id',
|
path: ':database_id',
|
||||||
name: 'databaseInfo',
|
name: 'databaseInfo',
|
||||||
component: () => import('../views/DataBaseInfoView.vue'),
|
component: () => import('../views/DataBaseInfoView.vue'),
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'graph',
|
|
||||||
name: 'graph',
|
|
||||||
component: () => import('../views/GraphView.vue'),
|
|
||||||
meta: { keepAlive: true }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div class="conversations" :class="['conversations', { 'is-open': state.isSidebarOpen }]">
|
<div class="conversations" :class="['conversations', { 'is-open': state.isSidebarOpen }]">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
|
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
|
||||||
<span style="font-weight: bold;">对话历史</span>
|
<span style="font-weight: bold; user-select: none;">对话历史</span>
|
||||||
<div class="action close" @click="state.isSidebarOpen = false"><MenuOutlined /></div>
|
<div class="action close" @click="state.isSidebarOpen = false"><MenuOutlined /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<a-button type="text" danger class="del-db" @click="deleteDatabse"><DeleteOutlined /></a-button>
|
<a-button type="text" danger class="del-db" @click="deleteDatabse"><DeleteOutlined /></a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div class="icon"><ReadFilled /></div>
|
<!-- <div class="icon"><ReadFilled /></div> -->
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h3>{{ database.name }}</h3>
|
<h3>{{ database.name }}</h3>
|
||||||
<p><span>{{ database.metadata?.row_count }}行 · {{ database.files?.length || 0 }}文件</span></p>
|
<p><span>{{ database.metadata?.row_count }}行 · {{ database.files?.length || 0 }}文件</span></p>
|
||||||
@ -15,17 +15,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="description">{{ database.description }}</p>
|
<p class="description">{{ database.description }}</p>
|
||||||
<div class="tags">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<a-divider/>
|
<a-divider/>
|
||||||
<div class="pagebtns">
|
|
||||||
<div @click="state.curPage='add'" :class="{ 'active': state.curPage === 'add' }">
|
|
||||||
<CloudUploadOutlined />添加文件
|
|
||||||
</div>
|
|
||||||
<div @click="state.curPage='query-test'" :class="{ 'active': state.curPage === 'query-test' }">
|
|
||||||
<SearchOutlined />检索测试
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="query-params" v-if="state.curPage == 'query-test'">
|
<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">
|
<div class="params-item">
|
||||||
@ -46,103 +38,113 @@
|
|||||||
<div class="sider-bottom">
|
<div class="sider-bottom">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="db-info-container" v-if="state.curPage == 'add'">
|
<a-tabs v-model:activeKey="state.curPage" class="atab-container" type="card">
|
||||||
<h3>向知识库中添加文件</h3>
|
<a-tab-pane key="add">
|
||||||
<div class="upload">
|
<template #tab><span><CloudUploadOutlined />添加文件</span></template>
|
||||||
<a-upload-dragger
|
<div class="db-info-container">
|
||||||
class="upload-dragger"
|
<h3>向知识库中添加文件</h3>
|
||||||
v-model:fileList="fileList"
|
<div class="upload">
|
||||||
name="file"
|
<a-upload-dragger
|
||||||
:multiple="true"
|
class="upload-dragger"
|
||||||
:disabled="state.loading"
|
v-model:fileList="fileList"
|
||||||
action="/api/database/upload"
|
name="file"
|
||||||
@change="handleFileUpload"
|
:multiple="true"
|
||||||
@drop="handleDrop"
|
:disabled="state.loading"
|
||||||
>
|
action="/api/database/upload"
|
||||||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
@change="handleFileUpload"
|
||||||
<p class="ant-upload-hint">
|
@drop="handleDrop"
|
||||||
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加。
|
>
|
||||||
</p>
|
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||||||
</a-upload-dragger>
|
<p class="ant-upload-hint">
|
||||||
</div>
|
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加。
|
||||||
<a-button
|
</p>
|
||||||
type="primary"
|
</a-upload-dragger>
|
||||||
@click="addDocumentByFile"
|
</div>
|
||||||
:loading="state.loading"
|
<a-button
|
||||||
:disabled="fileList.length === 0"
|
type="primary"
|
||||||
style="margin: 0px 20px 20px 0;"
|
@click="addDocumentByFile"
|
||||||
>
|
:loading="state.loading"
|
||||||
添加到知识库
|
:disabled="fileList.length === 0"
|
||||||
</a-button>
|
style="margin: 0px 20px 20px 0;"
|
||||||
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
|
>
|
||||||
<a-table :columns="columns" :data-source="database.files" row-key="file_id" class="my-table">
|
添加到知识库
|
||||||
<template #bodyCell="{ column, text, record }">
|
</a-button>
|
||||||
<template v-if="column.key === 'file_id'">
|
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
|
||||||
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text.toUpperCase() }}</a-button>
|
<a-table :columns="columns" :data-source="database.files" row-key="file_id" class="my-table">
|
||||||
</template>
|
<template #bodyCell="{ column, text, record }">
|
||||||
<template v-else-if="column.key === 'type'"><span :class="text">{{ text.toUpperCase() }}</span></template>
|
<template v-if="column.key === 'file_id'">
|
||||||
<template v-else-if="column.key === 'status' && text === 'done'">
|
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text.toUpperCase() }}</a-button>
|
||||||
<CheckCircleFilled style="color: #41A317;"/>
|
</template>
|
||||||
</template>
|
<template v-else-if="column.key === 'type'"><span :class="text">{{ text.toUpperCase() }}</span></template>
|
||||||
<template v-else-if="column.key === 'status' && text === 'failed'">
|
<template v-else-if="column.key === 'status' && text === 'done'">
|
||||||
<CloseCircleFilled style="color: #FF4D4F ;"/>
|
<CheckCircleFilled style="color: #41A317;"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status' && text === 'processing'">
|
<template v-else-if="column.key === 'status' && text === 'failed'">
|
||||||
<HourglassFilled style="color: #1677FF;"/>
|
<CloseCircleFilled style="color: #FF4D4F ;"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status' && text === 'waiting'">
|
<template v-else-if="column.key === 'status' && text === 'processing'">
|
||||||
<ClockCircleFilled style="color: #FFCD43;"/>
|
<HourglassFilled style="color: #1677FF;"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'status' && text === 'waiting'">
|
||||||
<a-button class="del-btn" type="link"
|
<ClockCircleFilled style="color: #FFCD43;"/>
|
||||||
@click="deleteFile(text)"
|
</template>
|
||||||
:disabled="state.lock || record.status == 'processing' || record.status == 'waiting' "
|
<template v-else-if="column.key === 'action'">
|
||||||
>删除
|
<a-button class="del-btn" type="link"
|
||||||
</a-button>
|
@click="deleteFile(text)"
|
||||||
</template>
|
:disabled="state.lock || record.status == 'processing' || record.status == 'waiting' "
|
||||||
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
|
>删除
|
||||||
<span v-else>{{ text }}</span>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
|
||||||
<a-drawer
|
<span v-else>{{ text }}</span>
|
||||||
width="50%"
|
</template>
|
||||||
v-model:open="state.drawer"
|
</a-table>
|
||||||
class="custom-class"
|
<a-drawer
|
||||||
:title="selectedFile?.filename"
|
width="50%"
|
||||||
placement="right"
|
v-model:open="state.drawer"
|
||||||
@after-open-change="afterOpenChange"
|
class="custom-class"
|
||||||
>
|
:title="selectedFile?.filename"
|
||||||
<h2>共 {{ selectedFile?.lines.length }} 个片段</h2>
|
placement="right"
|
||||||
<p v-for="line in selectedFile?.lines" :key="line.id">
|
@after-open-change="afterOpenChange"
|
||||||
<strong>Chunk #{{ line.id }}</strong> {{ line.text }}
|
>
|
||||||
</p>
|
<h2>共 {{ selectedFile?.lines.length }} 个片段</h2>
|
||||||
</a-drawer>
|
<p v-for="line in selectedFile?.lines" :key="line.id">
|
||||||
</div>
|
<strong>Chunk #{{ line.id }}</strong> {{ line.text }}
|
||||||
<div class="db-info-container" v-else-if="state.curPage == 'query-test'">
|
</p>
|
||||||
<h3>检索测试</h3>
|
</a-drawer>
|
||||||
<div class="query-action">
|
|
||||||
<a-textarea
|
|
||||||
v-model:value="queryText"
|
|
||||||
placeholder="填写需要查询的句子"
|
|
||||||
:auto-size="{ minRows: 2, maxRows: 10 }"
|
|
||||||
/>
|
|
||||||
<!-- :loading="state.searchLoading" -->
|
|
||||||
<a-button @click="onQuery" :disabled="queryText.length == 0" :loading="state.searchLoading">
|
|
||||||
<SearchOutlined v-if="!state.searchLoading"/>检索
|
|
||||||
</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">
|
|
||||||
<p>
|
|
||||||
<strong>#{{ idx + 1 }} </strong>
|
|
||||||
<span>{{ result.file.filename }} </span>
|
|
||||||
<span><strong>距离</strong>:{{ result.distance.toFixed(4) }} </span>
|
|
||||||
<span v-if="result.rerank_score"><strong>重排序</strong>:{{ result.rerank_score.toFixed(4) }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="query-text">{{ result.entity.text }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a-tab-pane>
|
||||||
</div>
|
<a-tab-pane key="query-test" force-render>
|
||||||
|
<template #tab><span><SearchOutlined />检索测试</span></template>
|
||||||
|
<div class="db-info-container">
|
||||||
|
<h3>检索测试</h3>
|
||||||
|
<div class="query-action">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="queryText"
|
||||||
|
placeholder="填写需要查询的句子"
|
||||||
|
:auto-size="{ minRows: 2, maxRows: 10 }"
|
||||||
|
/>
|
||||||
|
<!-- :loading="state.searchLoading" -->
|
||||||
|
<a-button class="btn-query" @click="onQuery" :disabled="queryText.length == 0">
|
||||||
|
<span v-if="!state.searchLoading"><SearchOutlined /> 检索</span>
|
||||||
|
<span v-else><LoadingOutlined /></span>
|
||||||
|
</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">
|
||||||
|
<p>
|
||||||
|
<strong>#{{ idx + 1 }} </strong>
|
||||||
|
<span>{{ result.file.filename }} </span>
|
||||||
|
<span><strong>距离</strong>:{{ result.distance.toFixed(4) }} </span>
|
||||||
|
<span v-if="result.rerank_score"><strong>重排序</strong>:{{ result.rerank_score.toFixed(4) }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="query-text">{{ result.entity.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
<!-- <a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane> -->
|
||||||
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -160,6 +162,7 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
CloudUploadOutlined,
|
CloudUploadOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
|
LoadingOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
@ -476,8 +479,7 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-top: 10px;
|
padding: 10px;
|
||||||
padding-bottom: 10px;
|
|
||||||
background-color: var(--main-light-5);
|
background-color: var(--main-light-5);
|
||||||
border-bottom: 1px solid #E0EAFF;
|
border-bottom: 1px solid #E0EAFF;
|
||||||
|
|
||||||
@ -485,42 +487,44 @@ onMounted(() => {
|
|||||||
height: auto;
|
height: auto;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--c-text-light-1);
|
color: var(--c-text-light-1);
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagebtns {
|
// .pagebtns {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
gap: 16px;
|
// gap: 16px;
|
||||||
|
|
||||||
> div {
|
// > div {
|
||||||
gap: 1rem;
|
// gap: 1rem;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
display: flex;
|
// display: flex;
|
||||||
justify-content: center;
|
// justify-content: center;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
padding: 10px 16px;
|
// padding: 10px 16px;
|
||||||
height: auto;
|
// height: auto;
|
||||||
border-radius: 4px;
|
// border-radius: 4px;
|
||||||
border: none;
|
// border: none;
|
||||||
background: var(--main-light-5);
|
// background: var(--main-light-5);
|
||||||
letter-spacing: 4px;
|
// letter-spacing: 4px;
|
||||||
border-radius: 8px;
|
// border-radius: 8px;
|
||||||
border: 1px solid var(--main-light-2);
|
// border: 1px solid var(--main-light-2);
|
||||||
|
|
||||||
&:hover {
|
// &:hover {
|
||||||
cursor: pointer;
|
// cursor: pointer;
|
||||||
background: var(--main-light-3);
|
// background: var(--main-light-3);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
.active {
|
// .active {
|
||||||
color: var(--main-color);
|
// color: var(--main-color);
|
||||||
background: var(--main-light-3);
|
// background: var(--main-light-3);
|
||||||
font-weight: bold;
|
// font-weight: bold;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
.query-params {
|
.query-params {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -548,10 +552,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.atab-container {
|
||||||
|
padding: 12px 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.db-info-container {
|
.db-info-container {
|
||||||
padding: 20px;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: scroll;
|
|
||||||
|
|
||||||
.query-action {
|
.query-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -563,13 +570,13 @@ onMounted(() => {
|
|||||||
border: 1px solid var(--main-light-2);
|
border: 1px solid var(--main-light-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button.btn-query {
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 120px;
|
width: 100px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: var(--main-light-2);
|
background: var(--main-light-3);
|
||||||
color: var(--main-color);
|
color: var(--main-color);
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@ -626,6 +633,24 @@ onMounted(() => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--main-color);
|
||||||
|
display: block;
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: var(--c-text-light-1);
|
color: var(--c-text-light-1);
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="database-container" v-if="configStore.config.enable_knowledge_base">
|
<div class="database-container layout-container" v-if="configStore.config.enable_knowledge_base">
|
||||||
<h2>文档知识库</h2>
|
<h2>文档知识库</h2>
|
||||||
<p>知识型数据库,主要是非结构化的文本组成,使用向量检索使用。</p>
|
<p>知识型数据库,主要是非结构化的文本组成,使用向量检索使用。</p>
|
||||||
<a-modal :open="newDatabase.open" title="新建数据库" @ok="createDatabase">
|
<a-modal :open="newDatabase.open" title="新建数据库" @ok="createDatabase">
|
||||||
@ -49,7 +49,7 @@
|
|||||||
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
|
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>图数据库 <a-spin v-if="graphloading" :indicator="indicator" /></h2>
|
<!-- <h2>图数据库 <a-spin v-if="graphloading" :indicator="indicator" /></h2>
|
||||||
<p>基于 neo4j 构建的图数据库。</p>
|
<p>基于 neo4j 构建的图数据库。</p>
|
||||||
<div :class="{'graphloading': graphloading, 'databases': true}" v-if="graph">
|
<div :class="{'graphloading': graphloading, 'databases': true}" v-if="graph">
|
||||||
<div class="dbcard graphbase" @click="navigateToGraph">
|
<div class="dbcard graphbase" @click="navigateToGraph">
|
||||||
@ -64,9 +64,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="description">基于 neo4j 构建的图数据库。基于 neo4j 构建的图数据库。基于 neo4j 构建的图数据库。</p>
|
<p class="description">基于 neo4j 构建的图数据库。基于 neo4j 构建的图数据库。基于 neo4j 构建的图数据库。</p>
|
||||||
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="database-empty" v-else>
|
<div class="database-empty" v-else>
|
||||||
<a-empty>
|
<a-empty>
|
||||||
@ -103,7 +102,7 @@ const newDatabase = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loadDatabases = () => {
|
const loadDatabases = () => {
|
||||||
loadGraph()
|
// loadGraph()
|
||||||
fetch('/api/database/', {
|
fetch('/api/database/', {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
})
|
})
|
||||||
@ -154,23 +153,23 @@ const navigateToGraph = () => {
|
|||||||
router.push({ path: `/database/graph` });
|
router.push({ path: `/database/graph` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadGraph = () => {
|
// const loadGraph = () => {
|
||||||
graphloading.value = true
|
// graphloading.value = true
|
||||||
fetch('/api/database/graph', {
|
// fetch('/api/database/graph', {
|
||||||
method: "GET",
|
// method: "GET",
|
||||||
})
|
// })
|
||||||
.then(response => response.json())
|
// .then(response => response.json())
|
||||||
.then(data => {
|
// .then(data => {
|
||||||
console.log(data)
|
// console.log(data)
|
||||||
graph.value = data.graph
|
// graph.value = data.graph
|
||||||
graphloading.value = false
|
// graphloading.value = false
|
||||||
})
|
// })
|
||||||
.catch(error => {
|
// .catch(error => {
|
||||||
console.error(error)
|
// console.error(error)
|
||||||
message.error(error.message)
|
// message.error(error.message)
|
||||||
graphloading.value = false
|
// graphloading.value = false
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
watch(() => route.path, (newPath, oldPath) => {
|
watch(() => route.path, (newPath, oldPath) => {
|
||||||
if (newPath === '/database') {
|
if (newPath === '/database') {
|
||||||
@ -185,14 +184,6 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.database-container {
|
|
||||||
padding: 10px 30px;
|
|
||||||
background-color: #FCFEFF;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 20px 0 10px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.database-actions, .document-actions {
|
.database-actions, .document-actions {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -278,10 +269,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 整个卡片是模糊的
|
// 整个卡片是模糊的
|
||||||
.graphloading {
|
// .graphloading {
|
||||||
filter: blur(2px);
|
// filter: blur(2px);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
.database-empty {
|
.database-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -1,9 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="graph-container">
|
<div class="database-empty" v-if="!state.showPage">
|
||||||
|
<a-empty>
|
||||||
|
<template #description>
|
||||||
|
<span>
|
||||||
|
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link> 页面配置图数据库。
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</a-empty>
|
||||||
|
</div>
|
||||||
|
<div class="graph-container layout-container" v-else>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h1>Neo4j 图数据库</h1>
|
<h2>图数据库 {{ graph?.database_name }}</h2>
|
||||||
<p>基于 Neo4j 构建的图数据库。</p>
|
<p>
|
||||||
</div>
|
<span v-if="state.graphloading">加载中</span>
|
||||||
|
<span class="green-dot" v-if="graph?.status == 'open'"></span>
|
||||||
|
<span class="red-dot" v-else></span>
|
||||||
|
<span>{{ graph?.status }}</span> ·
|
||||||
|
<span>共 {{ graph?.entity_count }} 实体,{{ graph?.relationship_count }} 个关系</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="actions-left">
|
<div class="actions-left">
|
||||||
<a-button @click="state.showModal = true">上传文件</a-button>
|
<a-button @click="state.showModal = true">上传文件</a-button>
|
||||||
@ -32,6 +47,8 @@
|
|||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
<input v-model="sampleNodeCount">
|
||||||
|
<a-button @click="loadSampleNodes">确定</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-right">
|
<div class="action-right">
|
||||||
<input
|
<input
|
||||||
@ -49,8 +66,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main" id="container"></div>
|
<div class="main" id="container" ref="container"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -58,34 +74,47 @@
|
|||||||
import { Graph } from "@antv/g6";
|
import { Graph } from "@antv/g6";
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
|
import { useConfigStore } from '@/stores/config';
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
let graphInstance
|
let graphInstance
|
||||||
|
const graph = ref(null)
|
||||||
|
const container = ref(null);
|
||||||
const fileList = ref([]);
|
const fileList = ref([]);
|
||||||
|
const sampleNodeCount = ref(100);
|
||||||
const subgraph = reactive({
|
const subgraph = reactive({
|
||||||
nodes: [
|
nodes: [],
|
||||||
{ id: '1', name: 'node1' },
|
edges: [],
|
||||||
{ id: '2', name: 'node2' },
|
|
||||||
{ id: '3', name: 'node3' },
|
|
||||||
{ id: '4', name: 'node4' },
|
|
||||||
{ id: '5', name: 'node5' },
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{ id: 'e1', source_id: '1', target_id: '2', type: 'edge1' },
|
|
||||||
{ id: 'e2', source_id: '1', target_id: '3', type: 'edge2' },
|
|
||||||
{ id: 'e3', source_id: '2', target_id: '4', type: 'edge3' },
|
|
||||||
{ id: 'e4', source_id: '2', target_id: '5', type: 'edge4' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
graphloading: false,
|
||||||
searchInput: '',
|
searchInput: '',
|
||||||
searchLoading: false,
|
searchLoading: false,
|
||||||
showModal: false,
|
showModal: false,
|
||||||
precessing: false,
|
precessing: false,
|
||||||
|
showPage: computed(() => configStore.config.enable_knowledge_base && configStore.config.enable_knowledge_graph),
|
||||||
})
|
})
|
||||||
|
|
||||||
const getCurWidth = () => document.getElementById("container").offsetWidth
|
|
||||||
const getCurHeight = () => document.getElementById("container").offsetHeight
|
const loadGraph = () => {
|
||||||
|
state.graphloading = true
|
||||||
|
fetch('/api/database/graph', {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log(data)
|
||||||
|
graph.value = data.graph
|
||||||
|
state.graphloading = false
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
message.error(error.message)
|
||||||
|
state.graphloading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const graphData = computed(() => {
|
const graphData = computed(() => {
|
||||||
return {
|
return {
|
||||||
@ -129,6 +158,29 @@ const addDocumentByFile = () => {
|
|||||||
.finally(() => state.precessing = false)
|
.finally(() => state.precessing = false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSampleNodes = () => {
|
||||||
|
fetch(`/api/database/graph/nodes?kgdb_name=neo4j&num=${sampleNodeCount.value}`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("加载失败");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
subgraph.nodes = data.result.nodes
|
||||||
|
subgraph.edges = data.result.edges
|
||||||
|
console.log(data)
|
||||||
|
console.log(subgraph)
|
||||||
|
setTimeout(() => {
|
||||||
|
randerGraph()
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
message.error(error.message);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onSearch = () => {
|
const onSearch = () => {
|
||||||
if (!state.searchInput) {
|
if (!state.searchInput) {
|
||||||
message.error('请输入要查询的实体')
|
message.error('请输入要查询的实体')
|
||||||
@ -162,44 +214,50 @@ const randerGraph = () => {
|
|||||||
graphInstance.render();
|
graphInstance.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
graphInstance = new Graph({
|
loadGraph();
|
||||||
container: document.getElementById("container"),
|
loadSampleNodes();
|
||||||
width: getCurWidth(),
|
setTimeout(() => {
|
||||||
height: getCurHeight(),
|
if (state.showPage) {
|
||||||
autoFit: true,
|
graphInstance = new Graph({
|
||||||
autoResize: true,
|
container: container.value,
|
||||||
layout: {
|
width: container.value.offsetWidth,
|
||||||
type: 'force-atlas2',
|
height: container.value.offsetHeight,
|
||||||
preventOverlap: true,
|
autoFit: true,
|
||||||
kr: 100,
|
autoResize: true,
|
||||||
},
|
layout: {
|
||||||
node: {
|
type: 'd3-force',
|
||||||
type: 'circle',
|
preventOverlap: true,
|
||||||
style: {
|
kr: 100,
|
||||||
labelText: (d) => d.data.label,
|
collide: {
|
||||||
size: 40,
|
strength: 0.5,
|
||||||
},
|
},
|
||||||
palette: {
|
},
|
||||||
field: 'label',
|
node: {
|
||||||
color: 'tableau',
|
type: 'circle',
|
||||||
},
|
style: {
|
||||||
},
|
labelText: (d) => d.data.label,
|
||||||
edge: {
|
size: 40,
|
||||||
type: 'line',
|
},
|
||||||
style: {
|
palette: {
|
||||||
labelText: (d) => d.data.label,
|
field: 'label',
|
||||||
labelBackground: '#fff',
|
color: 'tableau',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
behaviors: ['drag-element'],
|
edge: {
|
||||||
});
|
type: 'line',
|
||||||
graphInstance.setData(graphData.value);
|
style: {
|
||||||
graphInstance.render();
|
labelText: (d) => d.data.label,
|
||||||
window.addEventListener('resize', randerGraph);
|
labelBackground: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
|
||||||
|
});
|
||||||
|
graphInstance.setData(graphData.value);
|
||||||
|
graphInstance.render();
|
||||||
|
window.addEventListener('resize', randerGraph);
|
||||||
|
}
|
||||||
|
}, 400)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -217,19 +275,46 @@ const handleDrop = (event) => {
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.graph-container {
|
.graph-container {
|
||||||
padding: 20px;
|
|
||||||
|
.info span.green-dot, .info span.red-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info span.green-dot {
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info span.red-dot {
|
||||||
|
background: #f5222d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.actions-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
width: 100px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border: 2px solid #d9d9d9;
|
border: 2px solid var(--main-300);
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
|
|
||||||
@ -253,17 +338,22 @@ const handleDrop = (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
background: #F7F7F7;
|
background: #F7F7F7;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 200px);
|
height: 800px;
|
||||||
|
resize: horizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.database-empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--c-text-light-1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-container">
|
<div class="setting-container layout-container">
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<h2>设置</h2>
|
<h2>设置</h2>
|
||||||
<h3>模型配置</h3>
|
<h3>模型配置</h3>
|
||||||
@ -88,13 +88,13 @@
|
|||||||
@change="handleChange('enable_knowledge_base', !configStore.config.enable_knowledge_base)"
|
@change="handleChange('enable_knowledge_base', !configStore.config.enable_knowledge_base)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="card">
|
<div class="card">
|
||||||
<span class="label">{{ items?.enable_knowledge_graph.des }}</span>
|
<span class="label">{{ items?.enable_knowledge_graph.des }}</span>
|
||||||
<a-switch
|
<a-switch
|
||||||
:checked="configStore.config.enable_knowledge_graph"
|
:checked="configStore.config.enable_knowledge_graph"
|
||||||
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
|
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
|
||||||
/>
|
/>
|
||||||
</div> -->
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="label">{{ items?.enable_search_engine.des }}</span>
|
<span class="label">{{ items?.enable_search_engine.des }}</span>
|
||||||
<a-switch
|
<a-switch
|
||||||
@ -136,6 +136,16 @@ const state = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (key, e) => {
|
const handleChange = (key, e) => {
|
||||||
|
if (key == 'enable_knowledge_graph' && e && !configStore.config.enable_knowledge_base) {
|
||||||
|
message.error('启动知识图谱必须请先启用知识库功能')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'enable_knowledge_base' && !e && configStore.config.enable_knowledge_graph) {
|
||||||
|
message.error('关闭知识库功能必须请先关闭知识图谱功能')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Change', key, e)
|
console.log('Change', key, e)
|
||||||
needRestart[key] = true
|
needRestart[key] = true
|
||||||
configStore.setConfigValue(key, e)
|
configStore.setConfigValue(key, e)
|
||||||
@ -156,10 +166,6 @@ const sendRestart = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.setting-container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting {
|
.setting {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user