index_entity_mixin.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. IndexEntityMixin extracted from IndexManager.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import logging
  9. import re
  10. import sqlite3
  11. from datetime import datetime
  12. from typing import Any, Dict, List, Optional
  13. logger = logging.getLogger(__name__)
  14. class IndexEntityMixin:
  15. def upsert_entity(self, entity: EntityMeta, update_metadata: bool = False) -> bool:
  16. """
  17. 插入或更新实体 (智能合并)
  18. - 新实体: 直接插入
  19. - 已存在: 更新 current_json, last_appearance, updated_at
  20. - update_metadata=True: 同时更新 canonical_name/tier/desc/is_protagonist/is_archived
  21. 返回是否为新实体
  22. """
  23. with self._get_conn() as conn:
  24. cursor = conn.cursor()
  25. # 检查是否存在
  26. cursor.execute(
  27. "SELECT id, current_json FROM entities WHERE id = ?", (entity.id,)
  28. )
  29. existing = cursor.fetchone()
  30. if existing:
  31. # 已存在: 智能合并 current_json
  32. old_current = {}
  33. if existing["current_json"]:
  34. try:
  35. old_current = json.loads(existing["current_json"])
  36. except json.JSONDecodeError as exc:
  37. logger.warning(
  38. "failed to parse JSON in entities.current_json: %s",
  39. exc,
  40. )
  41. # 合并 current (新值覆盖旧值)
  42. merged_current = {**old_current, **entity.current}
  43. if update_metadata:
  44. # 完整更新(包括元数据)
  45. cursor.execute(
  46. """
  47. UPDATE entities SET
  48. canonical_name = ?,
  49. tier = ?,
  50. desc = ?,
  51. current_json = ?,
  52. last_appearance = ?,
  53. is_protagonist = ?,
  54. is_archived = ?,
  55. updated_at = CURRENT_TIMESTAMP
  56. WHERE id = ?
  57. """,
  58. (
  59. entity.canonical_name,
  60. entity.tier,
  61. entity.desc,
  62. json.dumps(merged_current, ensure_ascii=False),
  63. entity.last_appearance,
  64. 1 if entity.is_protagonist else 0,
  65. 1 if entity.is_archived else 0,
  66. entity.id,
  67. ),
  68. )
  69. else:
  70. # 只更新 current 和 last_appearance
  71. cursor.execute(
  72. """
  73. UPDATE entities SET
  74. current_json = ?,
  75. last_appearance = ?,
  76. updated_at = CURRENT_TIMESTAMP
  77. WHERE id = ?
  78. """,
  79. (
  80. json.dumps(merged_current, ensure_ascii=False),
  81. entity.last_appearance,
  82. entity.id,
  83. ),
  84. )
  85. conn.commit()
  86. return False
  87. else:
  88. # 新实体: 插入
  89. cursor.execute(
  90. """
  91. INSERT INTO entities
  92. (id, type, canonical_name, tier, desc, current_json,
  93. first_appearance, last_appearance, is_protagonist, is_archived)
  94. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  95. """,
  96. (
  97. entity.id,
  98. entity.type,
  99. entity.canonical_name,
  100. entity.tier,
  101. entity.desc,
  102. json.dumps(entity.current, ensure_ascii=False),
  103. entity.first_appearance,
  104. entity.last_appearance,
  105. 1 if entity.is_protagonist else 0,
  106. 1 if entity.is_archived else 0,
  107. ),
  108. )
  109. conn.commit()
  110. return True
  111. def get_entity(self, entity_id: str) -> Optional[Dict]:
  112. """获取单个实体"""
  113. with self._get_conn() as conn:
  114. cursor = conn.cursor()
  115. cursor.execute("SELECT * FROM entities WHERE id = ?", (entity_id,))
  116. row = cursor.fetchone()
  117. if row:
  118. return self._row_to_dict(row, parse_json=["current_json"])
  119. return None
  120. def get_entities_by_type(
  121. self, entity_type: str, include_archived: bool = False
  122. ) -> List[Dict]:
  123. """按类型获取实体"""
  124. with self._get_conn() as conn:
  125. cursor = conn.cursor()
  126. if include_archived:
  127. cursor.execute(
  128. """
  129. SELECT * FROM entities WHERE type = ?
  130. ORDER BY last_appearance DESC
  131. """,
  132. (entity_type,),
  133. )
  134. else:
  135. cursor.execute(
  136. """
  137. SELECT * FROM entities WHERE type = ? AND is_archived = 0
  138. ORDER BY last_appearance DESC
  139. """,
  140. (entity_type,),
  141. )
  142. return [
  143. self._row_to_dict(row, parse_json=["current_json"])
  144. for row in cursor.fetchall()
  145. ]
  146. def get_entities_by_tier(self, tier: str) -> List[Dict]:
  147. """按重要度获取实体 (核心/重要/次要/装饰)"""
  148. with self._get_conn() as conn:
  149. cursor = conn.cursor()
  150. cursor.execute(
  151. """
  152. SELECT * FROM entities WHERE tier = ? AND is_archived = 0
  153. ORDER BY last_appearance DESC
  154. """,
  155. (tier,),
  156. )
  157. return [
  158. self._row_to_dict(row, parse_json=["current_json"])
  159. for row in cursor.fetchall()
  160. ]
  161. def get_core_entities(self) -> List[Dict]:
  162. """获取所有核心实体 (用于 Context Agent 全量加载)"""
  163. with self._get_conn() as conn:
  164. cursor = conn.cursor()
  165. cursor.execute("""
  166. SELECT * FROM entities
  167. WHERE (tier IN ('核心', '重要') OR is_protagonist = 1) AND is_archived = 0
  168. ORDER BY is_protagonist DESC, tier, last_appearance DESC
  169. """)
  170. return [
  171. self._row_to_dict(row, parse_json=["current_json"])
  172. for row in cursor.fetchall()
  173. ]
  174. def get_protagonist(self) -> Optional[Dict]:
  175. """获取主角实体"""
  176. with self._get_conn() as conn:
  177. cursor = conn.cursor()
  178. cursor.execute("SELECT * FROM entities WHERE is_protagonist = 1 LIMIT 1")
  179. row = cursor.fetchone()
  180. if row:
  181. return self._row_to_dict(row, parse_json=["current_json"])
  182. return None
  183. def update_entity_current(self, entity_id: str, updates: Dict) -> bool:
  184. """
  185. 增量更新实体的 current 字段 (不覆盖其他字段)
  186. 例如: update_entity_current("xiaoyan", {"realm": "斗师"})
  187. """
  188. with self._get_conn() as conn:
  189. cursor = conn.cursor()
  190. cursor.execute(
  191. "SELECT current_json FROM entities WHERE id = ?", (entity_id,)
  192. )
  193. row = cursor.fetchone()
  194. if not row:
  195. return False
  196. current = {}
  197. if row["current_json"]:
  198. try:
  199. current = json.loads(row["current_json"])
  200. except json.JSONDecodeError as exc:
  201. logger.warning(
  202. "failed to parse JSON in update_entity_current current_json: %s",
  203. exc,
  204. )
  205. current.update(updates)
  206. cursor.execute(
  207. """
  208. UPDATE entities SET
  209. current_json = ?,
  210. updated_at = CURRENT_TIMESTAMP
  211. WHERE id = ?
  212. """,
  213. (json.dumps(current, ensure_ascii=False), entity_id),
  214. )
  215. conn.commit()
  216. return True
  217. def archive_entity(self, entity_id: str) -> bool:
  218. """归档实体 (不删除,只是标记)"""
  219. with self._get_conn() as conn:
  220. cursor = conn.cursor()
  221. cursor.execute(
  222. """
  223. UPDATE entities SET is_archived = 1, updated_at = CURRENT_TIMESTAMP
  224. WHERE id = ?
  225. """,
  226. (entity_id,),
  227. )
  228. conn.commit()
  229. return cursor.rowcount > 0
  230. # ==================== v5.1 别名操作 ====================
  231. def register_alias(self, alias: str, entity_id: str, entity_type: str) -> bool:
  232. """
  233. 注册别名 (支持一对多)
  234. 同一别名可映射多个实体 (如 "天云宗" → 地点 + 势力)
  235. """
  236. with self._get_conn() as conn:
  237. cursor = conn.cursor()
  238. try:
  239. cursor.execute(
  240. """
  241. INSERT OR IGNORE INTO aliases (alias, entity_id, entity_type)
  242. VALUES (?, ?, ?)
  243. """,
  244. (alias, entity_id, entity_type),
  245. )
  246. conn.commit()
  247. return cursor.rowcount > 0
  248. except sqlite3.IntegrityError:
  249. return False
  250. def get_entities_by_alias(self, alias: str) -> List[Dict]:
  251. """
  252. 根据别名查找实体 (一对多)
  253. 返回所有匹配的实体 (可能有多个不同类型)
  254. """
  255. with self._get_conn() as conn:
  256. cursor = conn.cursor()
  257. cursor.execute(
  258. """
  259. SELECT e.*, a.entity_type as alias_type
  260. FROM entities e
  261. JOIN aliases a ON e.id = a.entity_id
  262. WHERE a.alias = ?
  263. """,
  264. (alias,),
  265. )
  266. return [
  267. self._row_to_dict(row, parse_json=["current_json"])
  268. for row in cursor.fetchall()
  269. ]
  270. def get_entity_aliases(self, entity_id: str) -> List[str]:
  271. """获取实体的所有别名"""
  272. with self._get_conn() as conn:
  273. cursor = conn.cursor()
  274. cursor.execute(
  275. "SELECT alias FROM aliases WHERE entity_id = ?", (entity_id,)
  276. )
  277. return [row["alias"] for row in cursor.fetchall()]
  278. def remove_alias(self, alias: str, entity_id: str) -> bool:
  279. """移除别名"""
  280. with self._get_conn() as conn:
  281. cursor = conn.cursor()
  282. cursor.execute(
  283. "DELETE FROM aliases WHERE alias = ? AND entity_id = ?",
  284. (alias, entity_id),
  285. )
  286. conn.commit()
  287. return cursor.rowcount > 0
  288. # ==================== v5.1 状态变化操作 ====================
  289. def record_state_change(self, change: StateChangeMeta) -> int:
  290. """
  291. 记录状态变化
  292. 返回记录 ID
  293. """
  294. with self._get_conn() as conn:
  295. cursor = conn.cursor()
  296. cursor.execute(
  297. """
  298. INSERT INTO state_changes
  299. (entity_id, field, old_value, new_value, reason, chapter)
  300. VALUES (?, ?, ?, ?, ?, ?)
  301. """,
  302. (
  303. change.entity_id,
  304. change.field,
  305. change.old_value,
  306. change.new_value,
  307. change.reason,
  308. change.chapter,
  309. ),
  310. )
  311. conn.commit()
  312. return cursor.lastrowid
  313. def get_entity_state_changes(self, entity_id: str, limit: int = 20) -> List[Dict]:
  314. """获取实体的状态变化历史"""
  315. with self._get_conn() as conn:
  316. cursor = conn.cursor()
  317. cursor.execute(
  318. """
  319. SELECT * FROM state_changes
  320. WHERE entity_id = ?
  321. ORDER BY chapter DESC, id DESC
  322. LIMIT ?
  323. """,
  324. (entity_id, limit),
  325. )
  326. return [dict(row) for row in cursor.fetchall()]
  327. def get_recent_state_changes(self, limit: int = 50) -> List[Dict]:
  328. """获取最近的状态变化"""
  329. with self._get_conn() as conn:
  330. cursor = conn.cursor()
  331. cursor.execute(
  332. """
  333. SELECT * FROM state_changes
  334. ORDER BY chapter DESC, id DESC
  335. LIMIT ?
  336. """,
  337. (limit,),
  338. )
  339. return [dict(row) for row in cursor.fetchall()]
  340. def get_chapter_state_changes(self, chapter: int) -> List[Dict]:
  341. """获取某章的所有状态变化"""
  342. with self._get_conn() as conn:
  343. cursor = conn.cursor()
  344. cursor.execute(
  345. """
  346. SELECT * FROM state_changes
  347. WHERE chapter = ?
  348. ORDER BY id
  349. """,
  350. (chapter,),
  351. )
  352. return [dict(row) for row in cursor.fetchall()]
  353. # ==================== v5.1 关系操作 ====================
  354. def upsert_relationship(self, rel: RelationshipMeta) -> bool:
  355. """
  356. 插入或更新关系
  357. 相同 (from, to, type) 会更新 description 和 chapter
  358. 返回是否为新关系
  359. """
  360. with self._get_conn() as conn:
  361. cursor = conn.cursor()
  362. # 检查是否存在
  363. cursor.execute(
  364. """
  365. SELECT id FROM relationships
  366. WHERE from_entity = ? AND to_entity = ? AND type = ?
  367. """,
  368. (rel.from_entity, rel.to_entity, rel.type),
  369. )
  370. existing = cursor.fetchone()
  371. if existing:
  372. cursor.execute(
  373. """
  374. UPDATE relationships SET
  375. description = ?,
  376. chapter = ?
  377. WHERE id = ?
  378. """,
  379. (rel.description, rel.chapter, existing["id"]),
  380. )
  381. conn.commit()
  382. return False
  383. else:
  384. cursor.execute(
  385. """
  386. INSERT INTO relationships
  387. (from_entity, to_entity, type, description, chapter)
  388. VALUES (?, ?, ?, ?, ?)
  389. """,
  390. (
  391. rel.from_entity,
  392. rel.to_entity,
  393. rel.type,
  394. rel.description,
  395. rel.chapter,
  396. ),
  397. )
  398. conn.commit()
  399. return True
  400. def get_entity_relationships(
  401. self, entity_id: str, direction: str = "both"
  402. ) -> List[Dict]:
  403. """
  404. 获取实体的关系
  405. direction: "from" | "to" | "both"
  406. """
  407. with self._get_conn() as conn:
  408. cursor = conn.cursor()
  409. if direction == "from":
  410. cursor.execute(
  411. """
  412. SELECT * FROM relationships WHERE from_entity = ?
  413. ORDER BY chapter DESC
  414. """,
  415. (entity_id,),
  416. )
  417. elif direction == "to":
  418. cursor.execute(
  419. """
  420. SELECT * FROM relationships WHERE to_entity = ?
  421. ORDER BY chapter DESC
  422. """,
  423. (entity_id,),
  424. )
  425. else: # both
  426. cursor.execute(
  427. """
  428. SELECT * FROM relationships
  429. WHERE from_entity = ? OR to_entity = ?
  430. ORDER BY chapter DESC
  431. """,
  432. (entity_id, entity_id),
  433. )
  434. return [dict(row) for row in cursor.fetchall()]
  435. def get_relationship_between(self, entity1: str, entity2: str) -> List[Dict]:
  436. """获取两个实体之间的所有关系"""
  437. with self._get_conn() as conn:
  438. cursor = conn.cursor()
  439. cursor.execute(
  440. """
  441. SELECT * FROM relationships
  442. WHERE (from_entity = ? AND to_entity = ?)
  443. OR (from_entity = ? AND to_entity = ?)
  444. ORDER BY chapter DESC
  445. """,
  446. (entity1, entity2, entity2, entity1),
  447. )
  448. return [dict(row) for row in cursor.fetchall()]
  449. def get_recent_relationships(self, limit: int = 30) -> List[Dict]:
  450. """获取最近建立的关系"""
  451. with self._get_conn() as conn:
  452. cursor = conn.cursor()
  453. cursor.execute(
  454. """
  455. SELECT * FROM relationships
  456. ORDER BY chapter DESC, id DESC
  457. LIMIT ?
  458. """,
  459. (limit,),
  460. )
  461. return [dict(row) for row in cursor.fetchall()]
  462. # ==================== v5.5 关系事件与图谱 ====================
  463. def _infer_relationship_polarity(self, rel_type: str) -> int:
  464. """基于关系类型推断极性:-1 敌对,0 中立,1 友好。"""
  465. t = str(rel_type or "")
  466. positive_keywords = ("盟友", "友好", "师徒", "同伴", "亲", "爱", "合作")
  467. negative_keywords = ("敌", "仇", "恨", "对立", "冲突", "背叛", "追杀")
  468. if any(k in t for k in negative_keywords):
  469. return -1
  470. if any(k in t for k in positive_keywords):
  471. return 1
  472. return 0
  473. def record_relationship_event(self, event: RelationshipEventMeta) -> int:
  474. """记录关系事件,返回事件 ID。"""
  475. from_entity = str(getattr(event, "from_entity", "") or "").strip()
  476. to_entity = str(getattr(event, "to_entity", "") or "").strip()
  477. rel_type = str(getattr(event, "type", "") or "").strip()
  478. if not from_entity or not to_entity or not rel_type:
  479. return 0
  480. action = str(getattr(event, "action", "update") or "update").strip().lower()
  481. if action not in {"create", "update", "decay", "remove"}:
  482. action = "update"
  483. try:
  484. chapter = int(getattr(event, "chapter", 0) or 0)
  485. except (TypeError, ValueError):
  486. return 0
  487. if chapter <= 0:
  488. return 0
  489. try:
  490. scene_index = int(getattr(event, "scene_index", 0) or 0)
  491. except (TypeError, ValueError):
  492. scene_index = 0
  493. raw_polarity = getattr(event, "polarity", None)
  494. if raw_polarity is None:
  495. polarity = self._infer_relationship_polarity(rel_type)
  496. else:
  497. try:
  498. polarity = int(raw_polarity)
  499. except (TypeError, ValueError):
  500. polarity = 0
  501. if polarity > 1:
  502. polarity = 1
  503. elif polarity < -1:
  504. polarity = -1
  505. try:
  506. strength = float(getattr(event, "strength", 0.5) or 0.5)
  507. except (TypeError, ValueError):
  508. strength = 0.5
  509. strength = max(0.0, min(1.0, strength))
  510. description = str(getattr(event, "description", "") or "").strip()
  511. evidence = str(getattr(event, "evidence", "") or "").strip()
  512. try:
  513. confidence = float(getattr(event, "confidence", 1.0) or 1.0)
  514. except (TypeError, ValueError):
  515. confidence = 1.0
  516. confidence = max(0.0, min(1.0, confidence))
  517. with self._get_conn() as conn:
  518. cursor = conn.cursor()
  519. cursor.execute(
  520. """
  521. INSERT INTO relationship_events
  522. (from_entity, to_entity, type, action, polarity, strength, description, chapter, scene_index, evidence, confidence)
  523. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  524. """,
  525. (
  526. from_entity,
  527. to_entity,
  528. rel_type,
  529. action,
  530. polarity,
  531. strength,
  532. description,
  533. chapter,
  534. scene_index,
  535. evidence,
  536. confidence,
  537. ),
  538. )
  539. conn.commit()
  540. return int(cursor.lastrowid or 0)
  541. def get_relationship_events(
  542. self,
  543. entity_id: str,
  544. direction: str = "both",
  545. from_chapter: Optional[int] = None,
  546. to_chapter: Optional[int] = None,
  547. limit: int = 100,
  548. ) -> List[Dict[str, Any]]:
  549. """按实体查询关系事件。"""
  550. direction = str(direction or "both").lower()
  551. clauses: List[str] = []
  552. params: List[Any] = []
  553. if direction == "from":
  554. clauses.append("from_entity = ?")
  555. params.append(entity_id)
  556. elif direction == "to":
  557. clauses.append("to_entity = ?")
  558. params.append(entity_id)
  559. else:
  560. clauses.append("(from_entity = ? OR to_entity = ?)")
  561. params.extend([entity_id, entity_id])
  562. if from_chapter is not None:
  563. clauses.append("chapter >= ?")
  564. params.append(int(from_chapter))
  565. if to_chapter is not None:
  566. clauses.append("chapter <= ?")
  567. params.append(int(to_chapter))
  568. where_sql = " AND ".join(clauses) if clauses else "1=1"
  569. with self._get_conn() as conn:
  570. cursor = conn.cursor()
  571. cursor.execute(
  572. f"""
  573. SELECT * FROM relationship_events
  574. WHERE {where_sql}
  575. ORDER BY chapter DESC, id DESC
  576. LIMIT ?
  577. """,
  578. (*params, int(limit)),
  579. )
  580. return [dict(row) for row in cursor.fetchall()]
  581. def get_relationship_timeline(
  582. self,
  583. entity1: str,
  584. entity2: str,
  585. from_chapter: Optional[int] = None,
  586. to_chapter: Optional[int] = None,
  587. limit: int = 100,
  588. ) -> List[Dict[str, Any]]:
  589. """查询两个实体之间的关系时间线。"""
  590. clauses = [
  591. "((from_entity = ? AND to_entity = ?) OR (from_entity = ? AND to_entity = ?))"
  592. ]
  593. params: List[Any] = [entity1, entity2, entity2, entity1]
  594. if from_chapter is not None:
  595. clauses.append("chapter >= ?")
  596. params.append(int(from_chapter))
  597. if to_chapter is not None:
  598. clauses.append("chapter <= ?")
  599. params.append(int(to_chapter))
  600. where_sql = " AND ".join(clauses)
  601. with self._get_conn() as conn:
  602. cursor = conn.cursor()
  603. cursor.execute(
  604. f"""
  605. SELECT * FROM relationship_events
  606. WHERE {where_sql}
  607. ORDER BY chapter ASC, id ASC
  608. LIMIT ?
  609. """,
  610. (*params, int(limit)),
  611. )
  612. return [dict(row) for row in cursor.fetchall()]
  613. def _load_effective_relationship_edges(
  614. self,
  615. chapter: Optional[int] = None,
  616. relation_types: Optional[List[str]] = None,
  617. ) -> List[Dict[str, Any]]:
  618. """加载指定章节截面的有效关系边。"""
  619. relation_types = [str(t) for t in (relation_types or []) if str(t).strip()]
  620. with self._get_conn() as conn:
  621. cursor = conn.cursor()
  622. if chapter is None:
  623. clauses = []
  624. params: List[Any] = []
  625. if relation_types:
  626. placeholders = ",".join("?" for _ in relation_types)
  627. clauses.append(f"type IN ({placeholders})")
  628. params.extend(relation_types)
  629. where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
  630. cursor.execute(
  631. f"""
  632. SELECT from_entity, to_entity, type, description, chapter
  633. FROM relationships
  634. {where_sql}
  635. ORDER BY chapter DESC, id DESC
  636. """,
  637. tuple(params),
  638. )
  639. rows = cursor.fetchall()
  640. return [
  641. {
  642. "from": str(r["from_entity"]),
  643. "to": str(r["to_entity"]),
  644. "type": str(r["type"]),
  645. "description": str(r["description"] or ""),
  646. "chapter": int(r["chapter"] or 0),
  647. "action": "snapshot",
  648. "polarity": self._infer_relationship_polarity(str(r["type"])),
  649. "strength": 0.5,
  650. "evidence": "",
  651. "confidence": 1.0,
  652. }
  653. for r in rows
  654. ]
  655. clauses = ["chapter <= ?"]
  656. params = [int(chapter)]
  657. if relation_types:
  658. placeholders = ",".join("?" for _ in relation_types)
  659. clauses.append(f"type IN ({placeholders})")
  660. params.extend(relation_types)
  661. cursor.execute(
  662. f"""
  663. SELECT *
  664. FROM relationship_events
  665. WHERE {' AND '.join(clauses)}
  666. ORDER BY chapter DESC, id DESC
  667. """,
  668. tuple(params),
  669. )
  670. event_rows = cursor.fetchall()
  671. # 兼容旧数据:若事件流不完整,回退 relationships 快照补边
  672. snapshot_clauses = ["chapter <= ?"]
  673. snapshot_params: List[Any] = [int(chapter)]
  674. if relation_types:
  675. placeholders = ",".join("?" for _ in relation_types)
  676. snapshot_clauses.append(f"type IN ({placeholders})")
  677. snapshot_params.extend(relation_types)
  678. cursor.execute(
  679. f"""
  680. SELECT from_entity, to_entity, type, description, chapter
  681. FROM relationships
  682. WHERE {' AND '.join(snapshot_clauses)}
  683. ORDER BY chapter DESC, id DESC
  684. """,
  685. tuple(snapshot_params),
  686. )
  687. snapshot_rows = cursor.fetchall()
  688. # 章节截面:相同关系只保留“最近一次事件”,remove 视为已失效。
  689. effective: List[Dict[str, Any]] = []
  690. seen: set[tuple[str, str, str]] = set()
  691. for row in event_rows:
  692. key = (
  693. str(row["from_entity"]),
  694. str(row["to_entity"]),
  695. str(row["type"]),
  696. )
  697. if key in seen:
  698. continue
  699. seen.add(key)
  700. action = str(row["action"] or "update")
  701. if action == "remove":
  702. continue
  703. effective.append(
  704. {
  705. "from": key[0],
  706. "to": key[1],
  707. "type": key[2],
  708. "description": str(row["description"] or ""),
  709. "chapter": int(row["chapter"] or 0),
  710. "action": action,
  711. "polarity": int(row["polarity"] or 0),
  712. "strength": float(row["strength"] or 0.5),
  713. "evidence": str(row["evidence"] or ""),
  714. "confidence": float(row["confidence"] or 1.0),
  715. }
  716. )
  717. # 事件流缺失时,从关系快照补齐(若 key 已出现则以事件为准)
  718. for row in snapshot_rows:
  719. key = (
  720. str(row["from_entity"]),
  721. str(row["to_entity"]),
  722. str(row["type"]),
  723. )
  724. if key in seen:
  725. continue
  726. effective.append(
  727. {
  728. "from": key[0],
  729. "to": key[1],
  730. "type": key[2],
  731. "description": str(row["description"] or ""),
  732. "chapter": int(row["chapter"] or 0),
  733. "action": "snapshot",
  734. "polarity": self._infer_relationship_polarity(key[2]),
  735. "strength": 0.5,
  736. "evidence": "",
  737. "confidence": 1.0,
  738. }
  739. )
  740. return effective
  741. def build_relationship_subgraph(
  742. self,
  743. center_entity: str,
  744. depth: int = 2,
  745. chapter: Optional[int] = None,
  746. top_edges: int = 50,
  747. relation_types: Optional[List[str]] = None,
  748. ) -> Dict[str, Any]:
  749. """按中心实体构建关系子图。"""
  750. center_entity = str(center_entity or "").strip()
  751. depth = max(1, int(depth or 1))
  752. top_edges = max(1, int(top_edges or 1))
  753. edges_all = self._load_effective_relationship_edges(
  754. chapter=chapter,
  755. relation_types=relation_types,
  756. )
  757. edges_all.sort(key=lambda x: int(x.get("chapter", 0)), reverse=True)
  758. selected_edges: List[Dict[str, Any]] = []
  759. selected_keys: set[tuple[str, str, str]] = set()
  760. visited_nodes: set[str] = {center_entity} if center_entity else set()
  761. frontier: set[str] = {center_entity} if center_entity else set()
  762. for _ in range(depth):
  763. if not frontier:
  764. break
  765. next_frontier: set[str] = set()
  766. for edge in edges_all:
  767. from_entity = str(edge.get("from") or "")
  768. to_entity = str(edge.get("to") or "")
  769. if from_entity not in frontier and to_entity not in frontier:
  770. continue
  771. key = (from_entity, to_entity, str(edge.get("type") or ""))
  772. if key in selected_keys:
  773. continue
  774. selected_keys.add(key)
  775. selected_edges.append(edge)
  776. if from_entity and from_entity not in visited_nodes:
  777. visited_nodes.add(from_entity)
  778. next_frontier.add(from_entity)
  779. if to_entity and to_entity not in visited_nodes:
  780. visited_nodes.add(to_entity)
  781. next_frontier.add(to_entity)
  782. if len(selected_edges) >= top_edges:
  783. break
  784. frontier = next_frontier
  785. if len(selected_edges) >= top_edges:
  786. break
  787. if center_entity and center_entity not in visited_nodes:
  788. visited_nodes.add(center_entity)
  789. # 查询节点详情
  790. entity_map: Dict[str, Dict[str, Any]] = {}
  791. if visited_nodes:
  792. with self._get_conn() as conn:
  793. cursor = conn.cursor()
  794. placeholders = ",".join("?" for _ in visited_nodes)
  795. cursor.execute(
  796. f"""
  797. SELECT id, canonical_name, type, tier, last_appearance
  798. FROM entities
  799. WHERE id IN ({placeholders})
  800. """,
  801. tuple(visited_nodes),
  802. )
  803. for row in cursor.fetchall():
  804. entity_map[str(row["id"])] = {
  805. "id": str(row["id"]),
  806. "name": str(row["canonical_name"] or row["id"]),
  807. "type": str(row["type"] or "未知"),
  808. "tier": str(row["tier"] or "装饰"),
  809. "last_appearance": int(row["last_appearance"] or 0),
  810. }
  811. nodes: List[Dict[str, Any]] = []
  812. for entity_id in sorted(
  813. visited_nodes,
  814. key=lambda eid: (
  815. 0 if eid == center_entity else 1,
  816. -(entity_map.get(eid, {}).get("last_appearance", 0)),
  817. eid,
  818. ),
  819. ):
  820. if entity_id in entity_map:
  821. nodes.append(entity_map[entity_id])
  822. else:
  823. nodes.append(
  824. {
  825. "id": entity_id,
  826. "name": entity_id or "未知",
  827. "type": "未知",
  828. "tier": "装饰",
  829. "last_appearance": 0,
  830. }
  831. )
  832. return {
  833. "center": center_entity,
  834. "depth": depth,
  835. "chapter": chapter,
  836. "nodes": nodes,
  837. "edges": selected_edges[:top_edges],
  838. "generated_at": datetime.now().isoformat(timespec="seconds"),
  839. }
  840. def _sanitize_mermaid_node_id(self, raw_id: str) -> str:
  841. safe = re.sub(r"[^0-9a-zA-Z_]", "_", str(raw_id or "node"))
  842. if not safe:
  843. safe = "node"
  844. if safe[0].isdigit():
  845. safe = f"n_{safe}"
  846. return safe
  847. def render_relationship_subgraph_mermaid(self, graph: Dict[str, Any]) -> str:
  848. """将关系子图渲染为 Mermaid。"""
  849. lines = ["```mermaid", "graph LR"]
  850. nodes = graph.get("nodes") or []
  851. edges = graph.get("edges") or []
  852. if not nodes:
  853. lines.append(" EMPTY[暂无关系数据]")
  854. lines.append("```")
  855. return "\n".join(lines)
  856. node_alias: Dict[str, str] = {}
  857. for node in nodes:
  858. entity_id = str(node.get("id") or "")
  859. if not entity_id:
  860. continue
  861. alias = self._sanitize_mermaid_node_id(entity_id)
  862. node_alias[entity_id] = alias
  863. label = str(node.get("name") or entity_id).replace('"', "'")
  864. lines.append(f' {alias}["{label}"]')
  865. for edge in edges:
  866. from_entity = str(edge.get("from") or "")
  867. to_entity = str(edge.get("to") or "")
  868. if from_entity not in node_alias or to_entity not in node_alias:
  869. continue
  870. edge_type = str(edge.get("type") or "关联")
  871. chapter = edge.get("chapter")
  872. chapter_suffix = f"@{chapter}" if chapter not in (None, "") else ""
  873. label = f"{edge_type}{chapter_suffix}".replace('"', "'")
  874. try:
  875. polarity = int(edge.get("polarity", 0) or 0)
  876. except (TypeError, ValueError):
  877. polarity = 0
  878. if polarity < 0:
  879. connector = "-.->"
  880. else:
  881. connector = "-->"
  882. lines.append(
  883. f" {node_alias[from_entity]} {connector}|{label}| {node_alias[to_entity]}"
  884. )
  885. lines.append("```")
  886. return "\n".join(lines)
  887. # ==================== v5.3 Override Contract 操作 ====================
  888. def update_entity_field(self, entity_id: str, field: str, value: Any) -> bool:
  889. """Compatibility helper to update a single entity field in current_json."""
  890. return self.update_entity_current(entity_id, {field: value})