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,319 @@
|
|
|
1
|
+
"""Shared runtime helpers used by every mode.
|
|
2
|
+
|
|
3
|
+
These keep the per-mode files focused on algorithm, not plumbing:
|
|
4
|
+
|
|
5
|
+
* Prompt loading (en/zh, cached).
|
|
6
|
+
* SSE event emission (``_emit``).
|
|
7
|
+
* Document load + atomic write.
|
|
8
|
+
* LLM call wrapper with retries + a one-line warning on failure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Awaitable, Callable
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from deeptutor.services.file_io import atomic_write_text as _atomic_write
|
|
22
|
+
from deeptutor.services.llm import clean_thinking_tags
|
|
23
|
+
from deeptutor.services.llm import complete as llm_complete
|
|
24
|
+
from deeptutor.services.llm import stream as llm_stream
|
|
25
|
+
from deeptutor.services.memory.document import Document, parse, serialize
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
OnEvent = Callable[[dict[str, Any]], Awaitable[None]]
|
|
30
|
+
|
|
31
|
+
_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
|
32
|
+
_PROMPT_CACHE: dict[tuple[str, str], dict[str, str]] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_META_CACHE: dict[str, dict[str, Any]] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_prompt(name: str, language: str) -> dict[str, str]:
|
|
39
|
+
"""Load and cache one prompt YAML by name + language (en/zh)."""
|
|
40
|
+
lang = _lang_code(language)
|
|
41
|
+
key = (lang, name)
|
|
42
|
+
cached = _PROMPT_CACHE.get(key)
|
|
43
|
+
if cached is not None:
|
|
44
|
+
return cached
|
|
45
|
+
path = _PROMPTS_DIR / lang / f"{name}.yaml"
|
|
46
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
47
|
+
if not isinstance(data, dict) or "system" not in data or "user" not in data:
|
|
48
|
+
raise RuntimeError(f"prompt {path} missing 'system'/'user' keys")
|
|
49
|
+
_PROMPT_CACHE[key] = {"system": data["system"], "user": data["user"]}
|
|
50
|
+
return _PROMPT_CACHE[key]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_focus_meta(language: str) -> dict[str, Any]:
|
|
54
|
+
"""Load the per-surface / per-slot focus + sections map for a language."""
|
|
55
|
+
lang = _lang_code(language)
|
|
56
|
+
cached = _META_CACHE.get(lang)
|
|
57
|
+
if cached is not None:
|
|
58
|
+
return cached
|
|
59
|
+
path = _PROMPTS_DIR / lang / "_meta.yaml"
|
|
60
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
61
|
+
_META_CACHE[lang] = data
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def surface_focus(language: str, surface: str) -> tuple[str, list[str]]:
|
|
66
|
+
meta = load_focus_meta(language).get("surfaces", {}).get(surface) or {}
|
|
67
|
+
return meta.get("focus", ""), list(meta.get("sections", []) or [])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def slot_focus(language: str, slot: str) -> tuple[str, list[str]]:
|
|
71
|
+
meta = load_focus_meta(language).get("slots", {}).get(slot) or {}
|
|
72
|
+
return meta.get("focus", ""), list(meta.get("sections", []) or [])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _lang_code(language: str) -> str:
|
|
76
|
+
return "zh" if (language or "").lower().startswith("zh") else "en"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def emit(on_event: OnEvent | None, event: dict[str, Any]) -> None:
|
|
80
|
+
if on_event is None:
|
|
81
|
+
return
|
|
82
|
+
try:
|
|
83
|
+
await on_event(event)
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.debug("consolidator: on_event consumer raised", exc_info=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def call_llm(
|
|
89
|
+
*,
|
|
90
|
+
system_prompt: str,
|
|
91
|
+
user_prompt: str,
|
|
92
|
+
temperature: float = 0.2,
|
|
93
|
+
max_tokens: int = 1500,
|
|
94
|
+
on_event: OnEvent | None = None,
|
|
95
|
+
turn: int | None = None,
|
|
96
|
+
chunk_index: int | None = None,
|
|
97
|
+
label: str | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Single LLM call. Returns the raw text body; "" on failure.
|
|
100
|
+
|
|
101
|
+
The model/provider is resolved from the *active* LLM config — the
|
|
102
|
+
mode is expected to have installed a scoped config via
|
|
103
|
+
:func:`activate_llm_selection` if the user picked a non-default
|
|
104
|
+
model. Emits ``llm_io_start`` / ``llm_io_end`` events for the
|
|
105
|
+
workbench trace.
|
|
106
|
+
"""
|
|
107
|
+
from deeptutor.services.llm import get_llm_config
|
|
108
|
+
|
|
109
|
+
model_label = get_llm_config().model or None
|
|
110
|
+
if on_event is not None:
|
|
111
|
+
await on_event(
|
|
112
|
+
{
|
|
113
|
+
"stage": "llm_io_start",
|
|
114
|
+
"turn": turn,
|
|
115
|
+
"chunk_index": chunk_index,
|
|
116
|
+
"label": label,
|
|
117
|
+
"system_prompt": system_prompt,
|
|
118
|
+
"user_prompt": user_prompt,
|
|
119
|
+
"model": model_label,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
response_parts: list[str] = []
|
|
123
|
+
in_think_block = False
|
|
124
|
+
try:
|
|
125
|
+
async for delta in llm_stream(
|
|
126
|
+
prompt=user_prompt,
|
|
127
|
+
system_prompt=system_prompt,
|
|
128
|
+
temperature=temperature,
|
|
129
|
+
max_tokens=max_tokens,
|
|
130
|
+
stream_coalesce_chars=64,
|
|
131
|
+
stream_coalesce_seconds=0.05,
|
|
132
|
+
):
|
|
133
|
+
if not delta:
|
|
134
|
+
continue
|
|
135
|
+
response_parts.append(delta)
|
|
136
|
+
visible_delta, in_think_block = _strip_thinking_delta(delta, in_think_block)
|
|
137
|
+
if on_event is not None:
|
|
138
|
+
if visible_delta:
|
|
139
|
+
await on_event(
|
|
140
|
+
{
|
|
141
|
+
"stage": "llm_io_delta",
|
|
142
|
+
"turn": turn,
|
|
143
|
+
"chunk_index": chunk_index,
|
|
144
|
+
"label": label,
|
|
145
|
+
"delta": visible_delta,
|
|
146
|
+
"model": model_label,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
response = clean_thinking_tags("".join(response_parts))
|
|
150
|
+
if on_event is not None:
|
|
151
|
+
await on_event(
|
|
152
|
+
{
|
|
153
|
+
"stage": "llm_io_end",
|
|
154
|
+
"turn": turn,
|
|
155
|
+
"chunk_index": chunk_index,
|
|
156
|
+
"label": label,
|
|
157
|
+
"response": response,
|
|
158
|
+
"error": None,
|
|
159
|
+
"model": model_label,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return response
|
|
163
|
+
except Exception as exc: # noqa: BLE001
|
|
164
|
+
# Some providers still do not implement streaming. Fall back to the
|
|
165
|
+
# non-streaming path so memory jobs remain usable.
|
|
166
|
+
logger.warning("consolidator streaming LLM call failed; falling back: %s", exc)
|
|
167
|
+
try:
|
|
168
|
+
response = await llm_complete(
|
|
169
|
+
prompt=user_prompt,
|
|
170
|
+
system_prompt=system_prompt,
|
|
171
|
+
temperature=temperature,
|
|
172
|
+
max_tokens=max_tokens,
|
|
173
|
+
)
|
|
174
|
+
response = clean_thinking_tags(response)
|
|
175
|
+
if on_event is not None:
|
|
176
|
+
await on_event(
|
|
177
|
+
{
|
|
178
|
+
"stage": "llm_io_delta",
|
|
179
|
+
"turn": turn,
|
|
180
|
+
"chunk_index": chunk_index,
|
|
181
|
+
"label": label,
|
|
182
|
+
"delta": response,
|
|
183
|
+
"model": model_label,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
await on_event(
|
|
187
|
+
{
|
|
188
|
+
"stage": "llm_io_end",
|
|
189
|
+
"turn": turn,
|
|
190
|
+
"chunk_index": chunk_index,
|
|
191
|
+
"label": label,
|
|
192
|
+
"response": response,
|
|
193
|
+
"error": None,
|
|
194
|
+
"model": model_label,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
return response
|
|
198
|
+
except Exception as fallback_exc: # noqa: BLE001
|
|
199
|
+
logger.warning("consolidator LLM call failed: %s", fallback_exc)
|
|
200
|
+
if on_event is not None:
|
|
201
|
+
await on_event(
|
|
202
|
+
{
|
|
203
|
+
"stage": "llm_io_end",
|
|
204
|
+
"turn": turn,
|
|
205
|
+
"chunk_index": chunk_index,
|
|
206
|
+
"label": label,
|
|
207
|
+
"response": "",
|
|
208
|
+
"error": str(fallback_exc),
|
|
209
|
+
"model": model_label,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def load_doc(path: Path, *, default_title: str) -> Document:
|
|
216
|
+
if not path.exists():
|
|
217
|
+
return Document(title=default_title)
|
|
218
|
+
return parse(path.read_text(encoding="utf-8"))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def write_doc_atomic(path: Path, doc: Document) -> None:
|
|
222
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
text = serialize(doc)
|
|
224
|
+
await asyncio.to_thread(_atomic_write, path, text)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def write_doc_checkpoint(
|
|
228
|
+
path: Path,
|
|
229
|
+
doc: Document,
|
|
230
|
+
*,
|
|
231
|
+
layer: str,
|
|
232
|
+
key: str,
|
|
233
|
+
on_event: OnEvent | None = None,
|
|
234
|
+
turn: int | None = None,
|
|
235
|
+
label: str | None = None,
|
|
236
|
+
action: str = "write",
|
|
237
|
+
) -> int:
|
|
238
|
+
"""Write a doc now and register one run-scoped undo checkpoint."""
|
|
239
|
+
existed = path.exists()
|
|
240
|
+
previous = path.read_text(encoding="utf-8") if existed else ""
|
|
241
|
+
await write_doc_atomic(path, doc)
|
|
242
|
+
from deeptutor.services.memory.consolidator.runs import push_undo_checkpoint
|
|
243
|
+
|
|
244
|
+
undo_depth = push_undo_checkpoint(
|
|
245
|
+
layer=layer,
|
|
246
|
+
key=key,
|
|
247
|
+
path=path,
|
|
248
|
+
existed=existed,
|
|
249
|
+
previous_content=previous,
|
|
250
|
+
action=action,
|
|
251
|
+
turn=turn,
|
|
252
|
+
label=label,
|
|
253
|
+
)
|
|
254
|
+
await emit(
|
|
255
|
+
on_event,
|
|
256
|
+
{
|
|
257
|
+
"stage": "doc_updated",
|
|
258
|
+
"layer": layer,
|
|
259
|
+
"key": key,
|
|
260
|
+
"turn": turn,
|
|
261
|
+
"label": label,
|
|
262
|
+
"action": action,
|
|
263
|
+
"undo_depth": undo_depth,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
return undo_depth
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _strip_thinking_delta(delta: str, in_block: bool) -> tuple[str, bool]:
|
|
270
|
+
"""Remove streamed <think> blocks before they reach the workbench UI."""
|
|
271
|
+
out: list[str] = []
|
|
272
|
+
text = delta
|
|
273
|
+
while text:
|
|
274
|
+
lower = text.lower()
|
|
275
|
+
if in_block:
|
|
276
|
+
close_at = lower.find("</think>")
|
|
277
|
+
close_len = len("</think>")
|
|
278
|
+
alt_close = lower.find("</thinking>")
|
|
279
|
+
if alt_close != -1 and (close_at == -1 or alt_close < close_at):
|
|
280
|
+
close_at = alt_close
|
|
281
|
+
close_len = len("</thinking>")
|
|
282
|
+
if close_at == -1:
|
|
283
|
+
return "".join(out), True
|
|
284
|
+
text = text[close_at + close_len :]
|
|
285
|
+
in_block = False
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
open_at = lower.find("<think>")
|
|
289
|
+
open_len = len("<think>")
|
|
290
|
+
alt_open = lower.find("<thinking>")
|
|
291
|
+
if alt_open != -1 and (open_at == -1 or alt_open < open_at):
|
|
292
|
+
open_at = alt_open
|
|
293
|
+
open_len = len("<thinking>")
|
|
294
|
+
if open_at == -1:
|
|
295
|
+
out.append(text)
|
|
296
|
+
break
|
|
297
|
+
out.append(text[:open_at])
|
|
298
|
+
text = text[open_at + open_len :]
|
|
299
|
+
in_block = True
|
|
300
|
+
return "".join(out), in_block
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def today_iso() -> str:
|
|
304
|
+
return datetime.now(tz=timezone.utc).date().isoformat()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
__all__ = [
|
|
308
|
+
"OnEvent",
|
|
309
|
+
"call_llm",
|
|
310
|
+
"emit",
|
|
311
|
+
"load_doc",
|
|
312
|
+
"load_focus_meta",
|
|
313
|
+
"load_prompt",
|
|
314
|
+
"slot_focus",
|
|
315
|
+
"surface_focus",
|
|
316
|
+
"today_iso",
|
|
317
|
+
"write_doc_atomic",
|
|
318
|
+
"write_doc_checkpoint",
|
|
319
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Legacy public-API shims for :mod:`deeptutor.services.memory.store`.
|
|
2
|
+
|
|
3
|
+
The pre-redesign module exposed ``consolidate_l2`` and ``consolidate_l3``
|
|
4
|
+
returning a :class:`ConsolidateResult`. The store / API router still
|
|
5
|
+
call these names. We keep them as thin wrappers around :func:`run_update`
|
|
6
|
+
so the surface that the router and the test fixtures import doesn't
|
|
7
|
+
change while the implementation switches to chunk-based update + dedup.
|
|
8
|
+
|
|
9
|
+
``apply_ops`` semantics
|
|
10
|
+
-----------------------
|
|
11
|
+
The pre-redesign ``apply_ops=False`` was a "preview the ops, do not
|
|
12
|
+
write" toggle for the workbench. The new chunk-based update writes
|
|
13
|
+
incrementally and there's no clean way to roll it back atomically. So:
|
|
14
|
+
|
|
15
|
+
* ``apply_ops=True`` (default) → :func:`run_update` runs end-to-end.
|
|
16
|
+
* ``apply_ops=False`` → we still run the update so the workbench can
|
|
17
|
+
observe what would be added via the SSE event stream, but we capture
|
|
18
|
+
the new entry ids and immediately delete them post-write. This keeps
|
|
19
|
+
the preview semantic functionally close to the old behaviour without
|
|
20
|
+
introducing a parallel preview pipeline. Auto-dedup is suppressed
|
|
21
|
+
in preview mode to avoid touching pre-existing entries.
|
|
22
|
+
|
|
23
|
+
Callers that want a true preview (no disk writes at all) should move
|
|
24
|
+
to the new ``run_update`` API directly with a custom on_event consumer.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any, Awaitable, Callable
|
|
31
|
+
|
|
32
|
+
from deeptutor.services.memory import paths
|
|
33
|
+
from deeptutor.services.memory.consolidator.modes.update import (
|
|
34
|
+
UpdateResult,
|
|
35
|
+
run_update,
|
|
36
|
+
)
|
|
37
|
+
from deeptutor.services.memory.document import parse, serialize
|
|
38
|
+
from deeptutor.services.memory.ops import ApplyReport, Op
|
|
39
|
+
from deeptutor.services.memory.paths import L3Slot, Surface
|
|
40
|
+
|
|
41
|
+
OnEvent = Callable[[dict[str, Any]], Awaitable[None]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConsolidateResult:
|
|
46
|
+
report: ApplyReport
|
|
47
|
+
backlog_count: int
|
|
48
|
+
proposed_ops: list[Op] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def consolidate_l2(
|
|
52
|
+
surface: Surface,
|
|
53
|
+
*,
|
|
54
|
+
language: str = "en",
|
|
55
|
+
user_label: str = "anonymous",
|
|
56
|
+
on_event: OnEvent | None = None,
|
|
57
|
+
apply_ops: bool = True,
|
|
58
|
+
) -> ConsolidateResult:
|
|
59
|
+
result = await run_update(
|
|
60
|
+
"L2",
|
|
61
|
+
surface,
|
|
62
|
+
language=language,
|
|
63
|
+
user_label=user_label,
|
|
64
|
+
on_event=on_event,
|
|
65
|
+
)
|
|
66
|
+
if not apply_ops:
|
|
67
|
+
_rollback_new_entries("L2", surface, result.new_entry_ids)
|
|
68
|
+
return _to_consolidate_result(result)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def consolidate_l3(
|
|
72
|
+
slot: L3Slot,
|
|
73
|
+
*,
|
|
74
|
+
language: str = "en",
|
|
75
|
+
user_label: str = "anonymous",
|
|
76
|
+
on_event: OnEvent | None = None,
|
|
77
|
+
apply_ops: bool = True,
|
|
78
|
+
) -> ConsolidateResult:
|
|
79
|
+
if slot == "preferences":
|
|
80
|
+
raise ValueError("preferences.md is not auto-consolidated")
|
|
81
|
+
result = await run_update(
|
|
82
|
+
"L3",
|
|
83
|
+
slot,
|
|
84
|
+
language=language,
|
|
85
|
+
user_label=user_label,
|
|
86
|
+
on_event=on_event,
|
|
87
|
+
)
|
|
88
|
+
if not apply_ops:
|
|
89
|
+
_rollback_new_entries("L3", slot, result.new_entry_ids)
|
|
90
|
+
return _to_consolidate_result(result)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _to_consolidate_result(result: UpdateResult) -> ConsolidateResult:
|
|
94
|
+
reason = (
|
|
95
|
+
"no new input"
|
|
96
|
+
if result.no_new_input
|
|
97
|
+
else f"applied via chunk-update ({result.facts_added} added)"
|
|
98
|
+
)
|
|
99
|
+
return ConsolidateResult(
|
|
100
|
+
report=ApplyReport(
|
|
101
|
+
accepted=True,
|
|
102
|
+
reason=reason,
|
|
103
|
+
results=[], # the new pipeline does not emit per-op OpResult objects
|
|
104
|
+
),
|
|
105
|
+
backlog_count=result.chunks_processed,
|
|
106
|
+
proposed_ops=[], # the chunk-based mode writes directly; preview is via SSE
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _rollback_new_entries(layer: str, key: str, ids: list[str]) -> None:
|
|
111
|
+
"""Remove entries by id (used by ``apply_ops=False`` preview mode).
|
|
112
|
+
|
|
113
|
+
This is best-effort: if the doc was edited externally between the
|
|
114
|
+
update and rollback, ids not found are silently skipped.
|
|
115
|
+
"""
|
|
116
|
+
if not ids:
|
|
117
|
+
return
|
|
118
|
+
path = paths.l2_file(key) if layer == "L2" else paths.l3_file(key) # type: ignore[arg-type]
|
|
119
|
+
if not path.exists():
|
|
120
|
+
return
|
|
121
|
+
doc = parse(path.read_text(encoding="utf-8"))
|
|
122
|
+
drop = set(ids)
|
|
123
|
+
for _name, entries in doc.sections:
|
|
124
|
+
entries[:] = [e for e in entries if e.id not in drop]
|
|
125
|
+
doc.sections[:] = [(n, e) for n, e in doc.sections if e]
|
|
126
|
+
path.write_text(serialize(doc), encoding="utf-8")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = ["ConsolidateResult", "consolidate_l2", "consolidate_l3"]
|