mindforge-memory 6.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MindForge.py +20 -0
- mindforge/__init__.py +111 -0
- mindforge/adapters/__init__.py +18 -0
- mindforge/adapters/claude_adapter.py +84 -0
- mindforge/adapters/crewai_memory.py +76 -0
- mindforge/adapters/generic_api.py +183 -0
- mindforge/adapters/langgraph_store.py +463 -0
- mindforge/adapters/openclaw_adapter.py +91 -0
- mindforge/api/__init__.py +1 -0
- mindforge/api/server.py +1567 -0
- mindforge/cli/__init__.py +1 -0
- mindforge/cli/main.py +13607 -0
- mindforge/core/__init__.py +37 -0
- mindforge/core/embedding.py +847 -0
- mindforge/core/encryption.py +597 -0
- mindforge/core/indexer.py +363 -0
- mindforge/core/mindforge.py +6298 -0
- mindforge/core/paths.py +43 -0
- mindforge/core/query.py +501 -0
- mindforge/core/storage.py +18656 -0
- mindforge/core/types.py +314 -0
- mindforge/core/version.py +56 -0
- mindforge/daemon/__init__.py +0 -0
- mindforge/daemon/server.py +277 -0
- mindforge/mcp/__init__.py +4 -0
- mindforge/mcp/server.py +2559 -0
- mindforge/modules/__init__.py +138 -0
- mindforge/modules/categorizer.py +151 -0
- mindforge/modules/conflict_detector.py +356 -0
- mindforge/modules/connectors.py +331 -0
- mindforge/modules/context_window.py +337 -0
- mindforge/modules/event_bus.py +572 -0
- mindforge/modules/evolution.py +210 -0
- mindforge/modules/experience.py +402 -0
- mindforge/modules/federated.py +722 -0
- mindforge/modules/federated_acl.py +393 -0
- mindforge/modules/follow_up.py +315 -0
- mindforge/modules/hybrid_search.py +565 -0
- mindforge/modules/importer.py +374 -0
- mindforge/modules/integrator.py +196 -0
- mindforge/modules/intent_router.py +359 -0
- mindforge/modules/knowledge_graph.py +395 -0
- mindforge/modules/memory_compressor.py +592 -0
- mindforge/modules/memory_decay.py +474 -0
- mindforge/modules/memory_passport.py +250 -0
- mindforge/modules/multi_agent.py +534 -0
- mindforge/modules/multimodal.py +304 -0
- mindforge/modules/outcome_learning.py +186 -0
- mindforge/modules/personality.py +275 -0
- mindforge/modules/principled_forgetting.py +258 -0
- mindforge/modules/privacy.py +736 -0
- mindforge/modules/recall.py +263 -0
- mindforge/modules/session_focus.py +305 -0
- mindforge/modules/share_conflict.py +521 -0
- mindforge/modules/skill_extractor.py +340 -0
- mindforge/modules/skill_pack.py +205 -0
- mindforge/modules/timeline_viz.py +206 -0
- mindforge/modules/v6.py +931 -0
- mindforge_memory-6.4.1.dist-info/METADATA +281 -0
- mindforge_memory-6.4.1.dist-info/RECORD +64 -0
- mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
- mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
- mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
- mindforge_memory-6.4.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge v5.0 知识图谱引擎
|
|
3
|
+
自动提取实体与关系,构建动态知识网络
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
import time
|
|
10
|
+
import sqlite3 # v5.4.7 修复 C-3:异常处理中引用 sqlite3.OperationalError 需要此导入
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import List, Dict, Optional, Tuple
|
|
13
|
+
from collections import defaultdict, deque # v5.4.7 修复 L-7:BFS 使用 deque
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class KnowledgeEntity:
|
|
18
|
+
"""知识实体"""
|
|
19
|
+
id: str
|
|
20
|
+
name: str
|
|
21
|
+
entity_type: str
|
|
22
|
+
description: str = ""
|
|
23
|
+
metadata: Dict = field(default_factory=dict)
|
|
24
|
+
created_at: float = 0.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class KnowledgeRelation:
|
|
29
|
+
"""知识关系"""
|
|
30
|
+
id: str
|
|
31
|
+
from_entity: str
|
|
32
|
+
to_entity: str
|
|
33
|
+
relation_type: str
|
|
34
|
+
weight: float = 1.0
|
|
35
|
+
memory_ids: List[str] = field(default_factory=list)
|
|
36
|
+
metadata: Dict = field(default_factory=dict)
|
|
37
|
+
created_at: float = 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class GraphPath:
|
|
42
|
+
"""图路径"""
|
|
43
|
+
entities: List[str]
|
|
44
|
+
relations: List[str]
|
|
45
|
+
total_weight: float
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
ENTITY_PATTERNS = {
|
|
49
|
+
"technology": [
|
|
50
|
+
r'\b(python|java|javascript|typescript|go|rust|c\+\+|ruby|php)\b',
|
|
51
|
+
r'\b(react|vue|angular|django|flask|spring|fastapi)\b',
|
|
52
|
+
r'\b(mysql|postgresql|mongodb|redis|elasticsearch|sqlite)\b',
|
|
53
|
+
r'\b(docker|kubernetes|k8s|aws|gcp|azure)\b',
|
|
54
|
+
# v5.7.9:中文技术栈词
|
|
55
|
+
r'(python|java|mysql|redis|sqlite|docker|kubernetes|微信小程序|浏览器插件)',
|
|
56
|
+
],
|
|
57
|
+
"person": [
|
|
58
|
+
r'(?:Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.)\s+[A-Z][a-z]+',
|
|
59
|
+
],
|
|
60
|
+
"organization": [
|
|
61
|
+
r'(?:Inc|Ltd|LLC|Corp|Corporation|Company|University|Institute)\b',
|
|
62
|
+
# v5.7.9:中文组织后缀(公司/集团/大学/研究院等)
|
|
63
|
+
r'[\u4e00-\u9fff]{2,12}(?:公司|集团|大学|研究院|实验室|工作室|团队|中心)',
|
|
64
|
+
],
|
|
65
|
+
"product": [
|
|
66
|
+
# v5.7.9:常见产品/平台名
|
|
67
|
+
r'\b(wechat|alipay|feishu|lark|dingtalk|douyin|chrome|vscode|github|gitlab|slack|notion)\b',
|
|
68
|
+
r'(微信|支付宝|飞书|钉钉|抖音|浏览器|编辑器)',
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
RELATION_PATTERNS = [
|
|
73
|
+
(r'(.+?)\s+(?:使用|利用|采用|运用)\s+(.+)', "uses"),
|
|
74
|
+
(r'(.+?)\s+(?:是|属于|归类为)\s+(.+)', "is_a"),
|
|
75
|
+
(r'(.+?)\s+(?:包含|包括|有)\s+(.+)', "contains"),
|
|
76
|
+
(r'(.+?)\s+(?:相关于|关联|有关)\s+(.+)', "related_to"),
|
|
77
|
+
(r'(.+?)\s+(?:优化|改进|提升)\s+(.+)', "improves"),
|
|
78
|
+
(r'(.+?)\s+(?:基于|依赖|需要)\s+(.+)', "depends_on"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class KnowledgeGraph:
|
|
83
|
+
"""知识图谱引擎"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, storage=None, auto_load=True):
|
|
86
|
+
self.storage = storage
|
|
87
|
+
self.entities: Dict[str, KnowledgeEntity] = {}
|
|
88
|
+
self.relations: Dict[str, KnowledgeRelation] = {}
|
|
89
|
+
self._adjacency: Dict[str, List[Tuple[str, str, float]]] = defaultdict(list)
|
|
90
|
+
# v5.7.9:同进程内已抽取记忆 ID(防抖,避免全库增量重复建关系)
|
|
91
|
+
self._processed_memory_ids: set = set()
|
|
92
|
+
if self.storage is not None and auto_load:
|
|
93
|
+
self._ensure_tables()
|
|
94
|
+
self._load_from_db()
|
|
95
|
+
|
|
96
|
+
def extract_entities(self, text: str) -> List[Tuple[str, str]]:
|
|
97
|
+
"""从文本中提取实体(简易版)"""
|
|
98
|
+
entities = []
|
|
99
|
+
|
|
100
|
+
for entity_type, patterns in ENTITY_PATTERNS.items():
|
|
101
|
+
for pattern in patterns:
|
|
102
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
103
|
+
for match in matches:
|
|
104
|
+
if isinstance(match, tuple):
|
|
105
|
+
match = match[0]
|
|
106
|
+
entities.append((match.strip().lower(), entity_type))
|
|
107
|
+
|
|
108
|
+
seen = set()
|
|
109
|
+
unique = []
|
|
110
|
+
for name, etype in entities:
|
|
111
|
+
key = (name, etype)
|
|
112
|
+
if key not in seen:
|
|
113
|
+
seen.add(key)
|
|
114
|
+
unique.append((name, etype))
|
|
115
|
+
|
|
116
|
+
return unique
|
|
117
|
+
|
|
118
|
+
def extract_relations(self, text: str, entities: List[str]) -> List[Tuple[str, str, str]]:
|
|
119
|
+
"""提取实体间关系(简易版)"""
|
|
120
|
+
relations = []
|
|
121
|
+
text_lower = text.lower()
|
|
122
|
+
|
|
123
|
+
for pattern, rel_type in RELATION_PATTERNS:
|
|
124
|
+
matches = re.findall(pattern, text_lower)
|
|
125
|
+
for match in matches:
|
|
126
|
+
if len(match) >= 2:
|
|
127
|
+
subj = match[0].strip()
|
|
128
|
+
obj = match[1].strip()
|
|
129
|
+
|
|
130
|
+
subj_entity = self._find_matching_entity(subj, entities)
|
|
131
|
+
obj_entity = self._find_matching_entity(obj, entities)
|
|
132
|
+
|
|
133
|
+
if subj_entity and obj_entity and subj_entity != obj_entity:
|
|
134
|
+
relations.append((subj_entity, rel_type, obj_entity))
|
|
135
|
+
|
|
136
|
+
return relations
|
|
137
|
+
|
|
138
|
+
def _find_matching_entity(self, text: str, entities: List[str]) -> Optional[str]:
|
|
139
|
+
text_lower = text.lower()
|
|
140
|
+
for entity in entities:
|
|
141
|
+
if entity.lower() in text_lower:
|
|
142
|
+
return entity
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def _ensure_tables(self) -> None:
|
|
146
|
+
"""v5.7.9:确保图谱持久化表存在(旧库升级容错)"""
|
|
147
|
+
try:
|
|
148
|
+
conn = self.storage._get_conn()
|
|
149
|
+
conn.executescript("""
|
|
150
|
+
CREATE TABLE IF NOT EXISTS knowledge_graph (
|
|
151
|
+
id TEXT PRIMARY KEY,
|
|
152
|
+
entity TEXT,
|
|
153
|
+
entity_type TEXT,
|
|
154
|
+
description TEXT,
|
|
155
|
+
metadata TEXT DEFAULT '{}',
|
|
156
|
+
created_at REAL
|
|
157
|
+
);
|
|
158
|
+
CREATE TABLE IF NOT EXISTS graph_relations (
|
|
159
|
+
id TEXT PRIMARY KEY,
|
|
160
|
+
from_entity TEXT,
|
|
161
|
+
to_entity TEXT,
|
|
162
|
+
relation_type TEXT,
|
|
163
|
+
weight REAL DEFAULT 1.0,
|
|
164
|
+
memory_ids TEXT DEFAULT '[]',
|
|
165
|
+
metadata TEXT DEFAULT '{}',
|
|
166
|
+
created_at REAL
|
|
167
|
+
);
|
|
168
|
+
""")
|
|
169
|
+
conn.commit()
|
|
170
|
+
except sqlite3.OperationalError as _kg_err:
|
|
171
|
+
logger.warning("创建知识图谱表失败: %s", _kg_err)
|
|
172
|
+
|
|
173
|
+
def _load_from_db(self) -> None:
|
|
174
|
+
"""v5.7.9:从持久化表重建内存态(重启后实体/关系/邻接可用)"""
|
|
175
|
+
try:
|
|
176
|
+
conn = self.storage._get_conn()
|
|
177
|
+
for row in conn.execute(
|
|
178
|
+
"SELECT id, entity, entity_type, description, metadata, created_at "
|
|
179
|
+
"FROM knowledge_graph"
|
|
180
|
+
):
|
|
181
|
+
eid, name, etype, desc_text, meta, created = row
|
|
182
|
+
self.entities[eid] = KnowledgeEntity(
|
|
183
|
+
id=eid, name=name, entity_type=etype or "general",
|
|
184
|
+
description=desc_text or "",
|
|
185
|
+
metadata=json.loads(meta) if meta else {},
|
|
186
|
+
created_at=created or 0.0,
|
|
187
|
+
)
|
|
188
|
+
for row in conn.execute(
|
|
189
|
+
"SELECT id, from_entity, to_entity, relation_type, weight, "
|
|
190
|
+
"memory_ids, metadata, created_at FROM graph_relations"
|
|
191
|
+
):
|
|
192
|
+
rid, fe, te, rtype, weight, mids, meta, created = row
|
|
193
|
+
rel = KnowledgeRelation(
|
|
194
|
+
id=rid, from_entity=fe, to_entity=te, relation_type=rtype,
|
|
195
|
+
weight=weight or 1.0,
|
|
196
|
+
memory_ids=json.loads(mids) if mids else [],
|
|
197
|
+
metadata=json.loads(meta) if meta else {},
|
|
198
|
+
created_at=created or 0.0,
|
|
199
|
+
)
|
|
200
|
+
self.relations[rid] = rel
|
|
201
|
+
self._adjacency[fe].append((te, rtype, rel.weight))
|
|
202
|
+
self._adjacency[te].append((fe, rtype, rel.weight))
|
|
203
|
+
for mid in rel.memory_ids:
|
|
204
|
+
self._processed_memory_ids.add(mid)
|
|
205
|
+
except sqlite3.OperationalError as _kg_err:
|
|
206
|
+
logger.warning("加载知识图谱失败: %s", _kg_err)
|
|
207
|
+
|
|
208
|
+
def add_entity(self, name: str, entity_type: str = "general",
|
|
209
|
+
description: str = "", metadata: Optional[Dict] = None) -> KnowledgeEntity:
|
|
210
|
+
"""添加实体"""
|
|
211
|
+
entity_id = str(uuid.uuid4())
|
|
212
|
+
now = time.time()
|
|
213
|
+
|
|
214
|
+
entity = KnowledgeEntity(
|
|
215
|
+
id=entity_id,
|
|
216
|
+
name=name,
|
|
217
|
+
entity_type=entity_type,
|
|
218
|
+
description=description,
|
|
219
|
+
metadata=metadata or {},
|
|
220
|
+
created_at=now,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
self.entities[entity_id] = entity
|
|
224
|
+
|
|
225
|
+
if self.storage:
|
|
226
|
+
try:
|
|
227
|
+
conn = self.storage._get_conn()
|
|
228
|
+
conn.execute("""
|
|
229
|
+
INSERT OR IGNORE INTO knowledge_graph (id, entity, entity_type, description, metadata, created_at)
|
|
230
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
231
|
+
""", (entity_id, name, entity_type, description,
|
|
232
|
+
json.dumps(metadata or {}, ensure_ascii=False), now))
|
|
233
|
+
conn.commit()
|
|
234
|
+
except sqlite3.OperationalError as _kg_err:
|
|
235
|
+
logger.warning("写入知识图谱实体失败: %s", _kg_err)
|
|
236
|
+
|
|
237
|
+
return entity
|
|
238
|
+
|
|
239
|
+
def add_relation(self, from_name: str, to_name: str, relation_type: str,
|
|
240
|
+
weight: float = 1.0, memory_id: str = "") -> KnowledgeRelation:
|
|
241
|
+
"""添加关系"""
|
|
242
|
+
from_entity = self._get_or_create_entity(from_name)
|
|
243
|
+
to_entity = self._get_or_create_entity(to_name)
|
|
244
|
+
|
|
245
|
+
rel_id = str(uuid.uuid4())
|
|
246
|
+
now = time.time()
|
|
247
|
+
|
|
248
|
+
relation = KnowledgeRelation(
|
|
249
|
+
id=rel_id,
|
|
250
|
+
from_entity=from_entity.id,
|
|
251
|
+
to_entity=to_entity.id,
|
|
252
|
+
relation_type=relation_type,
|
|
253
|
+
weight=weight,
|
|
254
|
+
memory_ids=[memory_id] if memory_id else [],
|
|
255
|
+
created_at=now,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
self.relations[rel_id] = relation
|
|
259
|
+
self._adjacency[from_entity.id].append((to_entity.id, relation_type, weight))
|
|
260
|
+
self._adjacency[to_entity.id].append((from_entity.id, relation_type, weight))
|
|
261
|
+
|
|
262
|
+
if self.storage:
|
|
263
|
+
try:
|
|
264
|
+
conn = self.storage._get_conn()
|
|
265
|
+
conn.execute("""
|
|
266
|
+
INSERT INTO graph_relations (id, from_entity, to_entity, relation_type, weight, memory_ids, metadata, created_at)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
268
|
+
""", (rel_id, from_entity.id, to_entity.id, relation_type,
|
|
269
|
+
weight, json.dumps([memory_id] if memory_id else []),
|
|
270
|
+
json.dumps({}), now))
|
|
271
|
+
conn.commit()
|
|
272
|
+
except sqlite3.OperationalError as _kg_err:
|
|
273
|
+
logger.warning("写入知识图谱关系失败: %s", _kg_err)
|
|
274
|
+
|
|
275
|
+
return relation
|
|
276
|
+
|
|
277
|
+
def _get_or_create_entity(self, name: str) -> KnowledgeEntity:
|
|
278
|
+
for entity in self.entities.values():
|
|
279
|
+
if entity.name.lower() == name.lower():
|
|
280
|
+
return entity
|
|
281
|
+
return self.add_entity(name)
|
|
282
|
+
|
|
283
|
+
def process_memory(self, memory_id: str, content: str) -> Tuple[List[KnowledgeEntity], List[KnowledgeRelation]]:
|
|
284
|
+
"""处理一条记忆,提取实体和关系"""
|
|
285
|
+
# v5.7.9:同进程内已处理记忆直接跳过(防抖)
|
|
286
|
+
if memory_id and memory_id in self._processed_memory_ids:
|
|
287
|
+
return [], []
|
|
288
|
+
entities_data = self.extract_entities(content)
|
|
289
|
+
entity_names = [name for name, _ in entities_data]
|
|
290
|
+
|
|
291
|
+
added_entities = []
|
|
292
|
+
for name, etype in entities_data:
|
|
293
|
+
entity = self._get_or_create_entity(name)
|
|
294
|
+
if entity.entity_type == "general" and etype != "general":
|
|
295
|
+
entity.entity_type = etype
|
|
296
|
+
added_entities.append(entity)
|
|
297
|
+
|
|
298
|
+
relations = self.extract_relations(content, entity_names)
|
|
299
|
+
added_relations = []
|
|
300
|
+
for subj, rel_type, obj in relations:
|
|
301
|
+
relation = self.add_relation(subj, obj, rel_type, memory_id=memory_id)
|
|
302
|
+
added_relations.append(relation)
|
|
303
|
+
|
|
304
|
+
if memory_id:
|
|
305
|
+
self._processed_memory_ids.add(memory_id)
|
|
306
|
+
|
|
307
|
+
return added_entities, added_relations
|
|
308
|
+
|
|
309
|
+
def get_related_entities(self, entity_name: str, depth: int = 2,
|
|
310
|
+
max_results: int = 20) -> List[Tuple[str, str, float]]:
|
|
311
|
+
"""获取相关实体(广度优先)"""
|
|
312
|
+
entity = self._find_entity_by_name(entity_name)
|
|
313
|
+
if not entity:
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
visited = {entity.id}
|
|
317
|
+
queue = deque([(entity.id, 0, 1.0)]) # v5.4.7 修复 L-7:使用 deque 替代 list.pop(0)
|
|
318
|
+
results = []
|
|
319
|
+
|
|
320
|
+
while queue:
|
|
321
|
+
current_id, current_depth, current_weight = queue.popleft()
|
|
322
|
+
if current_depth > depth:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
for neighbor_id, rel_type, weight in self._adjacency.get(current_id, []):
|
|
326
|
+
if neighbor_id in visited:
|
|
327
|
+
continue
|
|
328
|
+
visited.add(neighbor_id)
|
|
329
|
+
|
|
330
|
+
total_weight = current_weight * weight
|
|
331
|
+
neighbor = self.entities.get(neighbor_id)
|
|
332
|
+
if neighbor:
|
|
333
|
+
results.append((neighbor.name, rel_type, total_weight))
|
|
334
|
+
|
|
335
|
+
if current_depth < depth:
|
|
336
|
+
queue.append((neighbor_id, current_depth + 1, total_weight))
|
|
337
|
+
|
|
338
|
+
results.sort(key=lambda x: x[2], reverse=True)
|
|
339
|
+
return results[:max_results]
|
|
340
|
+
|
|
341
|
+
def find_path(self, from_name: str, to_name: str, max_depth: int = 3) -> Optional[GraphPath]:
|
|
342
|
+
"""查找两个实体间的路径"""
|
|
343
|
+
from_entity = self._find_entity_by_name(from_name)
|
|
344
|
+
to_entity = self._find_entity_by_name(to_name)
|
|
345
|
+
if not from_entity or not to_entity:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
from queue import deque
|
|
349
|
+
queue = deque([(from_entity.id, [from_entity.id], [])])
|
|
350
|
+
visited = {from_entity.id}
|
|
351
|
+
|
|
352
|
+
while queue:
|
|
353
|
+
current, path, rels = queue.popleft()
|
|
354
|
+
if len(path) > max_depth + 1:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
if current == to_entity.id:
|
|
358
|
+
entity_names = []
|
|
359
|
+
for eid in path:
|
|
360
|
+
e = self.entities.get(eid)
|
|
361
|
+
entity_names.append(e.name if e else eid)
|
|
362
|
+
total_w = 1.0
|
|
363
|
+
return GraphPath(entities=entity_names, relations=rels, total_weight=total_w)
|
|
364
|
+
|
|
365
|
+
for neighbor_id, rel_type, weight in self._adjacency.get(current, []):
|
|
366
|
+
if neighbor_id in visited:
|
|
367
|
+
continue
|
|
368
|
+
visited.add(neighbor_id)
|
|
369
|
+
queue.append((neighbor_id, path + [neighbor_id], rels + [rel_type]))
|
|
370
|
+
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
def _find_entity_by_name(self, name: str) -> Optional[KnowledgeEntity]:
|
|
374
|
+
name_lower = name.lower()
|
|
375
|
+
for entity in self.entities.values():
|
|
376
|
+
if entity.name.lower() == name_lower:
|
|
377
|
+
return entity
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
def get_entity_stats(self) -> Dict:
|
|
381
|
+
"""获取图谱统计"""
|
|
382
|
+
type_counts = defaultdict(int)
|
|
383
|
+
for entity in self.entities.values():
|
|
384
|
+
type_counts[entity.entity_type] += 1
|
|
385
|
+
|
|
386
|
+
rel_counts = defaultdict(int)
|
|
387
|
+
for relation in self.relations.values():
|
|
388
|
+
rel_counts[relation.relation_type] += 1
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"total_entities": len(self.entities),
|
|
392
|
+
"total_relations": len(self.relations),
|
|
393
|
+
"entity_types": dict(type_counts),
|
|
394
|
+
"relation_types": dict(rel_counts),
|
|
395
|
+
}
|