unique-user-memory 2026.32.0.dev3__tar.gz → 2026.32.0.dev4__tar.gz
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.
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/PKG-INFO +7 -4
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/README.md +6 -3
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/pyproject.toml +1 -1
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/tests/test_user_memory.py +32 -0
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/user_memory.py +13 -6
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/__init__.py +0 -0
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/config.py +0 -0
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/user_memory_message_log.py +0 -0
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/user_memory_postprocessor.py +0 -0
- {unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/user_memory_prompts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-user-memory
|
|
3
|
-
Version: 2026.32.0.
|
|
3
|
+
Version: 2026.32.0.dev4
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Fabian Schläpfer
|
|
6
6
|
Author-email: Fabian Schläpfer <fabian@unique.ch>
|
|
@@ -24,6 +24,7 @@ The package provides:
|
|
|
24
24
|
|
|
25
25
|
- `UserMemoryConfig` - Pydantic configuration for the consolidation model, profile token budget, and memory folder.
|
|
26
26
|
- `load_user_memory(...)` - resolves the user's private memory folder, downloads `memory.md`, and enforces the configured token budget. The `language_model` argument is used to tokenize `memory.md` when capping it, so it must be the same effective model the postprocessor uses for consolidation (see Integration below). Returns a `UserMemoryState` with the profile text and scope id.
|
|
27
|
+
- `profile_body(...)` - strips the YAML frontmatter and returns only the Markdown body. Use it whenever the profile is shown to a model; the frontmatter is bookkeeping for consolidation.
|
|
27
28
|
- `UserMemoryMessageLogger` - emits chat Steps (MessageLogs) for load and update, including typed `UserMemory` detail entries the chat frontend renders as a badge that opens Settings → Context Memory. Frontends that do not know the entry type render nothing, so the entries are safe to emit in any deploy order.
|
|
28
29
|
- `UserMemoryPostprocessor` - runs after the assistant response, consolidates the latest turn into the profile, and uploads the updated `memory.md`.
|
|
29
30
|
|
|
@@ -34,7 +35,7 @@ The memory file is intentionally small and structured. It is rewritten as a full
|
|
|
34
35
|
1. The orchestrator enables memory when `space.allow_user_memory` is true.
|
|
35
36
|
2. The orchestrator emits a **Loading context memory** Step, then `load_user_memory(...)` resolves the pre-provisioned root folder, ensures a private child folder for the current user, and downloads `/user-memory/<user_id>/memory.md` if it exists.
|
|
36
37
|
3. When load returns a `UserMemoryState`, that Step is completed with a **Context memory** detail entry (`type: UserMemory`) that the chat frontend renders as a badge opening Settings → Context Memory. A successful `None` return (soft skip) completes the Step without the entry; a raised exception marks the Step failed.
|
|
37
|
-
4. If memory was loaded, its text is passed into the agent context for the current turn.
|
|
38
|
+
4. If memory was loaded, `profile_body(...)` of its text is passed into the agent context for the current turn — the prompt only gets the Markdown body, while the postprocessor keeps the full file because it needs the frontmatter to carry `turn_count` forward.
|
|
38
39
|
5. `UserMemoryPostprocessor` runs after the assistant response.
|
|
39
40
|
6. The package asks the configured language model to either return `NOOP` or a complete rewritten profile.
|
|
40
41
|
7. If a rewrite runs, an **Updating your memory** Step is shown while consolidating (no settings entry yet).
|
|
@@ -113,7 +114,7 @@ Typical orchestration code loads memory before the agent loop and registers the
|
|
|
113
114
|
|
|
114
115
|
```python
|
|
115
116
|
from unique_toolkit.agentic.message_log_manager.service import MessageStepLogger
|
|
116
|
-
from unique_user_memory.user_memory import load_user_memory
|
|
117
|
+
from unique_user_memory.user_memory import load_user_memory, profile_body
|
|
117
118
|
from unique_user_memory.user_memory_message_log import UserMemoryMessageLogger
|
|
118
119
|
from unique_user_memory.user_memory_postprocessor import UserMemoryPostprocessor
|
|
119
120
|
|
|
@@ -157,7 +158,9 @@ finally:
|
|
|
157
158
|
|
|
158
159
|
if load_succeeded and user_memory_state is not None:
|
|
159
160
|
await memory_message_step_logger.log_loading_complete(with_settings_entry=True)
|
|
160
|
-
|
|
161
|
+
# The postprocessor keeps the full file (it needs the frontmatter to
|
|
162
|
+
# carry turn_count forward); the prompt only gets the Markdown body.
|
|
163
|
+
user_memory_text = profile_body(user_memory_state.text)
|
|
161
164
|
postprocessor_manager.add_postprocessor(
|
|
162
165
|
UserMemoryPostprocessor(
|
|
163
166
|
config=user_memory_config,
|
|
@@ -10,6 +10,7 @@ The package provides:
|
|
|
10
10
|
|
|
11
11
|
- `UserMemoryConfig` - Pydantic configuration for the consolidation model, profile token budget, and memory folder.
|
|
12
12
|
- `load_user_memory(...)` - resolves the user's private memory folder, downloads `memory.md`, and enforces the configured token budget. The `language_model` argument is used to tokenize `memory.md` when capping it, so it must be the same effective model the postprocessor uses for consolidation (see Integration below). Returns a `UserMemoryState` with the profile text and scope id.
|
|
13
|
+
- `profile_body(...)` - strips the YAML frontmatter and returns only the Markdown body. Use it whenever the profile is shown to a model; the frontmatter is bookkeeping for consolidation.
|
|
13
14
|
- `UserMemoryMessageLogger` - emits chat Steps (MessageLogs) for load and update, including typed `UserMemory` detail entries the chat frontend renders as a badge that opens Settings → Context Memory. Frontends that do not know the entry type render nothing, so the entries are safe to emit in any deploy order.
|
|
14
15
|
- `UserMemoryPostprocessor` - runs after the assistant response, consolidates the latest turn into the profile, and uploads the updated `memory.md`.
|
|
15
16
|
|
|
@@ -20,7 +21,7 @@ The memory file is intentionally small and structured. It is rewritten as a full
|
|
|
20
21
|
1. The orchestrator enables memory when `space.allow_user_memory` is true.
|
|
21
22
|
2. The orchestrator emits a **Loading context memory** Step, then `load_user_memory(...)` resolves the pre-provisioned root folder, ensures a private child folder for the current user, and downloads `/user-memory/<user_id>/memory.md` if it exists.
|
|
22
23
|
3. When load returns a `UserMemoryState`, that Step is completed with a **Context memory** detail entry (`type: UserMemory`) that the chat frontend renders as a badge opening Settings → Context Memory. A successful `None` return (soft skip) completes the Step without the entry; a raised exception marks the Step failed.
|
|
23
|
-
4. If memory was loaded, its text is passed into the agent context for the current turn.
|
|
24
|
+
4. If memory was loaded, `profile_body(...)` of its text is passed into the agent context for the current turn — the prompt only gets the Markdown body, while the postprocessor keeps the full file because it needs the frontmatter to carry `turn_count` forward.
|
|
24
25
|
5. `UserMemoryPostprocessor` runs after the assistant response.
|
|
25
26
|
6. The package asks the configured language model to either return `NOOP` or a complete rewritten profile.
|
|
26
27
|
7. If a rewrite runs, an **Updating your memory** Step is shown while consolidating (no settings entry yet).
|
|
@@ -99,7 +100,7 @@ Typical orchestration code loads memory before the agent loop and registers the
|
|
|
99
100
|
|
|
100
101
|
```python
|
|
101
102
|
from unique_toolkit.agentic.message_log_manager.service import MessageStepLogger
|
|
102
|
-
from unique_user_memory.user_memory import load_user_memory
|
|
103
|
+
from unique_user_memory.user_memory import load_user_memory, profile_body
|
|
103
104
|
from unique_user_memory.user_memory_message_log import UserMemoryMessageLogger
|
|
104
105
|
from unique_user_memory.user_memory_postprocessor import UserMemoryPostprocessor
|
|
105
106
|
|
|
@@ -143,7 +144,9 @@ finally:
|
|
|
143
144
|
|
|
144
145
|
if load_succeeded and user_memory_state is not None:
|
|
145
146
|
await memory_message_step_logger.log_loading_complete(with_settings_entry=True)
|
|
146
|
-
|
|
147
|
+
# The postprocessor keeps the full file (it needs the frontmatter to
|
|
148
|
+
# carry turn_count forward); the prompt only gets the Markdown body.
|
|
149
|
+
user_memory_text = profile_body(user_memory_state.text)
|
|
147
150
|
postprocessor_manager.add_postprocessor(
|
|
148
151
|
UserMemoryPostprocessor(
|
|
149
152
|
config=user_memory_config,
|
|
@@ -21,6 +21,7 @@ from unique_user_memory.user_memory import (
|
|
|
21
21
|
enforce_token_cap,
|
|
22
22
|
ensure_user_memory_folder,
|
|
23
23
|
fit_user_memory,
|
|
24
|
+
profile_body,
|
|
24
25
|
should_consolidate_memory,
|
|
25
26
|
upload_user_memory,
|
|
26
27
|
)
|
|
@@ -46,6 +47,37 @@ def test_memory_profile_keeps_follow_up_tasks_but_excludes_open_questions() -> N
|
|
|
46
47
|
assert "Concrete future tasks the user intends to complete" in gate_prompt
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
def test_profile_body_strips_frontmatter() -> None:
|
|
51
|
+
# The orchestrator renders this into the system prompt, where the
|
|
52
|
+
# bookkeeping fields are noise for the model.
|
|
53
|
+
content = (
|
|
54
|
+
"---\n"
|
|
55
|
+
"user_id: 233737684428787846\n"
|
|
56
|
+
"strategy: codex\n"
|
|
57
|
+
"schema_version: 1\n"
|
|
58
|
+
"last_updated: 2026-07-27T10:20:00+00:00\n"
|
|
59
|
+
"turn_count: 83\n"
|
|
60
|
+
"---\n\n"
|
|
61
|
+
"# User Memory\n\n## Identity\n- Andreas\n"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
body = profile_body(content)
|
|
65
|
+
|
|
66
|
+
assert body == "# User Memory\n\n## Identity\n- Andreas"
|
|
67
|
+
for field in ("user_id:", "strategy:", "schema_version:", "turn_count:"):
|
|
68
|
+
assert field not in body
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_profile_body_leaves_frontmatterless_profile_untouched() -> None:
|
|
72
|
+
content = "# User Memory\n\n## Identity\n- Andreas"
|
|
73
|
+
|
|
74
|
+
assert profile_body(content) == content
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_profile_body_returns_empty_for_frontmatter_only_profile() -> None:
|
|
78
|
+
assert profile_body("---\nuser_id: 42\nturn_count: 0\n---\n") == ""
|
|
79
|
+
|
|
80
|
+
|
|
49
81
|
def test_enforce_token_cap_truncates_long_content() -> None:
|
|
50
82
|
content = "\n\n".join(f"paragraph {index} " + "word " * 40 for index in range(50))
|
|
51
83
|
|
|
@@ -63,7 +63,14 @@ _FRONTMATTER_RE = re.compile(r"^---\n.*?\n---\n", re.DOTALL)
|
|
|
63
63
|
_TURN_COUNT_RE = re.compile(r"^turn_count:\s*(\d+)\s*$", re.MULTILINE)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def
|
|
66
|
+
def profile_body(content: str) -> str:
|
|
67
|
+
"""Return the profile without its YAML frontmatter.
|
|
68
|
+
|
|
69
|
+
The frontmatter is bookkeeping for the consolidation pass (turn count,
|
|
70
|
+
schema version, timestamps); consumers that show the profile to a model --
|
|
71
|
+
consolidation prompts and the orchestrator system prompt -- only want the
|
|
72
|
+
Markdown body.
|
|
73
|
+
"""
|
|
67
74
|
return _FRONTMATTER_RE.sub("", content, count=1).strip()
|
|
68
75
|
|
|
69
76
|
|
|
@@ -195,7 +202,7 @@ async def condense_user_memory(
|
|
|
195
202
|
condensed profile, or ``None`` when the call fails or the output does
|
|
196
203
|
not look like a profile (the caller then falls back to a hard cut).
|
|
197
204
|
"""
|
|
198
|
-
body =
|
|
205
|
+
body = profile_body(content)
|
|
199
206
|
current_tokens = count_tokens(content=body, language_model=language_model)
|
|
200
207
|
target_tokens = max(1, int(max_tokens * _CONDENSE_TARGET_RATIO))
|
|
201
208
|
|
|
@@ -265,7 +272,7 @@ async def condense_user_memory(
|
|
|
265
272
|
)
|
|
266
273
|
return None
|
|
267
274
|
|
|
268
|
-
candidate =
|
|
275
|
+
candidate = profile_body(_strip_code_fences(raw))
|
|
269
276
|
if not _is_well_formed_profile(candidate):
|
|
270
277
|
logger.warning(
|
|
271
278
|
"[user-memory] condense output did not look like a profile (%d chars)",
|
|
@@ -826,7 +833,7 @@ async def _rewrite_user_memory(
|
|
|
826
833
|
),
|
|
827
834
|
LanguageModelUserMessage(
|
|
828
835
|
content=consolidation_user_prompt(
|
|
829
|
-
existing_memory=
|
|
836
|
+
existing_memory=profile_body(safe_current),
|
|
830
837
|
user_message=_sanitize_for_xml_context(user_message or ""),
|
|
831
838
|
assistant_message=_sanitize_for_xml_context(
|
|
832
839
|
assistant_message or ""
|
|
@@ -883,7 +890,7 @@ async def _rewrite_user_memory(
|
|
|
883
890
|
logger.info("[user-memory] consolidation NOOP - keeping existing memory")
|
|
884
891
|
return safe_current
|
|
885
892
|
|
|
886
|
-
candidate_body =
|
|
893
|
+
candidate_body = profile_body(_strip_code_fences(raw))
|
|
887
894
|
if not _is_well_formed_profile(candidate_body):
|
|
888
895
|
logger.warning(
|
|
889
896
|
"[user-memory] LLM output did not look like a profile (%d chars)",
|
|
@@ -891,7 +898,7 @@ async def _rewrite_user_memory(
|
|
|
891
898
|
)
|
|
892
899
|
return safe_current
|
|
893
900
|
|
|
894
|
-
if safe_current and candidate_body ==
|
|
901
|
+
if safe_current and candidate_body == profile_body(safe_current):
|
|
895
902
|
logger.debug("[user-memory] memory body unchanged - skipping update")
|
|
896
903
|
return safe_current
|
|
897
904
|
|
|
File without changes
|
{unique_user_memory-2026.32.0.dev3 → unique_user_memory-2026.32.0.dev4}/unique_user_memory/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|