writer.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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.sha1(raw.encode("utf-8")).hexdigest()[:12]
  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,
  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 = str(row.get("domain", "") or "").strip() or str(row.get("scope", "") or "").strip() or "global"
  150. field = str(row.get("field", "") or "").strip() or rule[:32]
  151. item = MemoryItem(
  152. id=self._item_id("world_rule", subject, field, chapter),
  153. layer="semantic",
  154. category="world_rule",
  155. subject=subject,
  156. field=field,
  157. value=rule,
  158. payload={"scope": row.get("scope"), "rule_text": rule},
  159. source_chapter=int(chapter),
  160. evidence=[f"memory_facts:world_rule:{chapter}"],
  161. )
  162. self._upsert(item, stats)
  163. open_loops = memory_facts.get("open_loops") or []
  164. for row in open_loops:
  165. if not isinstance(row, dict):
  166. continue
  167. content = str(row.get("content", "") or "").strip()
  168. if not content:
  169. continue
  170. item = MemoryItem(
  171. id=self._item_id("open_loop", content, "status", chapter),
  172. layer="semantic",
  173. category="open_loop",
  174. subject=content,
  175. field="status",
  176. value=content,
  177. payload={
  178. "urgency": row.get("urgency"),
  179. "planted_chapter": row.get("planted_chapter"),
  180. "expected_payoff": row.get("expected_payoff"),
  181. "status": row.get("status"),
  182. },
  183. source_chapter=int(chapter),
  184. evidence=[f"memory_facts:open_loop:{chapter}"],
  185. )
  186. self._upsert(item, stats)
  187. reader_promises = memory_facts.get("reader_promises") or []
  188. for row in reader_promises:
  189. if not isinstance(row, dict):
  190. continue
  191. content = str(row.get("content", "") or "").strip()
  192. if not content:
  193. continue
  194. item = MemoryItem(
  195. id=self._item_id("reader_promise", content, "promise", chapter),
  196. layer="semantic",
  197. category="reader_promise",
  198. subject=content,
  199. field="promise",
  200. value=content,
  201. payload={"promise_type": row.get("type"), "target": row.get("target")},
  202. source_chapter=int(chapter),
  203. evidence=[f"memory_facts:reader_promise:{chapter}"],
  204. )
  205. self._upsert(item, stats)