deeptutor-memory 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. deeptutor/agents/loop/native_memory.py +24 -0
  2. deeptutor/api/routers/memory.py +785 -0
  3. deeptutor/services/memory/__init__.py +42 -0
  4. deeptutor/services/memory/consolidator/__init__.py +62 -0
  5. deeptutor/services/memory/consolidator/chunker.py +131 -0
  6. deeptutor/services/memory/consolidator/guards.py +104 -0
  7. deeptutor/services/memory/consolidator/line_doc.py +486 -0
  8. deeptutor/services/memory/consolidator/meta.py +194 -0
  9. deeptutor/services/memory/consolidator/modes/__init__.py +31 -0
  10. deeptutor/services/memory/consolidator/modes/_runtime.py +319 -0
  11. deeptutor/services/memory/consolidator/modes/_shims.py +129 -0
  12. deeptutor/services/memory/consolidator/modes/audit.py +481 -0
  13. deeptutor/services/memory/consolidator/modes/dedup.py +228 -0
  14. deeptutor/services/memory/consolidator/modes/merge.py +227 -0
  15. deeptutor/services/memory/consolidator/modes/update.py +706 -0
  16. deeptutor/services/memory/consolidator/parse.py +135 -0
  17. deeptutor/services/memory/consolidator/prompts/en/_meta.yaml +36 -0
  18. deeptutor/services/memory/consolidator/prompts/en/audit_l2.yaml +42 -0
  19. deeptutor/services/memory/consolidator/prompts/en/audit_l3.yaml +38 -0
  20. deeptutor/services/memory/consolidator/prompts/en/dedup.yaml +37 -0
  21. deeptutor/services/memory/consolidator/prompts/en/update_l2.yaml +45 -0
  22. deeptutor/services/memory/consolidator/prompts/en/update_l3.yaml +51 -0
  23. deeptutor/services/memory/consolidator/prompts/zh/_meta.yaml +35 -0
  24. deeptutor/services/memory/consolidator/prompts/zh/audit_l2.yaml +40 -0
  25. deeptutor/services/memory/consolidator/prompts/zh/audit_l3.yaml +34 -0
  26. deeptutor/services/memory/consolidator/prompts/zh/dedup.yaml +34 -0
  27. deeptutor/services/memory/consolidator/prompts/zh/update_l2.yaml +40 -0
  28. deeptutor/services/memory/consolidator/prompts/zh/update_l3.yaml +43 -0
  29. deeptutor/services/memory/consolidator/references.py +388 -0
  30. deeptutor/services/memory/consolidator/runs.py +405 -0
  31. deeptutor/services/memory/document.py +235 -0
  32. deeptutor/services/memory/ids.py +80 -0
  33. deeptutor/services/memory/ops.py +149 -0
  34. deeptutor/services/memory/paths.py +104 -0
  35. deeptutor/services/memory/prompts/en.yaml +156 -0
  36. deeptutor/services/memory/prompts/zh.yaml +144 -0
  37. deeptutor/services/memory/recall.py +237 -0
  38. deeptutor/services/memory/settings.py +198 -0
  39. deeptutor/services/memory/snapshot/__init__.py +127 -0
  40. deeptutor/services/memory/snapshot/adapters.py +690 -0
  41. deeptutor/services/memory/snapshot/diff.py +67 -0
  42. deeptutor/services/memory/snapshot/entity.py +92 -0
  43. deeptutor/services/memory/snapshot/store.py +113 -0
  44. deeptutor/services/memory/store.py +453 -0
  45. deeptutor/services/memory/trace.py +153 -0
  46. deeptutor_cli/memory.py +99 -0
  47. deeptutor_memory-1.7.0.dist-info/METADATA +6 -0
  48. deeptutor_memory-1.7.0.dist-info/RECORD +50 -0
  49. deeptutor_memory-1.7.0.dist-info/WHEEL +5 -0
  50. deeptutor_memory-1.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,227 @@
