graph support

This commit is contained in:
Wenjie Zhang 2024-09-18 21:03:12 +08:00
parent ce9faffb8c
commit b6abb2c8e7
8 changed files with 401 additions and 184 deletions

View File

@ -268,18 +268,18 @@ class GraphDatabase:
return self.query_by_vector(entity_name=entity_name, **kwargs)
def query_by_vector(self, entity_name, threshold=0.9, kgdb_name='neo4j', hops=2, num_of_res=5):
result = self.query_by_vector_tep(entity_name=entity_name)
querys = []
for i in range(num_of_res):
if result[i][1] > threshold:
querys.append(result[i][0])
else:
break
ans = []
for query in querys:
tep = self.query_specific_entity(entity_name=query, hops=hops) # 这里是只获取第一个 TODO: 优化
ans.extend(tep)
return ans
results = self.query_by_vector_tep(entity_name=entity_name)
# 筛选出分数高于阈值的实体
qualified_entities = [result[0] for result in results[:num_of_res] if result[1] > threshold]
# 对每个合格的实体进行查询
all_query_results = []
for entity in qualified_entities:
query_result = self.query_specific_entity(entity_name=entity, hops=hops, kgdb_name=kgdb_name)
all_query_results.extend(query_result)
return all_query_results
def query_by_vector_tep(self, entity_name, kgdb_name='neo4j'):
"""向量查询"""

View File

@ -25,27 +25,28 @@ class Retriever:
return refs
def construct_query(self, query, refs, meta):
if len(refs) == 0:
if not refs or len(refs) == 0:
return query
external = ""
external_parts = []
# 解析知识库的结果
kb_res = refs.get("knowledge_base").get("results", [])
if len(kb_res) > 0:
kb_text = "\n".join([f"{r['id']}: {r['entity']['text']}" for r in kb_res])
external += f"知识库信息: \n\n{kb_text}"
kb_res = refs.get("knowledge_base", {}).get("results", [])
if kb_res:
kb_text = "\n".join(f"{r['id']}: {r['entity']['text']}" for r in kb_res)
external_parts.extend(["知识库信息:", kb_text])
# 解析图数据库的结果
db_res = refs.get("graph_base").get("results", [])
if len(db_res["nodes"]) > 0:
db_res = refs.get("graph_base", {}).get("results", {})
if db_res.get("nodes") and len(db_res["nodes"]) > 0:
db_text = "\n".join(
[f"{edge['source_name']}{edge['target_name']}的关系是{edge['type']}" for edge in db_res["edges"]]
[f"{edge['source_name']}{edge['target_name']}的关系是{edge['type']}" for edge in db_res.get("edges", [])]
)
external += f"图数据库信息: \n\n{db_text}"
external_parts.extend(["图数据库信息:", db_text])
# 构造查询
if len(external) > 0:
if external_parts and len(external_parts) > 0:
external = "\n\n".join(external_parts)
query = f"参考资料:\n\n\n{external}\n\n\n请根据前面的知识回答问题。\n\n问题:{query}\n\n回答:"
return query
@ -73,7 +74,9 @@ class Retriever:
kb_res = []
final_res = []
if not refs["meta"].get("db_name") or not self.config.enable_knowledge_base:
db_name = refs["meta"].get("db_name")
if not db_name or not self.config.enable_knowledge_base:
return {
"results": final_res,
"all_results": kb_res,
@ -83,21 +86,21 @@ class Retriever:
rw_query = self.rewrite_query(query, history, refs)
db_name = refs["meta"]["db_name"]
kb = self.dbm.metaname2db[db_name]
logger.debug(f"{refs['meta']=}")
max_query_count = refs["meta"].get("maxQueryCount", 10)
rerank_threshold = refs["meta"].get("rerankThreshold", 0.1)
distance_threshold = refs["meta"].get("distanceThreshold", 0)
top_k = refs["meta"].get("topK", 5)
meta = refs["meta"]
max_query_count = meta.get("maxQueryCount", 10)
rerank_threshold = meta.get("rerankThreshold", 0.1)
distance_threshold = meta.get("distanceThreshold", 0)
top_k = meta.get("topK", 5)
all_kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=max_query_count)
for r in all_kb_res:
r["file"] = kb.id2file(r["entity"]["file_id"])
# use distance threshold to filter results
if refs["meta"].get("mode") == "search":
if meta.get("mode") == "search":
kb_res = all_kb_res
else:
kb_res = [r for r in all_kb_res if r["distance"] > distance_threshold]
@ -136,11 +139,12 @@ class Retriever:
entities = []
if refs["meta"].get("use_graph"):
from src.utils.prompts import entity_extraction_prompt_template
from src.utils.prompts import entity_extraction_prompt_template as entity_template
from src.utils.prompts import keywords_prompt_template as entity_template
entity_extraction_prompt = entity_extraction_prompt_template.format(text=query)
entities = self.model.predict(entity_extraction_prompt).content.split(",")
entities = [entity for entity in entities if all(char.isalnum() or char in "汉字" for char in entity)]
entity_extraction_prompt = entity_template.format(text=query)
entities = self.model.predict(entity_extraction_prompt).content.split("<->")
# entities = [entity for entity in entities if all(char.isalnum() or char in "汉字" for char in entity)]
return entities
@ -202,7 +206,7 @@ class Retriever:
node_dict = {}
for item in results:
if not isinstance(item[1], list) or len(item[1]) == 0:
if not isinstance(item[1], list) or not item[1]:
continue
relationship = item[1][0]
@ -213,10 +217,7 @@ class Retriever:
if node_info is None or edge_info is None:
continue
for node in node_info:
if node["id"] not in node_dict:
node_dict[node["id"]] = node
node_dict.update({node["id"]: node for node in node_info})
formatted_results["edges"].append(edge_info)
formatted_results["nodes"] = list(node_dict.values())

