Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from dataclasses import dataclass, field, asdict
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
|
| 7 |
+
# --- 1. 数据结构定义 (与之前相同) ---
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class Entity:
|
| 11 |
+
name: str
|
| 12 |
+
entityType: str
|
| 13 |
+
observations: List[str] = field(default_factory=list)
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class Relation:
|
| 17 |
+
from_entity: str
|
| 18 |
+
to_entity: str
|
| 19 |
+
relationType: str
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class KnowledgeGraph:
|
| 23 |
+
entities: Dict[str, Entity] = field(default_factory=dict)
|
| 24 |
+
relations: List[Relation] = field(default_factory=list)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# --- 2. 核心逻辑: KnowledgeGraphManager (修改为纯内存) ---
|
| 28 |
+
|
| 29 |
+
class KnowledgeGraphManager:
|
| 30 |
+
"""
|
| 31 |
+
这个版本的 Manager 将知识图谱完全保存在内存中。
|
| 32 |
+
服务器重启后,所有数据都会丢失。
|
| 33 |
+
"""
|
| 34 |
+
def __init__(self):
|
| 35 |
+
# 服务器启动时,在内存中初始化一个空的知识图谱
|
| 36 |
+
self.graph = KnowledgeGraph()
|
| 37 |
+
print("Initialized a new, empty in-memory knowledge graph.")
|
| 38 |
+
|
| 39 |
+
def _reset_graph(self):
|
| 40 |
+
"""提供一个方法来手动重置图谱,主要用于UI。"""
|
| 41 |
+
self.graph = KnowledgeGraph()
|
| 42 |
+
return "In-memory knowledge graph has been reset to an empty state."
|
| 43 |
+
|
| 44 |
+
def create_entities(self, entities_data: List[Dict]) -> List[Entity]:
|
| 45 |
+
new_entities = []
|
| 46 |
+
for e_data in entities_data:
|
| 47 |
+
if e_data['name'] not in self.graph.entities:
|
| 48 |
+
new_entity = Entity(**e_data)
|
| 49 |
+
self.graph.entities[e_data['name']] = new_entity
|
| 50 |
+
new_entities.append(new_entity)
|
| 51 |
+
return new_entities
|
| 52 |
+
|
| 53 |
+
def create_relations(self, relations_data: List[Dict]) -> List[Relation]:
|
| 54 |
+
new_relations = []
|
| 55 |
+
existing_relations_set = {
|
| 56 |
+
(r.from_entity, r.to_entity, r.relationType) for r in self.graph.relations
|
| 57 |
+
}
|
| 58 |
+
for r_data in relations_data:
|
| 59 |
+
r_data['from_entity'] = r_data.pop('from')
|
| 60 |
+
r_data['to_entity'] = r_data.pop('to')
|
| 61 |
+
if (r_data['from_entity'], r_data['to_entity'], r_data['relationType']) not in existing_relations_set:
|
| 62 |
+
new_relation = Relation(**r_data)
|
| 63 |
+
self.graph.relations.append(new_relation)
|
| 64 |
+
new_relations.append(new_relation)
|
| 65 |
+
return new_relations
|
| 66 |
+
|
| 67 |
+
def add_observations(self, observations_data: List[Dict]) -> List[Dict]:
|
| 68 |
+
results = []
|
| 69 |
+
for o_data in observations_data:
|
| 70 |
+
entity = self.graph.entities.get(o_data['entityName'])
|
| 71 |
+
if not entity:
|
| 72 |
+
raise ValueError(f"Entity with name {o_data['entityName']} not found")
|
| 73 |
+
|
| 74 |
+
new_observations = [
|
| 75 |
+
obs for obs in o_data['contents'] if obs not in entity.observations
|
| 76 |
+
]
|
| 77 |
+
entity.observations.extend(new_observations)
|
| 78 |
+
results.append({"entityName": entity.name, "addedObservations": new_observations})
|
| 79 |
+
return results
|
| 80 |
+
|
| 81 |
+
def delete_entities(self, entity_names: List[str]) -> None:
|
| 82 |
+
names_to_delete = set(entity_names)
|
| 83 |
+
self.graph.entities = {name: entity for name, entity in self.graph.entities.items() if name not in names_to_delete}
|
| 84 |
+
self.graph.relations = [
|
| 85 |
+
r for r in self.graph.relations
|
| 86 |
+
if r.from_entity not in names_to_delete and r.to_entity not in names_to_delete
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
def delete_observations(self, deletions_data: List[Dict]) -> None:
|
| 90 |
+
for d_data in deletions_data:
|
| 91 |
+
entity = self.graph.entities.get(d_data['entityName'])
|
| 92 |
+
if entity:
|
| 93 |
+
obs_to_delete = set(d_data['observations'])
|
| 94 |
+
entity.observations = [obs for obs in entity.observations if obs not in obs_to_delete]
|
| 95 |
+
|
| 96 |
+
def delete_relations(self, relations_data: List[Dict]) -> None:
|
| 97 |
+
relations_to_delete = {
|
| 98 |
+
(r.get('from'), r.get('to'), r.get('relationType')) for r in relations_data
|
| 99 |
+
}
|
| 100 |
+
self.graph.relations = [
|
| 101 |
+
r for r in self.graph.relations
|
| 102 |
+
if (r.from_entity, r.to_entity, r.relationType) not in relations_to_delete
|
| 103 |
+
]
|
| 104 |
+
|
| 105 |
+
def read_graph(self) -> Dict:
|
| 106 |
+
return {
|
| 107 |
+
"entities": [asdict(e) for e in self.graph.entities.values()],
|
| 108 |
+
"relations": [
|
| 109 |
+
{"from": r.from_entity, "to": r.to_entity, "relationType": r.relationType}
|
| 110 |
+
for r in self.graph.relations
|
| 111 |
+
]
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
def search_nodes(self, query: str) -> Dict:
|
| 115 |
+
query_lower = query.lower()
|
| 116 |
+
|
| 117 |
+
filtered_entities = [
|
| 118 |
+
e for e in self.graph.entities.values() if
|
| 119 |
+
query_lower in e.name.lower() or
|
| 120 |
+
query_lower in e.entityType.lower() or
|
| 121 |
+
any(query_lower in obs.lower() for obs in e.observations)
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
filtered_entity_names = {e.name for e in filtered_entities}
|
| 125 |
+
|
| 126 |
+
filtered_relations = [
|
| 127 |
+
r for r in self.graph.relations if
|
| 128 |
+
r.from_entity in filtered_entity_names and r.to_entity in filtered_entity_names
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
return {
|
| 132 |
+
"entities": [asdict(e) for e in filtered_entities],
|
| 133 |
+
"relations": [
|
| 134 |
+
{"from": r.from_entity, "to": r.to_entity, "relationType": r.relationType}
|
| 135 |
+
for r in filtered_relations
|
| 136 |
+
]
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
def open_nodes(self, names: List[str]) -> Dict:
|
| 140 |
+
names_set = set(names)
|
| 141 |
+
|
| 142 |
+
filtered_entities = [e for name, e in self.graph.entities.items() if name in names_set]
|
| 143 |
+
filtered_entity_names = {e.name for e in filtered_entities}
|
| 144 |
+
filtered_relations = [
|
| 145 |
+
r for r in self.graph.relations if
|
| 146 |
+
r.from_entity in filtered_entity_names and r.to_entity in filtered_entity_names
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
return {
|
| 150 |
+
"entities": [asdict(e) for e in filtered_entities],
|
| 151 |
+
"relations": [
|
| 152 |
+
{"from": r.from_entity, "to": r.to_entity, "relationType": r.relationType}
|
| 153 |
+
for r in filtered_relations
|
| 154 |
+
]
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
# 初始化一个全局的、单例的 Manager 实例
|
| 158 |
+
# 所有API调用都将共享这一个实例
|
| 159 |
+
kg_manager = KnowledgeGraphManager()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# --- 3. Gradio MCP 工具函数定义 (与之前相同, 只调用 manager) ---
|
| 163 |
+
# ... (所有工具函数如 create_entities, read_graph 等保持不变) ...
|
| 164 |
+
def create_entities(entities: List[Dict]) -> str:
|
| 165 |
+
try:
|
| 166 |
+
new_entities = kg_manager.create_entities(entities)
|
| 167 |
+
return json.dumps([asdict(e) for e in new_entities], indent=2)
|
| 168 |
+
except Exception as e: return f"Error: {e}"
|
| 169 |
+
def create_relations(relations: List[Dict]) -> str:
|
| 170 |
+
try:
|
| 171 |
+
new_relations = kg_manager.create_relations(relations)
|
| 172 |
+
return json.dumps([{"from": r.from_entity, "to": r.to_entity, "relationType": r.relationType} for r in new_relations], indent=2)
|
| 173 |
+
except Exception as e: return f"Error: {e}"
|
| 174 |
+
def add_observations(observations: List[Dict]) -> str:
|
| 175 |
+
try:
|
| 176 |
+
results = kg_manager.add_observations(observations)
|
| 177 |
+
return json.dumps(results, indent=2)
|
| 178 |
+
except Exception as e: return f"Error: {e}"
|
| 179 |
+
def delete_entities(entityNames: List[str]) -> str:
|
| 180 |
+
try:
|
| 181 |
+
kg_manager.delete_entities(entityNames)
|
| 182 |
+
return "Entities and their relations deleted successfully."
|
| 183 |
+
except Exception as e: return f"Error: {e}"
|
| 184 |
+
def delete_observations(deletions: List[Dict]) -> str:
|
| 185 |
+
try:
|
| 186 |
+
kg_manager.delete_observations(deletions)
|
| 187 |
+
return "Observations deleted successfully."
|
| 188 |
+
except Exception as e: return f"Error: {e}"
|
| 189 |
+
def delete_relations(relations: List[Dict]) -> str:
|
| 190 |
+
try:
|
| 191 |
+
kg_manager.delete_relations(relations)
|
| 192 |
+
return "Relations deleted successfully."
|
| 193 |
+
except Exception as e: return f"Error: {e}"
|
| 194 |
+
def read_graph() -> Dict: return kg_manager.read_graph()
|
| 195 |
+
def search_nodes(query: str) -> Dict: return kg_manager.search_nodes(query)
|
| 196 |
+
def open_nodes(names: List[str]) -> Dict: return kg_manager.open_nodes(names)
|
| 197 |
+
def reset_graph_ui() -> Tuple[str, Dict]:
|
| 198 |
+
message = kg_manager._reset_graph()
|
| 199 |
+
empty_graph = kg_manager.read_graph()
|
| 200 |
+
return message, empty_graph
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
# --- 4. Gradio 界面构建 (略作调整) ---
|
| 204 |
+
|
| 205 |
+
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 206 |
+
gr.Markdown("# 知识图谱记忆工具 (纯内存模式)")
|
| 207 |
+
gr.Markdown("所有数据都保存在服务器内存中。**关闭或重启服务器将清空所有数据。**")
|
| 208 |
+
|
| 209 |
+
with gr.Tabs():
|
| 210 |
+
# --- 查看/搜索 Tab ---
|
| 211 |
+
with gr.TabItem("查看与搜索"):
|
| 212 |
+
graph_output = gr.JSON(label="知识图谱内容")
|
| 213 |
+
with gr.Row():
|
| 214 |
+
read_btn = gr.Button("读取/刷新图谱")
|
| 215 |
+
reset_btn = gr.Button("清空并重置图谱", variant="stop")
|
| 216 |
+
with gr.Row():
|
| 217 |
+
search_query = gr.Textbox(label="搜索查询", placeholder="输入实体名称、类型或观察内容中的关键词...")
|
| 218 |
+
search_btn = gr.Button("搜索节点")
|
| 219 |
+
with gr.Row():
|
| 220 |
+
open_names = gr.Textbox(label="打开节点", placeholder="输入实体名称,用逗号分隔...")
|
| 221 |
+
open_btn = gr.Button("打开指定节点")
|
| 222 |
+
|
| 223 |
+
# ... 其他Tab (创建/添加, 删除) 保持不变 ...
|
| 224 |
+
with gr.TabItem("创建与添加"):
|
| 225 |
+
status_output_create = gr.Textbox(label="操作结果", interactive=False)
|
| 226 |
+
with gr.Accordion("创建实体", open=True):
|
| 227 |
+
create_entities_input = gr.Code(label="实体 (JSON格式)", language="json", value='[\n {\n "name": "Claude 3",\n "entityType": "AI Model",\n "observations": ["Developed by Anthropic", "Is a large language model"]\n }\n]')
|
| 228 |
+
create_entities_btn = gr.Button("创建实体")
|
| 229 |
+
with gr.Accordion("创建关系", open=False):
|
| 230 |
+
create_relations_input = gr.Code(label="关系 (JSON格式)", language="json", value='[\n {\n "from": "Claude 3",\n "to": "Anthropic",\n "relationType": "developed by"\n }\n]')
|
| 231 |
+
create_relations_btn = gr.Button("创建关系")
|
| 232 |
+
with gr.Accordion("添加观察记录", open=False):
|
| 233 |
+
add_obs_input = gr.Code(label="观察记录 (JSON格式)", language="json", value='[\n {\n "entityName": "Claude 3",\n "contents": ["Released in 2024"]\n }\n]')
|
| 234 |
+
add_obs_btn = gr.Button("添加观察记录")
|
| 235 |
+
with gr.TabItem("删除"):
|
| 236 |
+
status_output_delete = gr.Textbox(label="操作结果", interactive=False)
|
| 237 |
+
with gr.Accordion("删除实体", open=True):
|
| 238 |
+
delete_entities_input = gr.Textbox(label="实体名称 (逗号分隔)", placeholder="Claude 3,Anthropic")
|
| 239 |
+
delete_entities_btn = gr.Button("删除实体")
|
| 240 |
+
with gr.Accordion("删除关系", open=False):
|
| 241 |
+
delete_relations_input = gr.Code(label="关系 (JSON格式)", language="json", value='[\n {\n "from": "Claude 3",\n "to": "Anthropic",\n "relationType": "developed by"\n }\n]')
|
| 242 |
+
delete_relations_btn = gr.Button("删除关系")
|
| 243 |
+
with gr.Accordion("删除观察记录", open=False):
|
| 244 |
+
delete_obs_input = gr.Code(label="要删除的观察记录 (JSON格式)", language="json", value='[\n {\n "entityName": "Claude 3",\n "observations": ["Released in 2024"]\n }\n]')
|
| 245 |
+
delete_obs_btn = gr.Button("删除观察记录")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# --- 5. 事件与API绑定 (与之前相同) ---
|
| 249 |
+
# ... (所有 .click 事件绑定保持不变, 添加 reset_btn 的绑定) ...
|
| 250 |
+
read_btn.click(fn=read_graph, outputs=graph_output, api_name="read_graph")
|
| 251 |
+
search_btn.click(fn=search_nodes, inputs=search_query, outputs=graph_output, api_name="search_nodes")
|
| 252 |
+
open_btn.click(fn=lambda names_str: open_nodes(names=[name.strip() for name in names_str.split(',')]), inputs=open_names, outputs=graph_output, api_name="open_nodes")
|
| 253 |
+
create_entities_btn.click(fn=lambda entities_json: create_entities(json.loads(entities_json)), inputs=create_entities_input, outputs=status_output_create, api_name="create_entities")
|
| 254 |
+
create_relations_btn.click(fn=lambda relations_json: create_relations(json.loads(relations_json)), inputs=create_relations_input, outputs=status_output_create, api_name="create_relations")
|
| 255 |
+
add_obs_btn.click(fn=lambda obs_json: add_observations(json.loads(obs_json)), inputs=add_obs_input, outputs=status_output_create, api_name="add_observations")
|
| 256 |
+
delete_entities_btn.click(fn=lambda names_str: delete_entities(entityNames=[name.strip() for name in names_str.split(',')]), inputs=delete_entities_input, outputs=status_output_delete, api_name="delete_entities")
|
| 257 |
+
delete_relations_btn.click(fn=lambda relations_json: delete_relations(json.loads(relations_json)), inputs=delete_relations_input, outputs=status_output_delete, api_name="delete_relations")
|
| 258 |
+
delete_obs_btn.click(fn=lambda obs_json: delete_observations(json.loads(obs_json)), inputs=delete_obs_input, outputs=status_output_delete, api_name="delete_observations")
|
| 259 |
+
|
| 260 |
+
# UI-only reset button
|
| 261 |
+
# We need a status output for the reset button, let's reuse one.
|
| 262 |
+
reset_btn.click(fn=reset_graph_ui, outputs=[status_output_create, graph_output])
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
app.launch(mcp_server=True)
|