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,42 @@
|
|
|
1
|
+
"""Three-layer memory subsystem.
|
|
2
|
+
|
|
3
|
+
- ``trace`` : L1 raw event capture (append-only JSONL per surface per day)
|
|
4
|
+
- ``document``: L2/L3 markdown + footnote-citation parser/serializer (pure)
|
|
5
|
+
- ``ops`` : add/edit/delete batch applier (pure)
|
|
6
|
+
- ``paths`` : per-user path resolution
|
|
7
|
+
- ``ids`` : ULID-style trace and entry id generators
|
|
8
|
+
- ``store`` : public facade — :class:`MemoryStore`
|
|
9
|
+
- ``consolidator``: LLM-driven L1→L2 and L2→L3 ops
|
|
10
|
+
|
|
11
|
+
The v1 two-file `MemoryService` is gone. All callers go through
|
|
12
|
+
:func:`get_memory_store` and the :class:`MemoryStore` facade.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .ids import is_entry_id, is_trace_id, new_entry_id, new_trace_id
|
|
16
|
+
from .paths import L3_SLOTS, SURFACES, L3Slot, Surface, memory_path_service_override
|
|
17
|
+
from .store import (
|
|
18
|
+
DocOverview,
|
|
19
|
+
MemoryStore,
|
|
20
|
+
get_memory_store,
|
|
21
|
+
migrate_partner_surface_if_needed,
|
|
22
|
+
migrate_v1_if_needed,
|
|
23
|
+
)
|
|
24
|
+
from .trace import TraceEvent
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DocOverview",
|
|
28
|
+
"L3_SLOTS",
|
|
29
|
+
"L3Slot",
|
|
30
|
+
"MemoryStore",
|
|
31
|
+
"SURFACES",
|
|
32
|
+
"Surface",
|
|
33
|
+
"TraceEvent",
|
|
34
|
+
"get_memory_store",
|
|
35
|
+
"is_entry_id",
|
|
36
|
+
"is_trace_id",
|
|
37
|
+
"memory_path_service_override",
|
|
38
|
+
"migrate_partner_surface_if_needed",
|
|
39
|
+
"migrate_v1_if_needed",
|
|
40
|
+
"new_entry_id",
|
|
41
|
+
"new_trace_id",
|
|
42
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Memory consolidator — chunk-based update / audit / dedup.
|
|
2
|
+
|
|
3
|
+
Public surface (call these from the API router / store / tests):
|
|
4
|
+
|
|
5
|
+
* :class:`ConsolidateResult`, :data:`OnEvent` — legacy types preserved
|
|
6
|
+
for :mod:`deeptutor.services.memory.store`.
|
|
7
|
+
* :func:`consolidate_l2`, :func:`consolidate_l3` — legacy shims that
|
|
8
|
+
delegate to :func:`run_update`.
|
|
9
|
+
* :func:`run_update`, :func:`run_audit`, :func:`run_dedup` — the three
|
|
10
|
+
modes the workbench drives directly.
|
|
11
|
+
* :func:`_parse_ops_response`, :func:`_filter_banned`,
|
|
12
|
+
:func:`_has_banned` — kept for :meth:`store.MemoryStore.apply_ops_payload`
|
|
13
|
+
and the existing test suite.
|
|
14
|
+
|
|
15
|
+
Submodule layout:
|
|
16
|
+
|
|
17
|
+
chunker.py pure char-based chunker with boundary expansion
|
|
18
|
+
line_doc.py line-numbered view + replace/delete/insert edits
|
|
19
|
+
meta.py *.meta.json read/write for "seen ids" diffs
|
|
20
|
+
references.py ref pool validation + raw-trace annotation
|
|
21
|
+
guards.py banned-phrase filter (legacy + L3 enforcement)
|
|
22
|
+
parse.py legacy ops-array parser (apply_ops_payload)
|
|
23
|
+
modes/
|
|
24
|
+
_runtime.py shared prompt loading + LLM + atomic write
|
|
25
|
+
_shims.py legacy consolidate_l2/l3 → run_update
|
|
26
|
+
update.py chunk-based incremental fact extraction
|
|
27
|
+
audit.py chunk-based line edits vs raw evidence
|
|
28
|
+
dedup.py iterative line-level dedup over full doc
|
|
29
|
+
prompts/
|
|
30
|
+
{en,zh}/{update_l2,update_l3,audit_l2,audit_l3,dedup,_meta}.yaml
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from deeptutor.services.memory.consolidator.guards import _filter_banned, _has_banned
|
|
36
|
+
from deeptutor.services.memory.consolidator.modes import (
|
|
37
|
+
consolidate_l2,
|
|
38
|
+
consolidate_l3,
|
|
39
|
+
run_audit,
|
|
40
|
+
run_dedup,
|
|
41
|
+
run_merge,
|
|
42
|
+
run_update,
|
|
43
|
+
)
|
|
44
|
+
from deeptutor.services.memory.consolidator.modes._shims import (
|
|
45
|
+
ConsolidateResult,
|
|
46
|
+
OnEvent,
|
|
47
|
+
)
|
|
48
|
+
from deeptutor.services.memory.consolidator.parse import _parse_ops_response
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"ConsolidateResult",
|
|
52
|
+
"OnEvent",
|
|
53
|
+
"_filter_banned",
|
|
54
|
+
"_has_banned",
|
|
55
|
+
"_parse_ops_response",
|
|
56
|
+
"consolidate_l2",
|
|
57
|
+
"consolidate_l3",
|
|
58
|
+
"run_audit",
|
|
59
|
+
"run_dedup",
|
|
60
|
+
"run_merge",
|
|
61
|
+
"run_update",
|
|
62
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Character-based chunking with boundary expansion.
|
|
2
|
+
|
|
3
|
+
The L2 / L3 update flow concatenates inputs into one string, then
|
|
4
|
+
:func:`chunk_with_boundary` cuts it into ≤ budget pieces. Each piece's
|
|
5
|
+
right edge is extended forward to the next paragraph or sentence
|
|
6
|
+
boundary — content is **never truncated mid-statement**. Adjacent
|
|
7
|
+
chunks overlap by a percentage of the target size so a fact straddling
|
|
8
|
+
a cut still gets a fair read.
|
|
9
|
+
|
|
10
|
+
Pure functions: no I/O, no LLM. Easy to unit-test.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
import math
|
|
17
|
+
import re
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
Boundary = Literal["paragraph", "sentence"]
|
|
21
|
+
|
|
22
|
+
# Paragraph boundary: one or more blank lines.
|
|
23
|
+
_PARA_BOUNDARY = re.compile(r"\n\s*\n+")
|
|
24
|
+
# Sentence boundary: terminal punctuation followed by space/newline.
|
|
25
|
+
# Covers ASCII (.!?) and CJK (。!?).
|
|
26
|
+
_SENT_BOUNDARY = re.compile(r"[.!?。!?](?:[\")»」』]+)?(?=\s|$)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ChunkSpan:
|
|
31
|
+
"""One chunk's coordinates inside the source text.
|
|
32
|
+
|
|
33
|
+
``start`` is inclusive, ``end`` exclusive. ``index`` is the 0-based
|
|
34
|
+
position in the returned list (useful for events).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
index: int
|
|
38
|
+
start: int
|
|
39
|
+
end: int
|
|
40
|
+
text: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def chunk_with_boundary(
|
|
44
|
+
text: str,
|
|
45
|
+
*,
|
|
46
|
+
budget: int,
|
|
47
|
+
overlap_ratio: float,
|
|
48
|
+
min_chunk_chars: int,
|
|
49
|
+
max_chunk_chars: int,
|
|
50
|
+
boundary: Boundary = "paragraph",
|
|
51
|
+
) -> list[ChunkSpan]:
|
|
52
|
+
"""Cut ``text`` into ≤ ``budget`` chunks aligned to natural boundaries.
|
|
53
|
+
|
|
54
|
+
* Target size = ``clamp(ceil(len(text) / budget), min, max)``.
|
|
55
|
+
* Right edge of each chunk is extended forward to the next
|
|
56
|
+
``boundary`` so no sentence/paragraph is split.
|
|
57
|
+
* Adjacent chunks overlap by ``round(target * overlap_ratio)`` chars.
|
|
58
|
+
* If the input is short enough to fit in a single chunk, returns
|
|
59
|
+
one ``ChunkSpan`` covering everything.
|
|
60
|
+
"""
|
|
61
|
+
if not text.strip():
|
|
62
|
+
return []
|
|
63
|
+
if budget < 1:
|
|
64
|
+
budget = 1
|
|
65
|
+
|
|
66
|
+
n = len(text)
|
|
67
|
+
target = math.ceil(n / budget)
|
|
68
|
+
target = max(min_chunk_chars, min(max_chunk_chars, target))
|
|
69
|
+
overlap = max(0, min(target - 1, round(target * overlap_ratio)))
|
|
70
|
+
|
|
71
|
+
# Short-circuit: input fits in one chunk.
|
|
72
|
+
if n <= target:
|
|
73
|
+
return [ChunkSpan(index=0, start=0, end=n, text=text)]
|
|
74
|
+
|
|
75
|
+
# Hard cap on how far the right edge can be pulled to find a
|
|
76
|
+
# boundary. Beyond this we accept a non-boundary cut so chunks
|
|
77
|
+
# never grow past ``max_chunk_chars`` even in degenerate input
|
|
78
|
+
# (e.g. a single long line with no paragraph/sentence breaks).
|
|
79
|
+
spans: list[ChunkSpan] = []
|
|
80
|
+
cursor = 0
|
|
81
|
+
while cursor < n:
|
|
82
|
+
target_end = min(n, cursor + target)
|
|
83
|
+
hard_cap = min(n, cursor + max_chunk_chars)
|
|
84
|
+
if target_end >= n:
|
|
85
|
+
end = n
|
|
86
|
+
else:
|
|
87
|
+
end = _expand_to_boundary(text, target_end, boundary, limit=hard_cap)
|
|
88
|
+
# Guarantee forward motion: the boundary expansion may pull us
|
|
89
|
+
# to len(text), or — degenerate input — to ``target_end`` itself.
|
|
90
|
+
if end <= cursor:
|
|
91
|
+
end = min(n, cursor + max(1, target))
|
|
92
|
+
spans.append(
|
|
93
|
+
ChunkSpan(
|
|
94
|
+
index=len(spans),
|
|
95
|
+
start=cursor,
|
|
96
|
+
end=end,
|
|
97
|
+
text=text[cursor:end],
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
if end >= n:
|
|
101
|
+
break
|
|
102
|
+
next_cursor = end - overlap
|
|
103
|
+
# No infinite loop: must advance by at least one char even with
|
|
104
|
+
# huge overlap.
|
|
105
|
+
if next_cursor <= cursor:
|
|
106
|
+
next_cursor = cursor + 1
|
|
107
|
+
cursor = next_cursor
|
|
108
|
+
|
|
109
|
+
return spans
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Internals ───────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _expand_to_boundary(text: str, target_end: int, boundary: Boundary, *, limit: int) -> int:
|
|
116
|
+
"""Push ``target_end`` forward to the next natural boundary.
|
|
117
|
+
|
|
118
|
+
The search is bounded by ``limit`` (exclusive). If no boundary is
|
|
119
|
+
found within that window the function returns ``limit`` — a non-
|
|
120
|
+
boundary cut, but a bounded one. Without this the chunker can
|
|
121
|
+
inflate a single chunk to the end of the input on pathological
|
|
122
|
+
text with no paragraph/sentence markers.
|
|
123
|
+
"""
|
|
124
|
+
pattern = _PARA_BOUNDARY if boundary == "paragraph" else _SENT_BOUNDARY
|
|
125
|
+
match = pattern.search(text, target_end, limit)
|
|
126
|
+
if match is None:
|
|
127
|
+
return limit
|
|
128
|
+
return match.end()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = ["Boundary", "ChunkSpan", "chunk_with_boundary"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Op-emit guards: banned-phrase filter + budgets.
|
|
2
|
+
|
|
3
|
+
Run at op-emit time (during the loop) so the model gets an observation
|
|
4
|
+
back when an op is rejected and can rewrite. Today's pre-redesign code
|
|
5
|
+
filtered banned phrases after the LLM call returned all ops at once,
|
|
6
|
+
which meant rejection was a silent drop.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
from deeptutor.services.memory.ops import Op
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# L3 objectivity guard: phrases the LLM is prompt-banned from emitting.
|
|
21
|
+
# Runtime enforces by dropping any L3 op whose text contains one of these
|
|
22
|
+
# (outside of quoted user verbatim 「」 / "…"). Logged as a warning so we
|
|
23
|
+
# can tune the list against real prompt regressions.
|
|
24
|
+
BANNED_PHRASES: tuple[str, ...] = (
|
|
25
|
+
# English absolutes
|
|
26
|
+
"deeply",
|
|
27
|
+
"truly",
|
|
28
|
+
"mastered",
|
|
29
|
+
"expert in",
|
|
30
|
+
"passionate",
|
|
31
|
+
"loves",
|
|
32
|
+
"hates",
|
|
33
|
+
"always",
|
|
34
|
+
"never",
|
|
35
|
+
"fully understands",
|
|
36
|
+
# Chinese absolutes
|
|
37
|
+
"深刻",
|
|
38
|
+
"彻底",
|
|
39
|
+
"完美掌握",
|
|
40
|
+
"完美理解",
|
|
41
|
+
"完全理解",
|
|
42
|
+
"完全掌握",
|
|
43
|
+
"专家",
|
|
44
|
+
"热爱",
|
|
45
|
+
"总是",
|
|
46
|
+
"从来不",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Per-loop budgets. Beyond these the dispatcher emits a hint observation
|
|
51
|
+
# instead of executing the action; the prompt nudges the model to finish.
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ToolBudgets:
|
|
54
|
+
read_entity: int = 30
|
|
55
|
+
search: int = 20
|
|
56
|
+
list_pending: int = 50 # cheap nav, generous
|
|
57
|
+
list_sections: int = 50
|
|
58
|
+
recent_changes: int = 3
|
|
59
|
+
add_entry: int = 12
|
|
60
|
+
edit_entry: int = 12
|
|
61
|
+
delete_entry: int = 12
|
|
62
|
+
note: int = 8
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_QUOTED_RE = re.compile(r"「[^」]*」|\"[^\"]*\"")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _has_banned(text: str) -> bool:
|
|
69
|
+
"""Return ``True`` iff a banned phrase appears outside every quote.
|
|
70
|
+
|
|
71
|
+
Quoted regions (CJK 「…」 or ASCII "…") are stripped first, because
|
|
72
|
+
the prompt allows verbatim user quotations to contain the otherwise-
|
|
73
|
+
banned absolutes.
|
|
74
|
+
"""
|
|
75
|
+
stripped = _QUOTED_RE.sub("", text).lower()
|
|
76
|
+
for phrase in BANNED_PHRASES:
|
|
77
|
+
if phrase in stripped:
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _op_text(op: Op) -> str:
|
|
83
|
+
text = getattr(op, "text", "") or getattr(op, "new_text", "")
|
|
84
|
+
return str(text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _filter_banned(ops: Iterable[Op]) -> list[Op]:
|
|
88
|
+
"""Drop ops whose text contains banned absolutist phrasing.
|
|
89
|
+
|
|
90
|
+
Used post-loop as a safety net even though the per-op emit path
|
|
91
|
+
already rejects them. Kept callable by name for legacy tests and
|
|
92
|
+
the apply_ops_payload preview/apply round-trip.
|
|
93
|
+
"""
|
|
94
|
+
kept: list[Op] = []
|
|
95
|
+
for op in ops:
|
|
96
|
+
text = _op_text(op)
|
|
97
|
+
if text and _has_banned(text):
|
|
98
|
+
logger.warning(
|
|
99
|
+
"memory consolidate: dropped op with banned phrase: %s",
|
|
100
|
+
text[:80],
|
|
101
|
+
)
|
|
102
|
+
continue
|
|
103
|
+
kept.append(op)
|
|
104
|
+
return kept
|