View File

@ -30,4 +30,12 @@ entity_extraction_prompt_template = """
3.只返回实体不得返回其他任何内容
4.返回的实体用逗号隔开<内容要求>
<文本>{text}</文本>
"""
keywords_prompt_template = """
你是用来辅助查询的助手请对以下文本进行关键词提取返回提取出的关键词
关键词是用来从知识图谱中检索到有用的信息所以关键词必须具有明确的意义即当用户使用这些关键词进行查询时能够从知识图谱中检索到有用的信息
返回的实体使用<->隔开关键词1<->关键词<->关键词3
不要改变关键词的语言
<文本>{text}</文本>
"""

View File

@ -572,19 +572,19 @@ watch(
gap: 10px;
.opt__button {
background-color: white;
color: #222;
padding: 6px 1rem;
border-radius: 1rem;
background-color: #f2f5f5;
color: #333;
padding: .5rem 1.5rem;
border-radius: 2rem;
cursor: pointer;
// border: 2px solid var(--main-light-4);
transition: background-color 0.3s;
box-shadow: 0px 0px 10px 4px var(--main-light-4);
// box-shadow: 0px 0px 10px 2px var(--main-light-4);
&:hover {
background-color: #fcfcfc;
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
background-color: #f0f1f1;
// box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
}
}
}
@ -672,19 +672,19 @@ watch(
max-width: 900px;
margin: 0 auto;
align-items: flex-end;
padding: 0.5rem;
box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
border: 1px solid #E5E5E5;
border-radius: 2rem;
background: #fafafa;
padding: 0.25rem 0.5rem;
// box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
border: 2px solid #E5E5E5;
border-radius: 1rem;
background: #fcfdfd;
transition: background 0.3s, box-shadow 0.3s;
&:focus-within {
border: 1px solid #BABABA;
border: 2px solid var(--main-500);
background: white;
box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
// box-shadow: rgb(42 60 79 / 5%) 0px 4px 10px 0px;
}
.user-input {
textarea.user-input {
flex: 1;
height: 40px;
padding: 0.5rem 0.5rem;
@ -696,7 +696,7 @@ watch(
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
outline: none;
resize: none;
&:focus {
outline: none;
box-shadow: none;
@ -706,27 +706,27 @@ watch(
outline: none;
}
}
.send-btn {
border: none;
background: transparent;
cursor: pointer;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 1rem;
transition: background-color 0.3s;
&:hover {
background-color: var(--main-light-3);
}
&:disabled {
cursor: not-allowed;
background-color: #DCDCDC;
}
}
}
button.ant-btn-icon-only {
font-size: 1.25rem;
cursor: pointer;
background-color: transparent;
border: none;
transition: color 0.3s;
box-shadow: none;
color: var(--main-700);;
padding: 0;
&:hover {
color: var(--c-text-dark-1);
}
&:disabled {
color: #ccc;
cursor: not-allowed;
}
}
.note {
width: 100%;
font-size: small;
@ -745,27 +745,6 @@ watch(
cursor: pointer;
}
.ant-btn-icon-only {
font-size: 16px;
cursor: pointer;
background-color: transparent;
border: none;
height: 2.5rem;
background-color: var(--main-color);
border-radius: 3rem;
color: white;
transition: background-color 0.3s;
&:hover {
color: white;
}
}
button:disabled {
background: #E0E0E0;
cursor: not-allowed;
}
.chat::-webkit-scrollbar {

View File

@ -0,0 +1,95 @@
<template>
<div class="graph-container" ref="container"></div>
</template>
<script setup>
import { Graph } from "@antv/g6";
import { onMounted, watch, ref } from 'vue';
const props = defineProps({
graphData: {
type: Object,
required: true,
default: () => ({ nodes: [], edges: [] })
}
});
const container = ref(null);
let graphInstance = null;
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 20,
collide: {
strength: 1.0,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 70,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
};
const renderGraph = () => {
if (!graphInstance) {
initGraph();
}
const formattedData = {
nodes: props.graphData.nodes.map(node => ({
id: node.id,
data: { label: node.name }
})),
edges: props.graphData.edges.map(edge => ({
source: edge.source_id,
target: edge.target_id,
data: { label: edge.type }
}))
};
graphInstance.setData(formattedData);
graphInstance.render();
};
onMounted(() => {
renderGraph();
window.addEventListener('resize', renderGraph);
});
watch(() => props.graphData, renderGraph, { deep: true });
</script>
<style scoped>
.graph-container {
background: #F7F7F7;
border-radius: 16px;
width: 100%;
height: 600px;
overflow: hidden;
}
</style>

View File

@ -1,17 +1,26 @@
<!-- RefsComponent.vue -->
<template>
<div class="refs" v-if="showRefs">
<div class="tags">
<span class="item"><GlobalOutlined /> {{ message.model_name }}</span>
<span class="filetag item"
v-for="(results, filename) in message.groupedResults"
<span class="item btn" @click="copyText(msg.text)"><CopyOutlined /></span>
<span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span>
<span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span>
<span class="item"><GlobalOutlined /> {{ msg.model_name }}</span>
<span
class="item btn"
@click="openSubGraph(msg)"
v-if="hasSubGraphData(msg)"
>
<GlobalOutlined /> 关系图
</span>
<span class="filetag item btn"
v-for="(results, filename) in msg.groupedResults"
:key="filename"
@click="toggleDrawer(filename)"
>
<FileTextOutlined /> {{ filename }}
<a-drawer
v-model:open="openDetail[filename]"
title="检索详情"
:title="filename"
width="800"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
@ -19,51 +28,95 @@
rootClassName="root"
>
<div class="fileinfo">
<p><strong>文件名:</strong> {{ filename }}</p>
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
<p><FileOutlined /> {{ results[0].file.type }}</p>
<p><ClockCircleOutlined /> {{ formatDate(results[0].file.created_at) }}</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="parseFloat((res.distance * 100).toFixed(2))" stroke-color="#1677FF" :size="[200, 10]"/>
<div class="results-list">
<div v-for="res in results" :key="res.id" class="result-item">
<div class="result-meta">
<div class="score-info">
<span>
<strong>相似度</strong>
<a-progress :percent="getPercent(res.distance)"/>
</span>
<span v-if="res.rerank_score">
<strong>重排序</strong>
<a-progress :percent="getPercent(res.rerank_score)"/>
</span>
</div>
<div class="result-id">ID: #{{ res.id }}</div>
</div>
</p>
<p class="result-rerank-score" v-if="res.rerank_score">
<strong>重排序分数:</strong>
<div class="scorebar">
<a-progress :percent="parseFloat((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 class="result-text">{{ res.entity.text }}</div>
</div>
</div>
</a-drawer>
</span>
</div>
<a-modal
v-model:open="subGraphVisible"
title="相关实体与关系"
:width="800"
:footer="null"
>
<GraphContainer :graphData="subGraphData" />
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
import { useClipboard } from '@vueuse/core'
import { message } from 'ant-design-vue'
import {
GlobalOutlined,
FileTextOutlined,
CopyOutlined,
LikeOutlined,
DislikeOutlined,
FileOutlined,
ClockCircleOutlined
} from '@ant-design/icons-vue'
import GraphContainer from './GraphContainer.vue' // GraphContainer
const props = defineProps({
message: Object,
})
const message = ref(props.message)
const msg = ref(props.message)
// 使 useClipboard
const { copy, isSupported } = useClipboard()
// copy
const copyText = async (text) => {
if (isSupported) {
try {
await copy(text)
message.success('文本已复制到剪贴板')
} catch (error) {
console.error('复制失败:', error)
message.error('复制失败,请手动复制')
}
} else {
console.warn('浏览器不支持自动复制')
message.warning('浏览器不支持自动复制,请手动复制')
}
}
//
const likeThisResponse = (msg) => {
console.log('Like this response:', msg)
}
const dislikeThisResponse = (msg) => {
console.log('Dislike this response:', msg)
}
// 使 reactive
const openDetail = reactive({})
// openDetail
for (const filename in message.value.groupedResults) {
for (const filename in msg.value.groupedResults) {
openDetail[filename] = false
}
@ -71,7 +124,40 @@ const toggleDrawer = (filename) => {
openDetail[filename] = !openDetail[filename]
}
const showRefs = computed(() => message.value.role=='received' && message.value.status=='finished')
const showRefs = computed(() => msg.value.role=='received' && msg.value.status=='finished')
const subGraphVisible = ref(false)
const subGraphData = ref(null)
const openSubGraph = (msg) => {
if (hasSubGraphData(msg)) {
subGraphData.value = msg.refs.graph_base.results
subGraphVisible.value = true
} else {
console.error('无法获取子图数据')
}
}
const closeSubGraph = () => {
subGraphVisible.value = false
}
const hasSubGraphData = (msg) => {
return msg.refs &&
msg.refs.graph_base &&
msg.refs.graph_base.results.nodes.length > 0;
}
//
const formatDate = (timestamp) => {
return new Date(timestamp * 1000).toLocaleString()
}
//
const getPercent = (value) => {
return parseFloat((value * 100).toFixed(2))
}
</script>
<style lang="less" scoped>
@ -83,11 +169,21 @@ const showRefs = computed(() => message.value.role=='received' && message.value.
gap: 10px;
.item {
background: var(--main-10);
color: var(--main-600);
background: var(--main-25);
color: var(--main-500);
padding: 2px 8px;
border-radius: 8px;
font-size: 14px;
&.btn {
cursor: pointer;
&:hover {
background: var(--main-25);
}
&:active {
background: var(--main-50);
}
}
}
.tags {
@ -99,56 +195,83 @@ const showRefs = computed(() => message.value.role=='received' && message.value.
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
&:hover {
background: var(--main-25);
}
}
}
}
.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);
display: flex;
justify-content: space-between;
padding: 12px 16px;
background-color: #f5f5f5;
border-radius: 4px;
margin-bottom: 16px;
p {
margin: 10px;
line-height: 1.5;
margin: 0;
color: #666;
}
}
.result-item {
margin-bottom: 20px;
padding: 24px 16px 10px 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: var(--main-light-6);
.score-info {
display: flex;
flex-wrap: wrap;
gap: 2rem;
margin-bottom: 8px;
.result-id,
.result-distance,
.result-rerank-score,
.result-text-label,
.result-text {
margin: 5px 0;
}
span {
display: flex;
align-items: center;
.scorebar {
margin-left: 10px;
display: inline-block;
width: 200px;
padding-bottom: 2px;
strong {
margin-right: 8px;
white-space: nowrap;
color: #666;
}
.ant-progress {
width: 170px;
margin-bottom: 0;
margin-inline: 10px;
& > * {
margin: 0;
.ant-progress-bg {
background-color: #666;
}
}
}
}
.result-id {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.result-text {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
background-color: #f9f9f9;
padding: 12px;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
}
</style>
.results-list {
.result-item {
border-bottom: 1px solid #f0f0f0;
padding: 16px 0;
&:last-child {
border-bottom: none;
}
}
.result-meta {
margin-bottom: 12px;
}
}
</style>

View File

@ -58,8 +58,18 @@ console.log(route)
<template>
<div class="app-layout">
<div class="debug-panel" @click="showDebug=!showDebug">
<div class="shown-btn"><BugOutlined /></div>
<div class="debug-panel" >
<a-float-button
@click="showDebug=!showDebug"
tooltip="调试面板"
:style="{
right: '12px',
}"
>
<template #icon>
<BugOutlined />
</template>
</a-float-button>
<a-drawer
v-model:open="showDebug"
title="调试面板"
@ -138,12 +148,6 @@ console.log(route)
right: 0;
bottom: 50px;
border-radius: 20px 0 0 20px;
background-color: var(--main-light-4);
padding: 6px;
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;
cursor: pointer;
}
}

View File

@ -868,29 +868,36 @@ onMounted(() => {
</style>
<style lang="less">
.atab-container {
padding: 0;
width: 100%;
max-height: 100%;
overflow: auto;
.db-main-container {
.atab-container {
padding: 0;
width: 100%;
max-height: 100%;
overflow: auto;
div.ant-tabs-nav {
background: var(--main-light-5);
padding: 8px 20px;
padding-bottom: 0;
div.ant-tabs-nav {
background: var(--main-light-5);
padding: 8px 20px;
padding-bottom: 0;
}
.ant-tabs-content-holder {
padding: 0 20px;
}
}
.ant-tabs-content-holder {
padding: 0 20px;
.params-item.col .ant-segmented {
width: 100%;
div.ant-segmented-group {
display: flex;
justify-content: space-around;
}
label.ant-segmented-item {
flex: 1;
text-align: center;
}
}
}
.params-item.col .ant-segmented {
width: 100%;
div.ant-segmented-group {
display: flex;
justify-content: space-around;
}
}
</style>