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,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