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.
- deeptutor/agents/loop/native_memory.py +24 -0
- deeptutor/api/routers/memory.py +785 -0
- deeptutor/services/memory/__init__.py +42 -0
- deeptutor/services/memory/consolidator/__init__.py +62 -0
- deeptutor/services/memory/consolidator/chunker.py +131 -0
- deeptutor/services/memory/consolidator/guards.py +104 -0
- deeptutor/services/memory/consolidator/line_doc.py +486 -0
- deeptutor/services/memory/consolidator/meta.py +194 -0
- deeptutor/services/memory/consolidator/modes/__init__.py +31 -0
- deeptutor/services/memory/consolidator/modes/_runtime.py +319 -0
- deeptutor/services/memory/consolidator/modes/_shims.py +129 -0
- deeptutor/services/memory/consolidator/modes/audit.py +481 -0
- deeptutor/services/memory/consolidator/modes/dedup.py +228 -0
- deeptutor/services/memory/consolidator/modes/merge.py +227 -0
- deeptutor/services/memory/consolidator/modes/update.py +706 -0
- deeptutor/services/memory/consolidator/parse.py +135 -0
- deeptutor/services/memory/consolidator/prompts/en/_meta.yaml +36 -0
- deeptutor/services/memory/consolidator/prompts/en/audit_l2.yaml +42 -0
- deeptutor/services/memory/consolidator/prompts/en/audit_l3.yaml +38 -0
- deeptutor/services/memory/consolidator/prompts/en/dedup.yaml +37 -0
- deeptutor/services/memory/consolidator/prompts/en/update_l2.yaml +45 -0
- deeptutor/services/memory/consolidator/prompts/en/update_l3.yaml +51 -0
- deeptutor/services/memory/consolidator/prompts/zh/_meta.yaml +35 -0
- deeptutor/services/memory/consolidator/prompts/zh/audit_l2.yaml +40 -0
- deeptutor/services/memory/consolidator/prompts/zh/audit_l3.yaml +34 -0
- deeptutor/services/memory/consolidator/prompts/zh/dedup.yaml +34 -0
- deeptutor/services/memory/consolidator/prompts/zh/update_l2.yaml +40 -0
- deeptutor/services/memory/consolidator/prompts/zh/update_l3.yaml +43 -0
- deeptutor/services/memory/consolidator/references.py +388 -0
- deeptutor/services/memory/consolidator/runs.py +405 -0
- deeptutor/services/memory/document.py +235 -0
- deeptutor/services/memory/ids.py +80 -0
- deeptutor/services/memory/ops.py +149 -0
- deeptutor/services/memory/paths.py +104 -0
- deeptutor/services/memory/prompts/en.yaml +156 -0
- deeptutor/services/memory/prompts/zh.yaml +144 -0
- deeptutor/services/memory/recall.py +237 -0
- deeptutor/services/memory/settings.py +198 -0
- deeptutor/services/memory/snapshot/__init__.py +127 -0
- deeptutor/services/memory/snapshot/adapters.py +690 -0
- deeptutor/services/memory/snapshot/diff.py +67 -0
- deeptutor/services/memory/snapshot/entity.py +92 -0
- deeptutor/services/memory/snapshot/store.py +113 -0
- deeptutor/services/memory/store.py +453 -0
- deeptutor/services/memory/trace.py +153 -0
- deeptutor_cli/memory.py +99 -0
- deeptutor_memory-1.7.0.dist-info/METADATA +6 -0
- deeptutor_memory-1.7.0.dist-info/RECORD +50 -0
- deeptutor_memory-1.7.0.dist-info/WHEEL +5 -0
- 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"]
|