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,80 @@
|
|
|
1
|
+
"""Stable, time-ordered identifiers for trace events and document entries.
|
|
2
|
+
|
|
3
|
+
Format: 26-character Crockford-base32 (ULID-style).
|
|
4
|
+
|
|
5
|
+
- Trace ids: ``<surface>:<ULID>`` — e.g. ``chat:01HZK4ABCDEFGHJKMNPQRSTVWX``
|
|
6
|
+
- Entry ids: ``m_<ULID>`` — used in MD footnote labels
|
|
7
|
+
|
|
8
|
+
The ULID's leading 10 characters encode a millisecond timestamp, giving
|
|
9
|
+
natural chronological sort across files and within a file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import secrets
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # Crockford
|
|
19
|
+
_ULID_TS_LEN = 10
|
|
20
|
+
_ULID_RAND_LEN = 16
|
|
21
|
+
_ULID_LEN = _ULID_TS_LEN + _ULID_RAND_LEN
|
|
22
|
+
|
|
23
|
+
_ENTRY_RE = re.compile(r"^m_[0-9A-HJKMNP-TV-Z]{26}$")
|
|
24
|
+
_TRACE_RE = re.compile(r"^[a-z][a-z0-9_-]*:[0-9A-HJKMNP-TV-Z]{26}$")
|
|
25
|
+
# Snapshot ref points to a current workspace entity. The id portion is
|
|
26
|
+
# whatever the per-surface adapter chose (doc_id / record_id / kb_name /
|
|
27
|
+
# bot name / session_id / "session:question" composites). Permissive
|
|
28
|
+
# enough to allow embedded ``:``; restrictive enough to keep refs safe
|
|
29
|
+
# to embed in the comma-separated footnote serialization.
|
|
30
|
+
_SNAPSHOT_RE = re.compile(r"^[a-z][a-z0-9_-]*:[A-Za-z0-9_.:\-]+$")
|
|
31
|
+
# L3 surface ref — bare surface name like ``chat``, ``notebook``. L3 is
|
|
32
|
+
# a synthesis layer that points at L2 *files*, not L2 entries, so its
|
|
33
|
+
# refs need no id portion. Whitelist (not a loose regex) so a malformed
|
|
34
|
+
# ref like ``not-an-id`` doesn't accidentally validate. Mirrors
|
|
35
|
+
# :data:`paths.SURFACES`; if you add a surface, add it here too.
|
|
36
|
+
_SHORTNAME_REFS = frozenset({"chat", "notebook", "quiz", "kb", "book", "partner", "cowriter"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _encode(n: int, length: int) -> str:
|
|
40
|
+
chars: list[str] = []
|
|
41
|
+
for _ in range(length):
|
|
42
|
+
chars.append(_BASE32[n & 0x1F])
|
|
43
|
+
n >>= 5
|
|
44
|
+
return "".join(reversed(chars))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def new_ulid() -> str:
|
|
48
|
+
ts = int(time.time() * 1000) & ((1 << (_ULID_TS_LEN * 5)) - 1)
|
|
49
|
+
rand = secrets.randbits(_ULID_RAND_LEN * 5)
|
|
50
|
+
return _encode(ts, _ULID_TS_LEN) + _encode(rand, _ULID_RAND_LEN)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def new_entry_id() -> str:
|
|
54
|
+
return f"m_{new_ulid()}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def new_trace_id(surface: str) -> str:
|
|
58
|
+
return f"{surface}:{new_ulid()}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_entry_id(s: str) -> bool:
|
|
62
|
+
return bool(_ENTRY_RE.match(s))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_trace_id(s: str) -> bool:
|
|
66
|
+
return bool(_TRACE_RE.match(s))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_snapshot_ref(s: str) -> bool:
|
|
70
|
+
return bool(_SNAPSHOT_RE.match(s))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_shortname_ref(s: str) -> bool:
|
|
74
|
+
"""Bare surface name — the form used by L3 refs (whitelist)."""
|
|
75
|
+
return s in _SHORTNAME_REFS
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_valid_ref(s: str) -> bool:
|
|
79
|
+
"""Any form of ref the ops validator accepts."""
|
|
80
|
+
return is_entry_id(s) or is_trace_id(s) or is_snapshot_ref(s) or is_shortname_ref(s)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Atomic add/edit/delete operations on memory documents.
|
|
2
|
+
|
|
3
|
+
A batch of ops is validated as a whole and applied only if all ops pass.
|
|
4
|
+
Conflicting ops (e.g. ``delete`` and ``edit`` on the same id within one
|
|
5
|
+
batch) reject the entire batch — the LLM doesn't get to self-contradict.
|
|
6
|
+
|
|
7
|
+
Pure functions; no I/O, no LLM.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Literal, Union
|
|
14
|
+
|
|
15
|
+
from deeptutor.services.memory.document import Document, Entry
|
|
16
|
+
from deeptutor.services.memory.ids import is_entry_id, is_valid_ref, new_entry_id
|
|
17
|
+
|
|
18
|
+
_MAX_TEXT_LEN = 240
|
|
19
|
+
_MAX_SECTION_LEN = 80
|
|
20
|
+
_DELETE_REASONS = frozenset({"contradicted", "superseded", "stale", "low-signal"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AddOp:
|
|
25
|
+
section: str
|
|
26
|
+
text: str
|
|
27
|
+
refs: list[str]
|
|
28
|
+
op: Literal["add"] = "add"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class EditOp:
|
|
33
|
+
target_id: str
|
|
34
|
+
new_text: str
|
|
35
|
+
new_refs: list[str]
|
|
36
|
+
op: Literal["edit"] = "edit"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class DeleteOp:
|
|
41
|
+
target_id: str
|
|
42
|
+
reason: str
|
|
43
|
+
op: Literal["delete"] = "delete"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Op = Union[AddOp, EditOp, DeleteOp]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class OpResult:
|
|
51
|
+
op: Op
|
|
52
|
+
status: Literal["applied"]
|
|
53
|
+
entry_id: str | None = None # populated for add ops
|
|
54
|
+
detail: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ApplyReport:
|
|
59
|
+
accepted: bool
|
|
60
|
+
results: list[OpResult] = field(default_factory=list)
|
|
61
|
+
reason: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class OpValidationError(Exception):
|
|
65
|
+
"""Raised when a batch fails pre-flight validation."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate(doc: Document, ops: list[Op]) -> None:
|
|
69
|
+
edits: set[str] = set()
|
|
70
|
+
deletes: set[str] = set()
|
|
71
|
+
|
|
72
|
+
for op in ops:
|
|
73
|
+
if isinstance(op, AddOp):
|
|
74
|
+
if not op.text or len(op.text) > _MAX_TEXT_LEN:
|
|
75
|
+
raise OpValidationError(
|
|
76
|
+
f"add: text length must be 1..{_MAX_TEXT_LEN} (got {len(op.text)})"
|
|
77
|
+
)
|
|
78
|
+
if not op.section or len(op.section) > _MAX_SECTION_LEN:
|
|
79
|
+
raise OpValidationError(f"add: invalid section {op.section!r}")
|
|
80
|
+
if not op.refs:
|
|
81
|
+
raise OpValidationError("add: refs must be non-empty")
|
|
82
|
+
for ref in op.refs:
|
|
83
|
+
if not is_valid_ref(ref):
|
|
84
|
+
raise OpValidationError(f"add: malformed ref {ref!r}")
|
|
85
|
+
elif isinstance(op, EditOp):
|
|
86
|
+
if not is_entry_id(op.target_id):
|
|
87
|
+
raise OpValidationError(f"edit: malformed target_id {op.target_id!r}")
|
|
88
|
+
if doc.find(op.target_id) is None:
|
|
89
|
+
raise OpValidationError(f"edit: target_id {op.target_id} not found")
|
|
90
|
+
if not op.new_text or len(op.new_text) > _MAX_TEXT_LEN:
|
|
91
|
+
raise OpValidationError(
|
|
92
|
+
f"edit: text length must be 1..{_MAX_TEXT_LEN} (got {len(op.new_text)})"
|
|
93
|
+
)
|
|
94
|
+
if not op.new_refs:
|
|
95
|
+
raise OpValidationError("edit: refs must be non-empty")
|
|
96
|
+
for ref in op.new_refs:
|
|
97
|
+
if not is_valid_ref(ref):
|
|
98
|
+
raise OpValidationError(f"edit: malformed ref {ref!r}")
|
|
99
|
+
if op.target_id in deletes:
|
|
100
|
+
raise OpValidationError(
|
|
101
|
+
f"batch conflict: edit and delete on same id {op.target_id}"
|
|
102
|
+
)
|
|
103
|
+
edits.add(op.target_id)
|
|
104
|
+
elif isinstance(op, DeleteOp):
|
|
105
|
+
if not is_entry_id(op.target_id):
|
|
106
|
+
raise OpValidationError(f"delete: malformed target_id {op.target_id!r}")
|
|
107
|
+
if doc.find(op.target_id) is None:
|
|
108
|
+
raise OpValidationError(f"delete: target_id {op.target_id} not found")
|
|
109
|
+
if op.reason not in _DELETE_REASONS:
|
|
110
|
+
raise OpValidationError(f"delete: reason must be one of {sorted(_DELETE_REASONS)}")
|
|
111
|
+
if op.target_id in edits:
|
|
112
|
+
raise OpValidationError(
|
|
113
|
+
f"batch conflict: edit and delete on same id {op.target_id}"
|
|
114
|
+
)
|
|
115
|
+
deletes.add(op.target_id)
|
|
116
|
+
else:
|
|
117
|
+
raise OpValidationError(f"unknown op {type(op).__name__}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def apply(doc: Document, ops: list[Op]) -> ApplyReport:
|
|
121
|
+
"""Apply ops as an atomic batch. Mutates ``doc`` in place on success.
|
|
122
|
+
|
|
123
|
+
On validation failure the document is untouched and the report carries
|
|
124
|
+
``accepted=False`` with ``reason``.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
_validate(doc, ops)
|
|
128
|
+
except OpValidationError as exc:
|
|
129
|
+
return ApplyReport(accepted=False, reason=str(exc))
|
|
130
|
+
|
|
131
|
+
results: list[OpResult] = []
|
|
132
|
+
for op in ops:
|
|
133
|
+
if isinstance(op, AddOp):
|
|
134
|
+
new_id = new_entry_id()
|
|
135
|
+
doc.section_entries(op.section).append(
|
|
136
|
+
Entry(id=new_id, section=op.section, text=op.text, refs=list(op.refs))
|
|
137
|
+
)
|
|
138
|
+
results.append(OpResult(op=op, status="applied", entry_id=new_id))
|
|
139
|
+
elif isinstance(op, EditOp):
|
|
140
|
+
entry = doc.find(op.target_id)
|
|
141
|
+
assert entry is not None # _validate ensured this
|
|
142
|
+
entry.text = op.new_text
|
|
143
|
+
entry.refs = list(op.new_refs)
|
|
144
|
+
results.append(OpResult(op=op, status="applied"))
|
|
145
|
+
else: # DeleteOp
|
|
146
|
+
doc.remove(op.target_id)
|
|
147
|
+
results.append(OpResult(op=op, status="applied", detail=op.reason))
|
|
148
|
+
|
|
149
|
+
return ApplyReport(accepted=True, results=results)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Path resolution for the three-layer memory subsystem.
|
|
2
|
+
|
|
3
|
+
Layout under the per-user memory root::
|
|
4
|
+
|
|
5
|
+
trace/<surface>/<YYYY-MM-DD>.jsonl (L1, append-only)
|
|
6
|
+
L2/<surface>.md (L2, per-surface summaries)
|
|
7
|
+
L3/<recent|profile|scope|preferences>.md (L3, cross-surface)
|
|
8
|
+
backup/<timestamp>/... (v1 migration archive)
|
|
9
|
+
|
|
10
|
+
The root itself is resolved via :class:`PathService` so the multi-user
|
|
11
|
+
context (workspace_root) is picked up at call time, not import time.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
from contextvars import ContextVar
|
|
18
|
+
from datetime import date
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Iterator, Literal, get_args
|
|
21
|
+
|
|
22
|
+
from deeptutor.services.path_service import get_path_service
|
|
23
|
+
from deeptutor.utils.secret_files import ensure_private_directory
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from deeptutor.services.path_service import PathService
|
|
27
|
+
|
|
28
|
+
# When set, memory paths resolve through this PathService instead of the active
|
|
29
|
+
# user's. A partner runtime installs the *owner's* (admin) service for the
|
|
30
|
+
# duration of a turn so the chat agent's ``read_memory`` / ``write_memory``
|
|
31
|
+
# tools see the owner's memory — not the partner's own (empty) scope — while
|
|
32
|
+
# every *other* service (rag / skills / notebooks) stays on the partner scope.
|
|
33
|
+
_memory_path_service: ContextVar[PathService | None] = ContextVar(
|
|
34
|
+
"memory_path_service", default=None
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@contextlib.contextmanager
|
|
39
|
+
def memory_path_service_override(service: PathService) -> Iterator[None]:
|
|
40
|
+
"""Resolve memory paths through *service* within this context."""
|
|
41
|
+
token = _memory_path_service.set(service)
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
finally:
|
|
45
|
+
_memory_path_service.reset(token)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Surface = Literal[
|
|
49
|
+
"chat",
|
|
50
|
+
"notebook",
|
|
51
|
+
"quiz",
|
|
52
|
+
"kb",
|
|
53
|
+
"book",
|
|
54
|
+
"partner",
|
|
55
|
+
"cowriter",
|
|
56
|
+
]
|
|
57
|
+
L3Slot = Literal["recent", "profile", "scope", "preferences"]
|
|
58
|
+
|
|
59
|
+
SURFACES: tuple[Surface, ...] = get_args(Surface)
|
|
60
|
+
L3_SLOTS: tuple[L3Slot, ...] = get_args(L3Slot)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def memory_root() -> Path:
|
|
64
|
+
override = _memory_path_service.get()
|
|
65
|
+
service = override if override is not None else get_path_service()
|
|
66
|
+
return service.get_memory_dir()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def trace_dir(surface: Surface) -> Path:
|
|
70
|
+
return memory_root() / "trace" / surface
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def trace_file(surface: Surface, day: date) -> Path:
|
|
74
|
+
return trace_dir(surface) / f"{day.isoformat()}.jsonl"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def l2_dir() -> Path:
|
|
78
|
+
return memory_root() / "L2"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def l2_file(surface: Surface) -> Path:
|
|
82
|
+
return l2_dir() / f"{surface}.md"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def l3_dir() -> Path:
|
|
86
|
+
return memory_root() / "L3"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def l3_file(slot: L3Slot) -> Path:
|
|
90
|
+
return l3_dir() / f"{slot}.md"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def backup_root() -> Path:
|
|
94
|
+
return memory_root() / "backup"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ensure_dirs() -> None:
|
|
98
|
+
"""Create the directory skeleton. Idempotent."""
|
|
99
|
+
root = memory_root()
|
|
100
|
+
ensure_private_directory(root)
|
|
101
|
+
l2_dir().mkdir(parents=True, exist_ok=True)
|
|
102
|
+
l3_dir().mkdir(parents=True, exist_ok=True)
|
|
103
|
+
for surface in SURFACES:
|
|
104
|
+
trace_dir(surface).mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Memory consolidation prompts (English).
|
|
2
|
+
#
|
|
3
|
+
# Used by deeptutor.services.memory.consolidator to turn raw L1 trace
|
|
4
|
+
# events into L2 ops, and the seven L2 docs into L3 ops. Every prompt
|
|
5
|
+
# uses the same output schema: {"ops": [...]}.
|
|
6
|
+
|
|
7
|
+
system_l2: |
|
|
8
|
+
You are the memory curator for DeepTutor user {user_label}. You read
|
|
9
|
+
the user's current workspace entities (full content, not event logs)
|
|
10
|
+
for a single surface and propose surgical edit operations on that
|
|
11
|
+
surface's markdown memory document. You never chat, explain, or
|
|
12
|
+
apologise — you emit JSON only.
|
|
13
|
+
|
|
14
|
+
Today is {today}.
|
|
15
|
+
|
|
16
|
+
EVERY add/edit op MUST include `refs`: at least one entity reference
|
|
17
|
+
from the input, written as `<surface>:<entity_id>` (the `ref:` line
|
|
18
|
+
above each entity block tells you the exact string to cite). No refs
|
|
19
|
+
→ do not emit. An empty `ops` list is a correct, expected output
|
|
20
|
+
when nothing material has changed.
|
|
21
|
+
|
|
22
|
+
Length cap: each `text`/`new_text` ≤ 240 characters. Be terse.
|
|
23
|
+
|
|
24
|
+
Banned phrases (unless quoted verbatim from the user, in which case
|
|
25
|
+
wrap them in 「 」): deeply, truly, mastered, expert, passionate,
|
|
26
|
+
loves, hates, always, never, fully understands.
|
|
27
|
+
|
|
28
|
+
Prefer verb phrases ("uses X", "prefers X over Y", "stuck on Z")
|
|
29
|
+
over adjectives. Quote the user's words for specific claims.
|
|
30
|
+
|
|
31
|
+
Delete reasons (enum): contradicted, superseded, stale, low-signal.
|
|
32
|
+
|
|
33
|
+
user_l2: |
|
|
34
|
+
Surface: {surface}
|
|
35
|
+
Focus: {focus}
|
|
36
|
+
Suggested sections (use these names; add new sections only if
|
|
37
|
+
clearly needed): {sections}
|
|
38
|
+
|
|
39
|
+
Existing document (each entry is annotated with its `[m_xxx]` id):
|
|
40
|
+
----
|
|
41
|
+
{existing}
|
|
42
|
+
----
|
|
43
|
+
|
|
44
|
+
Current workspace entities (one block per item, each block shows
|
|
45
|
+
the `ref:` you must cite, the label, timestamp, metadata, and FULL
|
|
46
|
+
content):
|
|
47
|
+
----
|
|
48
|
+
{entities}
|
|
49
|
+
----
|
|
50
|
+
|
|
51
|
+
Changes since last refresh (informational; do NOT cite these as refs):
|
|
52
|
+
----
|
|
53
|
+
{changes}
|
|
54
|
+
----
|
|
55
|
+
|
|
56
|
+
Recent KB queries (only present for the `kb` surface):
|
|
57
|
+
----
|
|
58
|
+
{kb_queries}
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
Emit a JSON object with this shape exactly:
|
|
62
|
+
|
|
63
|
+
{{
|
|
64
|
+
"ops": [
|
|
65
|
+
{{"op": "add", "section": "<one of the sections>", "text": "<≤240 chars>", "refs": ["<surface>:<entity_id>", ...]}},
|
|
66
|
+
{{"op": "edit", "target_id": "<m_xxx>", "new_text": "<≤240 chars>", "new_refs": ["<surface>:<entity_id>", ...]}},
|
|
67
|
+
{{"op": "delete", "target_id": "<m_xxx>", "reason": "<contradicted|superseded|stale|low-signal>"}}
|
|
68
|
+
]
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
If nothing material changed, emit {{"ops": []}}. Do not wrap output
|
|
72
|
+
in markdown fences. Do not add any prose.
|
|
73
|
+
|
|
74
|
+
system_l3: |
|
|
75
|
+
You are the cross-surface memory curator for DeepTutor user
|
|
76
|
+
{user_label}. You read the seven per-surface summaries (L2) and
|
|
77
|
+
propose surgical edit operations on ONE cross-surface document.
|
|
78
|
+
You never chat, explain, or apologise — you emit JSON only.
|
|
79
|
+
|
|
80
|
+
Today is {today}.
|
|
81
|
+
|
|
82
|
+
OBJECTIVITY IS NON-NEGOTIABLE for L3. Hard rules:
|
|
83
|
+
|
|
84
|
+
1. Every claim MUST cite ≥1 L2 entry id in `refs` (form m_xxx).
|
|
85
|
+
No refs → do not emit.
|
|
86
|
+
2. Banned absolutist phrasing: deeply, truly, mastered, expert,
|
|
87
|
+
passionate, loves, hates, always, never, fully understands.
|
|
88
|
+
Use only if quoting the user verbatim 「like this」.
|
|
89
|
+
3. Forced hedge template — claims about the user must take the
|
|
90
|
+
form: "Across N <surface> interactions, the user X". Bind
|
|
91
|
+
observations to a count or a surface.
|
|
92
|
+
|
|
93
|
+
Length cap 240 chars per entry. Be terse.
|
|
94
|
+
|
|
95
|
+
Delete reasons (enum): contradicted, superseded, stale, low-signal.
|
|
96
|
+
|
|
97
|
+
user_l3: |
|
|
98
|
+
Slot: {slot}
|
|
99
|
+
Focus: {focus}
|
|
100
|
+
Suggested sections: {sections}
|
|
101
|
+
|
|
102
|
+
Existing document:
|
|
103
|
+
----
|
|
104
|
+
{existing}
|
|
105
|
+
----
|
|
106
|
+
|
|
107
|
+
Seven L2 surface summaries (with their entry ids):
|
|
108
|
+
----
|
|
109
|
+
{l2_corpus}
|
|
110
|
+
----
|
|
111
|
+
|
|
112
|
+
Emit JSON in this exact shape:
|
|
113
|
+
|
|
114
|
+
{{
|
|
115
|
+
"ops": [
|
|
116
|
+
{{"op": "add", "section": "<section>", "text": "<≤240 chars, hedged>", "refs": ["m_xxx", ...]}},
|
|
117
|
+
{{"op": "edit", "target_id": "<m_xxx>", "new_text": "<≤240 chars, hedged>", "new_refs": ["m_xxx", ...]}},
|
|
118
|
+
{{"op": "delete", "target_id": "<m_xxx>", "reason": "<contradicted|superseded|stale|low-signal>"}}
|
|
119
|
+
]
|
|
120
|
+
}}
|
|
121
|
+
|
|
122
|
+
Empty `ops` is fine and expected when no L2 changes warrant L3 motion.
|
|
123
|
+
|
|
124
|
+
surfaces:
|
|
125
|
+
chat:
|
|
126
|
+
focus: "Stable misconceptions the user has surfaced, concepts they've demonstrated mastery of, and durable topics they keep returning to. Drop per-turn chatter."
|
|
127
|
+
sections: ["Misconceptions", "Mastery", "Topics"]
|
|
128
|
+
notebook:
|
|
129
|
+
focus: "Recurring note themes, formats the user prefers, and questions they keep coming back to in their notes."
|
|
130
|
+
sections: ["Themes", "Formats", "Open questions"]
|
|
131
|
+
quiz:
|
|
132
|
+
focus: "Error patterns across quiz attempts, topics with low/high success rate, and item types the user struggles with."
|
|
133
|
+
sections: ["Error patterns", "Strong topics", "Struggling topics"]
|
|
134
|
+
kb:
|
|
135
|
+
focus: "Document interests, query patterns into the knowledge base, and gaps in user's library."
|
|
136
|
+
sections: ["Interests", "Frequent queries", "Library gaps"]
|
|
137
|
+
book:
|
|
138
|
+
focus: "Reading pacing, sticking points (pages reopened or paused), and topics the user annotates heavily."
|
|
139
|
+
sections: ["Pacing", "Sticking points", "Annotation themes"]
|
|
140
|
+
partner:
|
|
141
|
+
focus: "Persistent themes across partner conversations and channel-specific behaviours."
|
|
142
|
+
sections: ["Themes", "Channels"]
|
|
143
|
+
cowriter:
|
|
144
|
+
focus: "Writing style preferences, recurring revisions, and topics the user drafts about."
|
|
145
|
+
sections: ["Style", "Recurring revisions", "Topics"]
|
|
146
|
+
|
|
147
|
+
slots:
|
|
148
|
+
recent:
|
|
149
|
+
focus: "A rolling timeline of what the user has been doing across all surfaces over the last 1–4 weeks. Time-anchored, surface-attributed."
|
|
150
|
+
sections: ["This week", "Earlier"]
|
|
151
|
+
profile:
|
|
152
|
+
focus: "Durable identity, learning style, and knowledge level. ONLY claims supported by multiple L2 entries across surfaces. No speculation about traits not directly evidenced."
|
|
153
|
+
sections: ["Identity", "Learning style", "Knowledge level"]
|
|
154
|
+
scope:
|
|
155
|
+
focus: "Concepts and topics the user has demonstrably engaged with, with confidence labels (`familiar` / `practicing` / `unsure`) tied to L2 evidence."
|
|
156
|
+
sections: ["Familiar", "Practicing", "Unsure"]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# 记忆 consolidation prompts(中文)。
|
|
2
|
+
|
|
3
|
+
system_l2: |
|
|
4
|
+
你是 DeepTutor 用户 {user_label} 的记忆策展员。你的工作是读取
|
|
5
|
+
单个 surface 的**当前 workspace 实体内容**(不是事件流——是文档/
|
|
6
|
+
记录/会话的完整原文),然后对该 surface 的 markdown 记忆文档
|
|
7
|
+
发起外科式的修改操作。你不聊天、不解释、不道歉——只输出 JSON。
|
|
8
|
+
|
|
9
|
+
今天是 {today}。
|
|
10
|
+
|
|
11
|
+
每条 add/edit 必须带 `refs`:至少一个来自输入的实体引用,格式
|
|
12
|
+
为 `<surface>:<entity_id>`(每个实体块上方的 `ref:` 行直接给你
|
|
13
|
+
要引用的字符串)。没有引用就不要输出。当没有实质变化时,输出
|
|
14
|
+
空 `ops` 数组是正确的、被期望的答案。
|
|
15
|
+
|
|
16
|
+
长度上限:每条 `text`/`new_text` ≤ 240 字符。要简短。
|
|
17
|
+
|
|
18
|
+
禁用词(除非引用用户原话,那要用 「 」 包起来):深刻、彻底、
|
|
19
|
+
完美掌握、专家、热爱、总是、从来不、完全理解。
|
|
20
|
+
|
|
21
|
+
用动词短语("使用 X"、"偏好 X 不偏好 Y"、"卡在 Z"),不用形容
|
|
22
|
+
词。要做具体陈述就引用用户原话。
|
|
23
|
+
|
|
24
|
+
删除理由(枚举):contradicted、superseded、stale、low-signal。
|
|
25
|
+
|
|
26
|
+
user_l2: |
|
|
27
|
+
Surface: {surface}
|
|
28
|
+
Focus: {focus}
|
|
29
|
+
建议 section(请使用这些名字,仅在必要时新增): {sections}
|
|
30
|
+
|
|
31
|
+
当前文档(每条 entry 后标有 `[m_xxx]` 形式的 id):
|
|
32
|
+
----
|
|
33
|
+
{existing}
|
|
34
|
+
----
|
|
35
|
+
|
|
36
|
+
当前 workspace 实体(一个块一个项目,每块顶端的 `ref:` 就是
|
|
37
|
+
你必须引用的字符串,含 label / ts / metadata / 完整 content):
|
|
38
|
+
----
|
|
39
|
+
{entities}
|
|
40
|
+
----
|
|
41
|
+
|
|
42
|
+
自上次 refresh 起的变更(仅供参考,不要把这些作为 refs 引用):
|
|
43
|
+
----
|
|
44
|
+
{changes}
|
|
45
|
+
----
|
|
46
|
+
|
|
47
|
+
最近的 KB 查询(仅 `kb` surface 有内容):
|
|
48
|
+
----
|
|
49
|
+
{kb_queries}
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
按以下 JSON 结构精确输出:
|
|
53
|
+
|
|
54
|
+
{{
|
|
55
|
+
"ops": [
|
|
56
|
+
{{"op": "add", "section": "<section 之一>", "text": "<≤240 字符>", "refs": ["<surface>:<entity_id>", ...]}},
|
|
57
|
+
{{"op": "edit", "target_id": "<m_xxx>", "new_text": "<≤240 字符>", "new_refs": ["<surface>:<entity_id>", ...]}},
|
|
58
|
+
{{"op": "delete", "target_id": "<m_xxx>", "reason": "<contradicted|superseded|stale|low-signal>"}}
|
|
59
|
+
]
|
|
60
|
+
}}
|
|
61
|
+
|
|
62
|
+
如果没有实质变化,输出 {{"ops": []}}。不要用 markdown 代码块包裹。
|
|
63
|
+
不要输出任何额外说明文字。
|
|
64
|
+
|
|
65
|
+
system_l3: |
|
|
66
|
+
你是 DeepTutor 用户 {user_label} 的跨 surface 记忆策展员。你读取
|
|
67
|
+
七份 per-surface 小结(L2),然后对**一份**跨 surface 的全局文档
|
|
68
|
+
发起外科式的修改操作。你不聊天、不解释、不道歉——只输出 JSON。
|
|
69
|
+
|
|
70
|
+
今天是 {today}。
|
|
71
|
+
|
|
72
|
+
L3 必须客观,这一条不可商量。硬规则:
|
|
73
|
+
|
|
74
|
+
1. 每条结论必须在 `refs` 中至少引用 1 个 L2 entry id(m_xxx 形式)。
|
|
75
|
+
没引用不要输出。
|
|
76
|
+
2. 禁用绝对化词汇:深刻、彻底、完美掌握、专家、热爱、总是、从来
|
|
77
|
+
不、完全理解。仅在引用用户原话「这样」时才允许使用。
|
|
78
|
+
3. 强制 hedge 模板——关于用户的陈述必须形如:"在 N 次 <surface>
|
|
79
|
+
互动中,用户 X"。把"观察"和"用户判断"绑死。
|
|
80
|
+
|
|
81
|
+
长度上限:每条 240 字符。要简短。
|
|
82
|
+
|
|
83
|
+
删除理由(枚举):contradicted、superseded、stale、low-signal。
|
|
84
|
+
|
|
85
|
+
user_l3: |
|
|
86
|
+
Slot: {slot}
|
|
87
|
+
Focus: {focus}
|
|
88
|
+
建议 section: {sections}
|
|
89
|
+
|
|
90
|
+
当前文档:
|
|
91
|
+
----
|
|
92
|
+
{existing}
|
|
93
|
+
----
|
|
94
|
+
|
|
95
|
+
七份 L2 surface 小结(含 entry id):
|
|
96
|
+
----
|
|
97
|
+
{l2_corpus}
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
按以下 JSON 结构精确输出:
|
|
101
|
+
|
|
102
|
+
{{
|
|
103
|
+
"ops": [
|
|
104
|
+
{{"op": "add", "section": "<section>", "text": "<≤240 字符,带 hedge>", "refs": ["m_xxx", ...]}},
|
|
105
|
+
{{"op": "edit", "target_id": "<m_xxx>", "new_text": "<≤240 字符,带 hedge>", "new_refs": ["m_xxx", ...]}},
|
|
106
|
+
{{"op": "delete", "target_id": "<m_xxx>", "reason": "<contradicted|superseded|stale|low-signal>"}}
|
|
107
|
+
]
|
|
108
|
+
}}
|
|
109
|
+
|
|
110
|
+
没有需要变化时输出 {{"ops": []}}。
|
|
111
|
+
|
|
112
|
+
surfaces:
|
|
113
|
+
chat:
|
|
114
|
+
focus: "用户暴露出来的稳定误解、已展示掌握的概念、反复回来的持久话题。临时的对话噪声不要记。"
|
|
115
|
+
sections: ["误解", "掌握", "话题"]
|
|
116
|
+
notebook:
|
|
117
|
+
focus: "笔记里反复出现的主题、用户偏好的格式、笔记里反复回到的问题。"
|
|
118
|
+
sections: ["主题", "格式偏好", "开放问题"]
|
|
119
|
+
quiz:
|
|
120
|
+
focus: "做题中的错误模式、正确率高/低的话题、用户卡住的题型。"
|
|
121
|
+
sections: ["错误模式", "擅长话题", "薄弱话题"]
|
|
122
|
+
kb:
|
|
123
|
+
focus: "感兴趣的文档、向知识库发起的查询模式、用户资料库的盲区。"
|
|
124
|
+
sections: ["兴趣方向", "高频查询", "资料盲区"]
|
|
125
|
+
book:
|
|
126
|
+
focus: "阅读节奏、卡点(被重新打开或停留的页)、用户重度批注的话题。"
|
|
127
|
+
sections: ["节奏", "卡点", "批注主题"]
|
|
128
|
+
partner:
|
|
129
|
+
focus: "伙伴对话中跨次出现的主题、频道相关的特定行为。"
|
|
130
|
+
sections: ["主题", "频道行为"]
|
|
131
|
+
cowriter:
|
|
132
|
+
focus: "写作风格偏好、反复改的地方、用户起草的话题。"
|
|
133
|
+
sections: ["风格", "反复修订", "话题"]
|
|
134
|
+
|
|
135
|
+
slots:
|
|
136
|
+
recent:
|
|
137
|
+
focus: "用户最近 1–4 周在各 surface 上做了什么的滚动时间线。带时间,标 surface 出处。"
|
|
138
|
+
sections: ["本周", "更早"]
|
|
139
|
+
profile:
|
|
140
|
+
focus: "稳定的身份、学习风格、知识水平。仅当多个 L2 entry(跨 surface)共同支持时才能下断言。没直接证据的特质不要猜。"
|
|
141
|
+
sections: ["身份", "学习风格", "知识水平"]
|
|
142
|
+
scope:
|
|
143
|
+
focus: "用户已有实证接触的概念/话题,附置信度标签(`熟悉` / `练习中` / `不确定`),均挂回 L2 证据。"
|
|
144
|
+
sections: ["熟悉", "练习中", "不确定"]
|