writer.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 章节结果 -> 长期记忆项映射。
  5. """
  6. from __future__ import annotations
  7. import hashlib
  8. from typing import Any, Dict, List
  9. from ..config import DataModulesConfig, get_config
  10. from .schema import MemoryItem
  11. from .store import ScratchpadManager
  12. class MemoryWriter:
  13. def __init__(self, config: DataModulesConfig | None = None):
  14. self.config = config or get_config()
  15. self.store = ScratchpadManager(self.config)
  16. def _item_id(self, category: str, subject: str, field: str, chapter: int) -> str:
  17. raw = f"{category}|{subject}|{field}|{chapter}"
  18. digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
  19. return f"mem-{category}-{digest}"
  20. def _upsert(self, item: MemoryItem, stats: Dict[str, Any]) -> None:
  21. result = self.store.upsert_item(item)
  22. stats["items_added"] += int(result.get("added", 0))
  23. stats["items_updated"] += int(result.get("updated", 0))
  24. stats["items_outdated"] += int(result.get("outdated", 0))
  25. def update_from_chapter_result(self, chapter: int, result: Dict[str, Any]) -> Dict[str, Any]:
  26. stats: Dict[str, Any] = {
  27. "chapter": int(chapter),
  28. "items_added": 0,
  29. "items_updated": 0,
  30. "items_outdated": 0,
  31. "warnings": [],
  32. }
  33. # Stage 2: 零成本结构化映射
  34. for change in result.get("state_changes", []) or []:
  35. entity_id = str(change.get("entity_id", "") or "").strip()
  36. field = str(change.get("field", "") or "").strip()
  37. if not entity_id or not field:
  38. continue
  39. item = MemoryItem(
  40. id=self._item_id("character_state", entity_id, field, chapter),
  41. layer="semantic",
  42. category="character_state",
  43. subject=entity_id,
  44. field=field,
  45. value=str(change.get("new", "") or ""),
  46. payload={"old_value": change.get("old")},
  47. source_chapter=int(chapter),
  48. evidence=[f"state_change:{entity_id}:{field}:{chapter}"],
  49. )
  50. self._upsert(item, stats)
  51. for entity in result.get("entities_new", []) or []:
  52. entity_id = str(entity.get("suggested_id") or entity.get("id") or "").strip()
  53. name = str(entity.get("name", "") or "").strip()
  54. if not entity_id:
  55. continue
  56. item = MemoryItem(
  57. id=self._item_id("character_state", entity_id, "first_seen", chapter),
  58. layer="semantic",
  59. category="character_state",
  60. subject=entity_id,
  61. field="first_seen",
  62. value=name,
  63. payload={"tier": entity.get("tier"), "type": entity.get("type")},
  64. source_chapter=int(chapter),
  65. evidence=[f"entity_new:{entity_id}:{chapter}"],
  66. )
  67. self._upsert(item, stats)
  68. for rel in result.get("relationships_new", []) or []:
  69. from_entity = str(rel.get("from") or rel.get("from_entity") or "").strip()
  70. to_entity = str(rel.get("to") or rel.get("to_entity") or "").strip()
  71. rel_type = str(rel.get("type", "") or "").strip()
  72. if not from_entity or not to_entity:
  73. continue
  74. item = MemoryItem(
  75. id=self._item_id("relationship", from_entity, to_entity, chapter),
  76. layer="semantic",
  77. category="relationship",
  78. subject=from_entity,
  79. field=to_entity,
  80. value=rel_type,
  81. payload={"description": rel.get("description", ""), "to_entity": to_entity},
  82. source_chapter=int(chapter),
  83. evidence=[f"relationship:{from_entity}:{to_entity}:{chapter}"],
  84. )
  85. self._upsert(item, stats)
  86. chapter_meta = result.get("chapter_meta") or {}
  87. hook = chapter_meta.get("hook")
  88. if isinstance(hook, dict):
  89. hook_content = str(hook.get("content", "") or "").strip()
  90. if hook_content:
  91. item = MemoryItem(
  92. id=self._item_id("story_fact", "chapter_hook", str(chapter), chapter),
  93. layer="semantic",
  94. category="story_fact",
  95. subject="chapter_hook",
  96. field=str(chapter),
  97. value=hook_content,
  98. payload={"hook_type": hook.get("type"), "strength": hook.get("strength")},
  99. source_chapter=int(chapter),
  100. evidence=[f"chapter_meta:hook:{chapter}"],
  101. )
  102. self._upsert(item, stats)
  103. elif isinstance(hook, str) and hook.strip():
  104. item = MemoryItem(
  105. id=self._item_id("story_fact", "chapter_hook", str(chapter), chapter),
  106. layer="semantic",
  107. category="story_fact",
  108. subject="chapter_hook",
  109. field=str(chapter),
  110. value=hook.strip(),
  111. payload={},
  112. source_chapter=int(chapter),
  113. evidence=[f"chapter_meta:hook:{chapter}"],
  114. )
  115. self._upsert(item, stats)
  116. # Stage 4: Data Agent 深度提取扩展
  117. memory_facts = result.get("memory_facts") or {}
  118. if isinstance(memory_facts, dict):
  119. self._apply_memory_facts(chapter, memory_facts, stats)
  120. return stats
  121. def _apply_memory_facts(self, chapter: int, memory_facts: Dict[str, Any], stats: Dict[str, Any]) -> None:
  122. timeline_events = memory_facts.get("timeline_events") or []
  123. for row in timeline_events:
  124. if not isinstance(row, dict):
  125. continue
  126. event = str(row.get("event", "") or "").strip()
  127. if not event:
  128. continue
  129. source_chapter = int(row.get("chapter") or chapter)
  130. item = MemoryItem(
  131. id=self._item_id("timeline", event, str(source_chapter), chapter),
  132. layer="semantic",
  133. category="timeline",
  134. subject=event[:64],
  135. field="event",
  136. value=event,
  137. payload={"time_hint": row.get("time_hint"), "event_type": row.get("event_type")},
  138. source_chapter=source_chapter,
  139. evidence=[f"memory_facts:timeline:{chapter}"],
  140. )
  141. self._upsert(item, stats)
  142. world_rules = memory_facts.get("world_rules") or []
  143. for row in world_rules:
  144. if not isinstance(row, dict):
  145. continue
  146. rule = str(row.get("rule", "") or "").strip()
  147. if not rule:
  148. continue
  149. subject = (
  150. str(row.get("domain", "") or "").strip()
  151. or str(row.get("scope", "") or "").strip()
  152. or "global"
  153. )
  154. field = str(row.get("field", "") or "").strip() or rule[:32]
  155. item = MemoryItem(
  156. id=self._item_id("world_rule", subject, field, chapter),
  157. layer="semantic",
  158. category="world_rule",
  159. subject=subject,
  160. field=field,
  161. value=rule,
  162. payload={"scope": row.get("scope"), "rule_text": rule},
  163. source_chapter=int(chapter),
  164. evidence=[f"memory_facts:world_rule:{chapter}"],
  165. )
  166. self._upsert(item, stats)
  167. open_loops = memory_facts.get("open_loops") or []
  168. for row in open_loops:
  169. if not isinstance(row, dict):
  170. continue
  171. content = str(row.get("content", "") or "").strip()
  172. if not content:
  173. continue
  174. item = MemoryItem(
  175. id=self._item_id("open_loop", content, "status", chapter),
  176. layer="semantic",
  177. category="open_loop",
  178. subject=content,
  179. field="status",
  180. value=content,
  181. payload={
  182. "urgency": row.get("urgency"),
  183. "planted_chapter": row.get("planted_chapter"),
  184. "expected_payoff": row.get("expected_payoff"),
  185. "status": row.get("status"),
  186. },
  187. source_chapter=int(chapter),
  188. evidence=[f"memory_facts:open_loop:{chapter}"],
  189. )
  190. self._upsert(item, stats)
  191. reader_promises = memory_facts.get("reader_promises") or []
  192. for row in reader_promises:
  193. if not isinstance(row, dict):
  194. continue
  195. content = str(row.get("content", "") or "").strip()
  196. if not content:
  197. continue
  198. item = MemoryItem(
  199. id=self._item_id("reader_promise", content, "promise", chapter),
  200. layer="semantic",
  201. category="reader_promise",
  202. subject=content,
  203. field="promise",
  204. value=content,
  205. payload={"promise_type": row.get("type"), "target": row.get("target")},
  206. source_chapter=int(chapter),
  207. evidence=[f"memory_facts:reader_promise:{chapter}"],
  208. )
  209. self._upsert(item, stats)
  210. def apply_commit_projection(self, commit_payload: Dict[str, Any]) -> Dict[str, Any]:
  211. chapter = int((commit_payload.get("meta") or {}).get("chapter") or 0)
  212. entity_deltas = list(commit_payload.get("entity_deltas") or [])
  213. accepted_events = list(commit_payload.get("accepted_events") or [])
  214. memory_facts: Dict[str, Any] = {
  215. "timeline_events": [],
  216. "world_rules": [],
  217. "open_loops": [],
  218. "reader_promises": [],
  219. }
  220. for event in accepted_events:
  221. if not isinstance(event, dict):
  222. continue
  223. event_type = str(event.get("event_type") or "").strip()
  224. payload = event.get("payload") or {}
  225. if event_type in {"world_rule_revealed", "world_rule_broken"}:
  226. rule_text = str(payload.get("proposed_value") or payload.get("rule") or payload.get("base_value") or "").strip()
  227. if rule_text:
  228. memory_facts["world_rules"].append(
  229. {
  230. "rule": rule_text,
  231. "scope": payload.get("scope") or "global",
  232. "domain": payload.get("domain") or event.get("subject") or "global",
  233. "field": payload.get("field") or event_type,
  234. }
  235. )
  236. elif event_type == "open_loop_created":
  237. content = str(payload.get("content") or event.get("subject") or "").strip()
  238. if content:
  239. memory_facts["open_loops"].append(
  240. {
  241. "content": content,
  242. "status": payload.get("status") or "active",
  243. "urgency": payload.get("urgency") or 0,
  244. }
  245. )
  246. elif event_type in {"promise_created", "promise_paid_off"}:
  247. content = str(payload.get("content") or event.get("subject") or "").strip()
  248. if content:
  249. memory_facts["reader_promises"].append(
  250. {
  251. "content": content,
  252. "type": payload.get("type") or event_type,
  253. "target": payload.get("target") or event.get("subject") or "",
  254. }
  255. )
  256. result = {
  257. "entities_new": [
  258. {
  259. "suggested_id": row.get("entity_id") or row.get("id"),
  260. "name": row.get("canonical_name") or row.get("name") or row.get("entity_id") or row.get("id"),
  261. "type": row.get("type") or "角色",
  262. "tier": row.get("tier") or "装饰",
  263. }
  264. for row in entity_deltas
  265. if isinstance(row, dict)
  266. and str(row.get("entity_id") or row.get("id") or "").strip()
  267. and not (row.get("from_entity") or row.get("from"))
  268. ],
  269. "state_changes": list(commit_payload.get("state_deltas") or []),
  270. "relationships_new": [
  271. {
  272. "from": row.get("from_entity") or row.get("from"),
  273. "to": row.get("to_entity") or row.get("to"),
  274. "type": row.get("relation_type") or row.get("relationship_type") or row.get("type"),
  275. "description": row.get("description") or "",
  276. }
  277. for row in entity_deltas
  278. if isinstance(row, dict)
  279. and str(row.get("from_entity") or row.get("from") or "").strip()
  280. and str(row.get("to_entity") or row.get("to") or "").strip()
  281. ],
  282. "memory_facts": memory_facts,
  283. }
  284. return self.update_from_chapter_result(chapter, result)