schema.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 长期记忆 schema 定义。
  5. """
  6. from __future__ import annotations
  7. from dataclasses import asdict, dataclass, field
  8. from datetime import datetime
  9. from typing import Any, Dict, List
  10. VALID_LAYERS = {"semantic", "episodic"}
  11. VALID_STATUSES = {"active", "outdated", "contradicted", "tentative"}
  12. CATEGORY_TO_BUCKET: Dict[str, str] = {
  13. "character_state": "character_state",
  14. "story_fact": "story_facts",
  15. "world_rule": "world_rules",
  16. "timeline": "timeline",
  17. "open_loop": "open_loops",
  18. "reader_promise": "reader_promises",
  19. "relationship": "relationships",
  20. }
  21. BUCKET_TO_CATEGORY: Dict[str, str] = {v: k for k, v in CATEGORY_TO_BUCKET.items()}
  22. CATEGORY_KEY_RULES: Dict[str, tuple[str, ...]] = {
  23. "character_state": ("subject", "field"),
  24. "relationship": ("subject", "field"),
  25. "world_rule": ("subject", "field"),
  26. "story_fact": ("subject", "field"),
  27. "timeline": ("subject", "source_chapter"),
  28. "open_loop": ("subject",),
  29. "reader_promise": ("subject",),
  30. }
  31. def memory_item_key(item: "MemoryItem") -> tuple:
  32. """根据 category 规则计算 MemoryItem 的去重 key。供 store/compactor 共用。"""
  33. fields = CATEGORY_KEY_RULES.get(item.category)
  34. if not fields:
  35. return (item.id,)
  36. return tuple(getattr(item, f, None) for f in fields)
  37. def now_iso() -> str:
  38. return datetime.now().isoformat(timespec="seconds")
  39. @dataclass
  40. class MemoryItem:
  41. id: str
  42. layer: str
  43. category: str
  44. subject: str
  45. field: str
  46. value: str
  47. payload: Dict[str, Any] = field(default_factory=dict)
  48. status: str = "active"
  49. source_chapter: int = 0
  50. evidence: List[str] = field(default_factory=list)
  51. updated_at: str = ""
  52. def normalized(self) -> "MemoryItem":
  53. layer = self.layer if self.layer in VALID_LAYERS else "semantic"
  54. category = self.category if self.category in CATEGORY_TO_BUCKET else "story_fact"
  55. status = self.status if self.status in VALID_STATUSES else "active"
  56. updated_at = self.updated_at or now_iso()
  57. return MemoryItem(
  58. id=str(self.id or ""),
  59. layer=layer,
  60. category=category,
  61. subject=str(self.subject or ""),
  62. field=str(self.field or ""),
  63. value=str(self.value or ""),
  64. payload=dict(self.payload or {}),
  65. status=status,
  66. source_chapter=int(self.source_chapter or 0),
  67. evidence=[str(x) for x in (self.evidence or []) if str(x)],
  68. updated_at=updated_at,
  69. )
  70. def to_dict(self) -> Dict[str, Any]:
  71. return asdict(self.normalized())
  72. @classmethod
  73. def from_dict(cls, payload: Dict[str, Any]) -> "MemoryItem":
  74. return cls(
  75. id=str(payload.get("id", "")),
  76. layer=str(payload.get("layer", "semantic")),
  77. category=str(payload.get("category", "story_fact")),
  78. subject=str(payload.get("subject", "")),
  79. field=str(payload.get("field", "")),
  80. value=str(payload.get("value", "")),
  81. payload=dict(payload.get("payload") or {}),
  82. status=str(payload.get("status", "active")),
  83. source_chapter=int(payload.get("source_chapter", 0) or 0),
  84. evidence=[str(x) for x in (payload.get("evidence") or []) if str(x)],
  85. updated_at=str(payload.get("updated_at", "")),
  86. ).normalized()
  87. @dataclass
  88. class ScratchpadData:
  89. character_state: List[MemoryItem] = field(default_factory=list)
  90. story_facts: List[MemoryItem] = field(default_factory=list)
  91. world_rules: List[MemoryItem] = field(default_factory=list)
  92. timeline: List[MemoryItem] = field(default_factory=list)
  93. open_loops: List[MemoryItem] = field(default_factory=list)
  94. reader_promises: List[MemoryItem] = field(default_factory=list)
  95. relationships: List[MemoryItem] = field(default_factory=list)
  96. meta: Dict[str, Any] = field(
  97. default_factory=lambda: {"version": 1, "last_updated": "", "total_items": 0}
  98. )
  99. @classmethod
  100. def empty(cls) -> "ScratchpadData":
  101. return cls()
  102. @classmethod
  103. def from_dict(cls, payload: Dict[str, Any]) -> "ScratchpadData":
  104. def _items(bucket: str) -> List[MemoryItem]:
  105. rows = payload.get(bucket, [])
  106. if not isinstance(rows, list):
  107. return []
  108. return [MemoryItem.from_dict(row) for row in rows if isinstance(row, dict)]
  109. data = cls(
  110. character_state=_items("character_state"),
  111. story_facts=_items("story_facts"),
  112. world_rules=_items("world_rules"),
  113. timeline=_items("timeline"),
  114. open_loops=_items("open_loops"),
  115. reader_promises=_items("reader_promises"),
  116. relationships=_items("relationships"),
  117. meta=dict(payload.get("meta") or {}),
  118. )
  119. data.meta.setdefault("version", 1)
  120. data.meta.setdefault("last_updated", "")
  121. data.meta.setdefault("total_items", 0)
  122. data.meta["total_items"] = data.count_items()
  123. return data
  124. def count_items(self) -> int:
  125. return sum(
  126. len(getattr(self, bucket))
  127. for bucket in BUCKET_TO_CATEGORY
  128. )
  129. def to_dict(self) -> Dict[str, Any]:
  130. result: Dict[str, Any] = {}
  131. for bucket in BUCKET_TO_CATEGORY:
  132. result[bucket] = [item.to_dict() for item in getattr(self, bucket)]
  133. meta = dict(self.meta or {})
  134. meta["version"] = int(meta.get("version", 1) or 1)
  135. meta["last_updated"] = meta.get("last_updated") or now_iso()
  136. meta["total_items"] = self.count_items()
  137. result["meta"] = meta
  138. return result