1
+ """Merge mode — consolidate footnote references on a single doc.
2
+
3
+ This mode is a *no-LLM* refactor pass. It loads the L2 or L3 document
4
+ and rewrites it through :func:`serialize`, which:
5
+
6
+ 1. Migrates legacy entry-keyed footnotes (``[^m_xxx]: r1, r2``) to the
7
+ new ref-keyed layout (``[^1]: r1``, ``[^2]: r2``).
8
+ 2. Collapses duplicate footnote definitions — N entries citing the same
9
+ source share one footnote label, so the rendered view stops repeating
10
+ ``notebook:3a563e6f`` once per entry.
11
+ 3. Re-numbers labels in first-appearance order so the output is stable.
12
+
13
+ For **L3 docs only**, merge ALSO runs a one-shot data migration: every
14
+ legacy ``m_<ULID>`` ref (which used to point at an L2 entry) is resolved
15
+ to its owning surface name, so the doc becomes citeable as
16
+ ``L3 → L2 md → L1 raw traces``. After the next update pass nothing is
17
+ left for this migration to do — it's a pure clean-up of pre-pivot docs.
18
+
19
+ Merge is invoked either:
20
+
21
+ * automatically after a successful :func:`run_update`, :func:`run_audit`,
22
+ or :func:`run_dedup` (controlled by the three
23
+ ``memory.merge.auto_after_*`` settings), or
24
+ * explicitly via the workbench `[Merge]` button → ``mode="merge"``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+ import logging
31
+ import re
32
+
33
+ from deeptutor.services.memory import paths
34
+ from deeptutor.services.memory.consolidator.modes._runtime import (
35
+ OnEvent,
36
+ emit,
37
+ load_doc,
38
+ write_doc_checkpoint,
39
+ )
40
+ from deeptutor.services.memory.ids import is_entry_id
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ # Cheap pre-write check: count footnote definitions in the on-disk text to
45
+ # tell the workbench how many rows the merge collapsed. We do this on the
46
+ # *raw* bytes rather than on parsed entries because the parsed model
47
+ # already has consolidated refs (`Entry.refs` carries unique refs per
48
+ # entry), so the meaningful "before" number is what's in the file.
49
+ _FOOTNOTE_DEF_RE = re.compile(r"^\[\^[^\]]+\]:\s*", re.MULTILINE)
50
+
51
+
52
+ @dataclass
53
+ class MergeResult:
54
+ layer: str
55
+ key: str
56
+ footnote_rows_before: int
57
+ footnote_rows_after: int
58
+ rewrote: bool
59
+ legacy_l3_refs_migrated: int = 0
60
+
61
+
62
+ async def run_merge(
63
+ layer: str,
64
+ key: str,
65
+ *,
66
+ language: str = "en",
67
+ user_label: str = "anonymous",
68
+ on_event: OnEvent | None = None,
69
+ ) -> MergeResult:
70
+ """Re-serialize ``layer/key``; collapse duplicate refs into one footnote each.
71
+
72
+ Idempotent: re-running on an already-merged doc rewrites the same
73
+ bytes and reports ``rewrote=False`` (no checkpoint is pushed).
74
+ """
75
+ # NOTE: ``language`` / ``user_label`` are accepted for signature symmetry
76
+ # with the other modes; merge itself does no localized work and no LLM
77
+ # calls, so neither is used inside the body.
78
+ del language, user_label
79
+
80
+ path = _path_for(layer, key)
81
+ if not path.exists():
82
+ await emit(on_event, {"stage": "done", "no_doc": True, "rewrote": False})
83
+ return MergeResult(
84
+ layer=layer, key=key, footnote_rows_before=0, footnote_rows_after=0, rewrote=False
85
+ )
86
+
87
+ raw_before = path.read_text(encoding="utf-8")
88
+ rows_before = len(_FOOTNOTE_DEF_RE.findall(raw_before))
89
+ doc = load_doc(path, default_title=_default_title(layer, key))
90
+
91
+ legacy_migrated = 0
92
+ if layer == "L3":
93
+ legacy_migrated = _migrate_l3_legacy_refs(doc)
94
+
95
+ # Count unique refs across the doc — this is the "after" footnote-row
96
+ # count once :func:`serialize` consolidates them.
97
+ unique_refs: set[str] = set()
98
+ for entry in doc.all_entries():
99
+ for ref in entry.refs:
100
+ unique_refs.add(ref)
101
+ rows_after = len(unique_refs)
102
+
103
+ await emit(
104
+ on_event,
105
+ {
106
+ "stage": "progress",
107
+ "mode": "merge",
108
+ "footnote_rows_before": rows_before,
109
+ "footnote_rows_after": rows_after,
110
+ "legacy_l3_refs_migrated": legacy_migrated,
111
+ },
112
+ )
113
+
114
+ # We always rewrite when the doc has any entries — even when
115
+ # before == after, the act of re-serializing renormalizes whitespace
116
+ # and migrates legacy entry-keyed layouts. Skip only when the file is
117
+ # byte-equal to what :func:`serialize` would produce.
118
+ from deeptutor.services.memory.document import serialize
119
+
120
+ expected = serialize(doc)
121
+ rewrote = raw_before != expected
122
+ if rewrote:
123
+ await write_doc_checkpoint(
124
+ path,
125
+ doc,
126
+ layer=layer,
127
+ key=key,
128
+ on_event=on_event,
129
+ turn=1,
130
+ label="merge",
131
+ action="merge_footnotes",
132
+ )
133
+
134
+ await emit(
135
+ on_event,
136
+ {
137
+ "stage": "done",
138
+ "footnote_rows_before": rows_before,
139
+ "footnote_rows_after": rows_after,
140
+ "rewrote": rewrote,
141
+ "legacy_l3_refs_migrated": legacy_migrated,
142
+ },
143
+ )
144
+ return MergeResult(
145
+ layer=layer,
146
+ key=key,
147
+ footnote_rows_before=rows_before,
148
+ footnote_rows_after=rows_after,
149
+ rewrote=rewrote,
150
+ legacy_l3_refs_migrated=legacy_migrated,
151
+ )
152
+
153
+
154
+ # ── Helpers ─────────────────────────────────────────────────────────────
155
+
156
+
157
+ def _migrate_l3_legacy_refs(doc) -> int:
158
+ """Resolve legacy ``m_<ULID>`` L3 refs to bare surface names.
159
+
160
+ Pre-pivot L3 docs cited L2 entries by their entry id. The current
161
+ design wants surface-level provenance only ("which L2 md did this
162
+ synthesize from"). For each ``m_<ULID>`` ref we scan every L2 md
163
+ for the owning entry and substitute the surface name.
164
+
165
+ Returns the number of entry refs that were migrated. Unresolvable
166
+ ids (entry deleted, or never existed) are dropped silently — the
167
+ next ``run_update`` round will re-synthesize from current L2 text.
168
+ """
169
+ from deeptutor.services.memory.document import parse
170
+
171
+ # Cache L2 entry-id → surface lookups: one scan per L2 md, reused
172
+ # across every L3 ref in the doc.
173
+ l2_owner: dict[str, str] = {}
174
+ for surface in paths.SURFACES:
175
+ l2_path = paths.l2_file(surface)
176
+ if not l2_path.exists():
177
+ continue
178
+ try:
179
+ l2_doc = parse(l2_path.read_text(encoding="utf-8"))
180
+ except Exception: # noqa: BLE001 — malformed L2 should not block L3 migration
181
+ continue
182
+ for entry in l2_doc.all_entries():
183
+ l2_owner.setdefault(entry.id, surface)
184
+
185
+ migrated = 0
186
+ for entry in doc.all_entries():
187
+ if not entry.refs:
188
+ continue
189
+ new_refs: list[str] = []
190
+ seen: set[str] = set()
191
+ for ref in entry.refs:
192
+ if is_entry_id(ref):
193
+ migrated += 1
194
+ resolved = l2_owner.get(ref)
195
+ if resolved is None or resolved in seen:
196
+ continue
197
+ seen.add(resolved)
198
+ new_refs.append(resolved)
199
+ else:
200
+ if ref in seen:
201
+ continue
202
+ seen.add(ref)
203
+ new_refs.append(ref)
204
+ entry.refs = new_refs
205
+ return migrated
206
+
207
+
208
+ def _path_for(layer: str, key: str):
209
+ if layer == "L2":
210
+ return paths.l2_file(key) # type: ignore[arg-type]
211
+ if layer == "L3":
212
+ return paths.l3_file(key) # type: ignore[arg-type]
213
+ raise ValueError(f"unknown layer {layer!r}")
214
+
215
+
216
+ def _default_title(layer: str, key: str) -> str:
217
+ if layer == "L2":
218
+ return f"{key} memory"
219
+ return {
220
+ "recent": "Recent summary",
221
+ "profile": "User profile",
222
+ "scope": "Knowledge scope",
223
+ "preferences": "Preferences",
224
+ }.get(key, f"{key} memory")
225
+
226
+
227
+ __all__ = ["MergeResult", "run_merge"]