sql_state_manager.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. SQL State Manager - SQLite 状态管理模块 (v5.4)
  5. 基于 IndexManager 扩展,提供与 StateManager 兼容的高级接口,
  6. 将大数据(实体、别名、状态变化、关系)存储到 SQLite 而非 JSON。
  7. 目标(v5.1 引入,v5.4 沿用):
  8. - 替代 state.json 中的大数据字段
  9. - 保持与 Data Agent / Context Agent 的接口兼容
  10. - 支持增量写入和按需查询
  11. """
  12. import json
  13. from typing import Dict, List, Optional, Any
  14. from dataclasses import dataclass, field
  15. from datetime import datetime
  16. from .index_manager import (
  17. IndexManager,
  18. EntityMeta,
  19. StateChangeMeta,
  20. RelationshipMeta
  21. )
  22. from .config import get_config
  23. from .observability import safe_log_tool_call
  24. @dataclass
  25. class EntityData:
  26. """实体数据(用于 Data Agent 输入)"""
  27. id: str
  28. type: str # 角色/地点/物品/势力/招式
  29. name: str
  30. tier: str = "装饰"
  31. desc: str = ""
  32. current: Dict[str, Any] = field(default_factory=dict)
  33. aliases: List[str] = field(default_factory=list)
  34. first_appearance: int = 0
  35. last_appearance: int = 0
  36. is_protagonist: bool = False
  37. class SQLStateManager:
  38. """
  39. SQLite 状态管理器(v5.1 引入,v5.4 沿用)
  40. 提供与 StateManager 兼容的接口,但数据存储在 SQLite (index.db) 中。
  41. 用于替代 state.json 中膨胀的数据结构。
  42. 用法:
  43. ```python
  44. manager = SQLStateManager(config)
  45. # 写入实体
  46. manager.upsert_entity(EntityData(
  47. id="xiaoyan",
  48. type="角色",
  49. name="萧炎",
  50. tier="核心",
  51. current={"realm": "斗师", "location": "天云宗"},
  52. aliases=["小炎子", "废柴"],
  53. is_protagonist=True
  54. ))
  55. # 写入状态变化
  56. manager.record_state_change(
  57. entity_id="xiaoyan",
  58. field="realm",
  59. old_value="斗者",
  60. new_value="斗师",
  61. reason="闭关突破",
  62. chapter=100
  63. )
  64. # 写入关系
  65. manager.upsert_relationship(
  66. from_entity="xiaoyan",
  67. to_entity="yaolao",
  68. type="师徒",
  69. description="药老收萧炎为徒",
  70. chapter=5
  71. )
  72. # 读取
  73. protagonist = manager.get_protagonist()
  74. core_entities = manager.get_core_entities()
  75. changes = manager.get_recent_state_changes(limit=50)
  76. ```
  77. """
  78. # v5.0 引入的实体类型
  79. ENTITY_TYPES = ["角色", "地点", "物品", "势力", "招式"]
  80. def __init__(self, config=None):
  81. self.config = config or get_config()
  82. self._index_manager = IndexManager(config)
  83. # ==================== 实体操作 ====================
  84. def upsert_entity(self, entity: EntityData) -> bool:
  85. """
  86. 插入或更新实体
  87. 自动处理:
  88. - 实体基本信息写入 entities 表
  89. - 别名写入 aliases 表
  90. - canonical_name 自动添加为别名
  91. 返回: 是否为新实体
  92. """
  93. # 构建 EntityMeta
  94. meta = EntityMeta(
  95. id=entity.id,
  96. type=entity.type,
  97. canonical_name=entity.name,
  98. tier=entity.tier,
  99. desc=entity.desc,
  100. current=entity.current,
  101. first_appearance=entity.first_appearance,
  102. last_appearance=entity.last_appearance,
  103. is_protagonist=entity.is_protagonist,
  104. is_archived=False
  105. )
  106. is_new = self._index_manager.upsert_entity(meta)
  107. # 注册别名
  108. # 1. canonical_name 本身作为别名
  109. self._index_manager.register_alias(entity.name, entity.id, entity.type)
  110. # 2. 其他别名
  111. for alias in entity.aliases:
  112. if alias and alias != entity.name:
  113. self._index_manager.register_alias(alias, entity.id, entity.type)
  114. return is_new
  115. def get_entity(self, entity_id: str) -> Optional[Dict]:
  116. """获取实体详情"""
  117. entity = self._index_manager.get_entity(entity_id)
  118. if entity:
  119. # 添加别名
  120. entity["aliases"] = self._index_manager.get_entity_aliases(entity_id)
  121. return entity
  122. def get_entities_by_type(self, entity_type: str, include_archived: bool = False) -> List[Dict]:
  123. """按类型获取实体"""
  124. entities = self._index_manager.get_entities_by_type(entity_type, include_archived)
  125. for e in entities:
  126. e["aliases"] = self._index_manager.get_entity_aliases(e["id"])
  127. return entities
  128. def get_core_entities(self) -> List[Dict]:
  129. """
  130. 获取核心实体(用于 Context Agent 全量加载)
  131. 返回所有 tier=核心/重要 或 is_protagonist=1 的实体
  132. (次要/装饰实体按需查询,不全量加载)
  133. """
  134. entities = self._index_manager.get_core_entities()
  135. for e in entities:
  136. e["aliases"] = self._index_manager.get_entity_aliases(e["id"])
  137. return entities
  138. def get_protagonist(self) -> Optional[Dict]:
  139. """获取主角实体"""
  140. protagonist = self._index_manager.get_protagonist()
  141. if protagonist:
  142. protagonist["aliases"] = self._index_manager.get_entity_aliases(protagonist["id"])
  143. return protagonist
  144. def update_entity_current(self, entity_id: str, updates: Dict) -> bool:
  145. """增量更新实体的 current 字段"""
  146. return self._index_manager.update_entity_current(entity_id, updates)
  147. def resolve_alias(self, alias: str) -> List[Dict]:
  148. """
  149. 根据别名解析实体(一对多)
  150. 返回所有匹配的实体
  151. """
  152. return self._index_manager.get_entities_by_alias(alias)
  153. def register_alias(self, alias: str, entity_id: str, entity_type: str) -> bool:
  154. """注册别名"""
  155. return self._index_manager.register_alias(alias, entity_id, entity_type)
  156. # ==================== 状态变化操作 ====================
  157. def record_state_change(
  158. self,
  159. entity_id: str,
  160. field: str,
  161. old_value: Any,
  162. new_value: Any,
  163. reason: str,
  164. chapter: int
  165. ) -> int:
  166. """
  167. 记录状态变化
  168. 返回: 记录 ID
  169. """
  170. change = StateChangeMeta(
  171. entity_id=entity_id,
  172. field=field,
  173. old_value=str(old_value) if old_value is not None else "",
  174. new_value=str(new_value),
  175. reason=reason,
  176. chapter=chapter
  177. )
  178. return self._index_manager.record_state_change(change)
  179. def get_entity_state_changes(self, entity_id: str, limit: int = 20) -> List[Dict]:
  180. """获取实体的状态变化历史"""
  181. return self._index_manager.get_entity_state_changes(entity_id, limit)
  182. def get_recent_state_changes(self, limit: int = 50) -> List[Dict]:
  183. """获取最近的状态变化"""
  184. return self._index_manager.get_recent_state_changes(limit)
  185. def get_chapter_state_changes(self, chapter: int) -> List[Dict]:
  186. """获取某章的所有状态变化"""
  187. return self._index_manager.get_chapter_state_changes(chapter)
  188. # ==================== 关系操作 ====================
  189. def upsert_relationship(
  190. self,
  191. from_entity: str,
  192. to_entity: str,
  193. type: str,
  194. description: str,
  195. chapter: int
  196. ) -> bool:
  197. """
  198. 插入或更新关系
  199. 返回: 是否为新关系
  200. """
  201. rel = RelationshipMeta(
  202. from_entity=from_entity,
  203. to_entity=to_entity,
  204. type=type,
  205. description=description,
  206. chapter=chapter
  207. )
  208. return self._index_manager.upsert_relationship(rel)
  209. def get_entity_relationships(self, entity_id: str, direction: str = "both") -> List[Dict]:
  210. """获取实体的关系"""
  211. return self._index_manager.get_entity_relationships(entity_id, direction)
  212. def get_relationship_between(self, entity1: str, entity2: str) -> List[Dict]:
  213. """获取两个实体之间的所有关系"""
  214. return self._index_manager.get_relationship_between(entity1, entity2)
  215. def get_recent_relationships(self, limit: int = 30) -> List[Dict]:
  216. """获取最近建立的关系"""
  217. return self._index_manager.get_recent_relationships(limit)
  218. # ==================== 批量写入(供 Data Agent 使用) ====================
  219. def process_chapter_entities(
  220. self,
  221. chapter: int,
  222. entities_appeared: List[Dict],
  223. entities_new: List[Dict],
  224. state_changes: List[Dict],
  225. relationships_new: List[Dict]
  226. ) -> Dict[str, int]:
  227. """
  228. 处理章节的实体数据(Data Agent 主入口)
  229. 参数:
  230. - chapter: 章节号
  231. - entities_appeared: 出场的已有实体
  232. [{"id": "xiaoyan", "type": "角色", "mentions": ["萧炎", "他"], "confidence": 0.95}]
  233. - entities_new: 新发现的实体
  234. [{"suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}]
  235. - state_changes: 状态变化
  236. [{"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破"}]
  237. - relationships_new: 新关系
  238. [{"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "初次见面"}]
  239. 返回: 写入统计
  240. """
  241. stats = {
  242. "entities_updated": 0,
  243. "entities_created": 0,
  244. "state_changes": 0,
  245. "relationships": 0,
  246. "aliases": 0
  247. }
  248. # 1. 处理出场实体(更新 last_appearance)
  249. for entity in entities_appeared:
  250. entity_id = entity.get("id")
  251. if not entity_id:
  252. continue
  253. self._index_manager.update_entity_current(entity_id, {}) # 触发 updated_at
  254. # 更新 last_appearance
  255. existing = self._index_manager.get_entity(entity_id)
  256. if existing:
  257. # 使用 SQL 直接更新 last_appearance
  258. self._update_last_appearance(entity_id, chapter)
  259. stats["entities_updated"] += 1
  260. # 记录出场(保留原有逻辑)
  261. self._index_manager.record_appearance(
  262. entity_id=entity_id,
  263. chapter=chapter,
  264. mentions=entity.get("mentions", []),
  265. confidence=entity.get("confidence", 1.0)
  266. )
  267. # 2. 处理新实体
  268. for entity in entities_new:
  269. suggested_id = entity.get("suggested_id") or entity.get("id")
  270. if not suggested_id:
  271. continue
  272. entity_data = EntityData(
  273. id=suggested_id,
  274. type=entity.get("type", "角色"),
  275. name=entity.get("name", suggested_id),
  276. tier=entity.get("tier", "装饰"),
  277. desc=entity.get("desc", ""),
  278. current=entity.get("current", {}),
  279. aliases=entity.get("aliases", []),
  280. first_appearance=chapter,
  281. last_appearance=chapter,
  282. is_protagonist=entity.get("is_protagonist", False)
  283. )
  284. is_new = self.upsert_entity(entity_data)
  285. if is_new:
  286. stats["entities_created"] += 1
  287. else:
  288. stats["entities_updated"] += 1
  289. # 统计别名
  290. stats["aliases"] += 1 + len(entity_data.aliases)
  291. # 记录新实体的首次出场(解决 appearances 缺失问题)
  292. mentions = entity.get("mentions", [])
  293. if not mentions:
  294. mentions = [entity_data.name] # 至少包含实体名
  295. self._index_manager.record_appearance(
  296. entity_id=suggested_id,
  297. chapter=chapter,
  298. mentions=mentions,
  299. confidence=entity.get("confidence", 1.0)
  300. )
  301. # 3. 处理状态变化
  302. for change in state_changes:
  303. entity_id = change.get("entity_id")
  304. if not entity_id:
  305. continue
  306. self.record_state_change(
  307. entity_id=entity_id,
  308. field=change.get("field", ""),
  309. old_value=change.get("old", change.get("old_value", "")),
  310. new_value=change.get("new", change.get("new_value", "")),
  311. reason=change.get("reason", ""),
  312. chapter=chapter
  313. )
  314. stats["state_changes"] += 1
  315. # 同步更新实体的 current
  316. field_name = change.get("field")
  317. new_value = change.get("new", change.get("new_value"))
  318. # 注意:new_value 可能是 0/""/False 等 falsy 值,需要用 is not None 判断
  319. if field_name and new_value is not None:
  320. self._index_manager.update_entity_current(entity_id, {field_name: new_value})
  321. # 4. 处理新关系
  322. for rel in relationships_new:
  323. from_entity = rel.get("from", rel.get("from_entity"))
  324. to_entity = rel.get("to", rel.get("to_entity"))
  325. if not from_entity or not to_entity:
  326. continue
  327. self.upsert_relationship(
  328. from_entity=from_entity,
  329. to_entity=to_entity,
  330. type=rel.get("type", "相识"),
  331. description=rel.get("description", ""),
  332. chapter=chapter
  333. )
  334. stats["relationships"] += 1
  335. return stats
  336. def _update_last_appearance(self, entity_id: str, chapter: int):
  337. """更新实体的 last_appearance"""
  338. with self._index_manager._get_conn() as conn:
  339. cursor = conn.cursor()
  340. cursor.execute("""
  341. UPDATE entities SET
  342. last_appearance = MAX(last_appearance, ?),
  343. updated_at = CURRENT_TIMESTAMP
  344. WHERE id = ?
  345. """, (chapter, entity_id))
  346. conn.commit()
  347. # ==================== 统计 ====================
  348. def get_stats(self) -> Dict[str, int]:
  349. """获取统计信息"""
  350. return self._index_manager.get_stats()
  351. # ==================== 格式转换(兼容性) ====================
  352. def export_to_entities_v3_format(self) -> Dict[str, Dict[str, Dict]]:
  353. """
  354. 导出为 entities_v3 格式(用于兼容性)
  355. 返回: {"角色": {"xiaoyan": {...}}, "地点": {...}, ...}
  356. """
  357. result = {t: {} for t in self.ENTITY_TYPES}
  358. for entity_type in self.ENTITY_TYPES:
  359. entities = self.get_entities_by_type(entity_type, include_archived=True)
  360. for e in entities:
  361. entity_dict = {
  362. "canonical_name": e.get("canonical_name"),
  363. "name": e.get("canonical_name"), # 兼容性别名
  364. "tier": e.get("tier", "装饰"),
  365. "aliases": e.get("aliases", []),
  366. "desc": e.get("desc", ""),
  367. "current": e.get("current_json", {}),
  368. "history": [], # 历史记录需要从 state_changes 表查询
  369. "first_appearance": e.get("first_appearance", 0),
  370. "last_appearance": e.get("last_appearance", 0)
  371. }
  372. if e.get("is_protagonist"):
  373. entity_dict["is_protagonist"] = True
  374. result[entity_type][e["id"]] = entity_dict
  375. return result
  376. def export_to_alias_index_format(self) -> Dict[str, List[Dict[str, str]]]:
  377. """
  378. 导出为 alias_index 格式(用于兼容性)
  379. 返回: {"萧炎": [{"type": "角色", "id": "xiaoyan"}], ...}
  380. """
  381. result = {}
  382. with self._index_manager._get_conn() as conn:
  383. cursor = conn.cursor()
  384. cursor.execute("SELECT alias, entity_id, entity_type FROM aliases")
  385. for row in cursor.fetchall():
  386. alias = row["alias"]
  387. if alias not in result:
  388. result[alias] = []
  389. result[alias].append({
  390. "type": row["entity_type"],
  391. "id": row["entity_id"]
  392. })
  393. return result
  394. # ==================== CLI 接口 ====================
  395. def main():
  396. import argparse
  397. from .cli_output import print_success, print_error
  398. from .index_manager import IndexManager
  399. parser = argparse.ArgumentParser(description="SQL State Manager CLI (v5.4)")
  400. parser.add_argument("--project-root", type=str, help="项目根目录")
  401. subparsers = parser.add_subparsers(dest="command")
  402. # 获取统计
  403. subparsers.add_parser("stats")
  404. # 获取主角
  405. subparsers.add_parser("get-protagonist")
  406. # 获取核心实体
  407. subparsers.add_parser("get-core-entities")
  408. # 导出 entities_v3 格式
  409. subparsers.add_parser("export-entities-v3")
  410. # 导出 alias_index 格式
  411. subparsers.add_parser("export-alias-index")
  412. # 处理章节数据
  413. process_parser = subparsers.add_parser("process-chapter")
  414. process_parser.add_argument("--chapter", type=int, required=True)
  415. process_parser.add_argument("--data", required=True, help="JSON 格式的章节数据")
  416. args = parser.parse_args()
  417. # 初始化
  418. config = None
  419. if args.project_root:
  420. from .config import DataModulesConfig
  421. config = DataModulesConfig.from_project_root(args.project_root)
  422. manager = SQLStateManager(config)
  423. logger = IndexManager(config)
  424. tool_name = f"sql_state_manager:{args.command or 'unknown'}"
  425. def emit_success(data=None, message: str = "ok"):
  426. print_success(data, message=message)
  427. safe_log_tool_call(logger, tool_name=tool_name, success=True)
  428. def emit_error(code: str, message: str, suggestion: str | None = None):
  429. print_error(code, message, suggestion=suggestion)
  430. safe_log_tool_call(
  431. logger,
  432. tool_name=tool_name,
  433. success=False,
  434. error_code=code,
  435. error_message=message,
  436. )
  437. if args.command == "stats":
  438. stats = manager.get_stats()
  439. emit_success(stats, message="stats")
  440. elif args.command == "get-protagonist":
  441. protagonist = manager.get_protagonist()
  442. if protagonist:
  443. emit_success(protagonist, message="protagonist")
  444. else:
  445. emit_error("NOT_FOUND", "未设置主角")
  446. elif args.command == "get-core-entities":
  447. entities = manager.get_core_entities()
  448. emit_success(entities, message="core_entities")
  449. elif args.command == "export-entities-v3":
  450. data = manager.export_to_entities_v3_format()
  451. emit_success(data, message="entities_v3")
  452. elif args.command == "export-alias-index":
  453. data = manager.export_to_alias_index_format()
  454. emit_success(data, message="alias_index")
  455. elif args.command == "process-chapter":
  456. data = json.loads(args.data)
  457. stats = manager.process_chapter_entities(
  458. chapter=args.chapter,
  459. entities_appeared=data.get("entities_appeared", []),
  460. entities_new=data.get("entities_new", []),
  461. state_changes=data.get("state_changes", []),
  462. relationships_new=data.get("relationships_new", []),
  463. )
  464. emit_success(stats, message="chapter_processed")
  465. else:
  466. emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
  467. if __name__ == "__main__":
  468. main()