everalgo-user-memory 0.1.0__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.
Files changed (30) hide show
  1. everalgo_user_memory-0.1.0/.gitignore +58 -0
  2. everalgo_user_memory-0.1.0/CHANGELOG.md +27 -0
  3. everalgo_user_memory-0.1.0/LICENSE +21 -0
  4. everalgo_user_memory-0.1.0/PKG-INFO +173 -0
  5. everalgo_user_memory-0.1.0/README.md +152 -0
  6. everalgo_user_memory-0.1.0/pyproject.toml +32 -0
  7. everalgo_user_memory-0.1.0/src/everalgo/user_memory/__init__.py +28 -0
  8. everalgo_user_memory-0.1.0/src/everalgo/user_memory/_render.py +28 -0
  9. everalgo_user_memory-0.1.0/src/everalgo/user_memory/atomic_fact.py +158 -0
  10. everalgo_user_memory-0.1.0/src/everalgo/user_memory/boundary.py +57 -0
  11. everalgo_user_memory-0.1.0/src/everalgo/user_memory/episode.py +159 -0
  12. everalgo_user_memory-0.1.0/src/everalgo/user_memory/foresight.py +227 -0
  13. everalgo_user_memory-0.1.0/src/everalgo/user_memory/profile.py +326 -0
  14. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/__init__.py +5 -0
  15. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/__init__.py +1 -0
  16. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/atomic_fact.py +88 -0
  17. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/episode.py +189 -0
  18. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/foresight.py +156 -0
  19. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/profile.py +217 -0
  20. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/__init__.py +1 -0
  21. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/atomic_fact.py +85 -0
  22. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/episode.py +189 -0
  23. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/foresight.py +148 -0
  24. everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/profile.py +196 -0
  25. everalgo_user_memory-0.1.0/src/everalgo/user_memory/py.typed +0 -0
  26. everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_atomic_fact.py +299 -0
  27. everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_episode.py +462 -0
  28. everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_foresight.py +378 -0
  29. everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_profile.py +530 -0
  30. everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_public_api.py +49 -0
@@ -0,0 +1,58 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+
13
+ # Environments
14
+ .env
15
+ .env.local
16
+ .venv
17
+ env/
18
+ venv/
19
+
20
+ # uv
21
+ .uv/
22
+
23
+ # Test / coverage / cache
24
+ .pytest_cache/
25
+ .cache
26
+ .coverage
27
+ .coverage.*
28
+ coverage.xml
29
+ report.xml
30
+ htmlcov/
31
+ .tox/
32
+ .nox/
33
+ .hypothesis/
34
+
35
+ # Type checkers / linters
36
+ .mypy_cache/
37
+ .dmypy.json
38
+ .pyre/
39
+ .pytype/
40
+ .ruff_cache/
41
+
42
+ # Jupyter
43
+ .ipynb_checkpoints/
44
+
45
+ # IDE / OS
46
+ .vscode/
47
+ .idea/
48
+ *.swp
49
+ .DS_Store
50
+
51
+ # Logs
52
+ *.log
53
+
54
+ # Project local artifacts (placed under local/ per project convention)
55
+ local/
56
+
57
+ # Claude Code session-local state (scheduled tasks, agent caches, locks)
58
+ .claude/
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. Format follows
4
+ [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Versioning
5
+ follows [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - `BoundaryDetector`: facade class wrapping `everalgo.boundary.detect_boundaries`; accepts `llm=` at construction time and manages the carry-forward `tail` across calls.
12
+ - `EpisodeExtractor`: per-sender Episode fan-out — one LLM call per unique `sender_id` found in the `MemCell`; accepts `llm=` and `prompt=` at construction time.
13
+ - `ForesightExtractor`: single `MemCell` → `list[Foresight]`; async with sync bridge via `asgiref.async_to_sync`.
14
+ - `AtomicFactExtractor`: single `MemCell` → `list[AtomicFact]`; async with sync bridge.
15
+ - `ProfileExtractor`: chronological `list[MemCell]` (last element = most recent) → single `Profile`; single-shot LLM snapshot.
16
+ - English and Chinese prompts for all four extractors under `user_memory/prompts/{en,zh}/`.
17
+ - `WorkspaceMemCellExtractor` re-export from `everalgo.boundary` for callers that import from one place.
18
+ - `DetectionResult` re-export from `everalgo.boundary`.
19
+
20
+ ### Changed
21
+
22
+ - `BoundaryDetector` renamed from `UserBoundaryDetector` (which was itself renamed from `ChatBoundaryDetector`) to match the no-prefix naming convention used across the package.
23
+ - `EpisodeExtractor.aextract` parameter `owner_id` renamed to `sender_id` to align with `ChatMessage.sender_id`.
24
+ - `ProfileExtractor` signature changed from separate `memcell` + `cluster_episodes` parameters to a single `memcells: Sequence[MemCell]` list, matching the other extractor contracts.
25
+ - `Episode`, `Foresight`, `AtomicFact`, `Profile` schemas dropped `parent_id` / `parent_type` fields and the `id` field; schemas now carry only the minimal required fields plus `ConfigDict(extra="allow")`.
26
+
27
+ [Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/main...HEAD
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EverMind
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: everalgo-user-memory
3
+ Version: 0.1.0
4
+ Summary: EverAlgo user memory: Episode / Foresight / AtomicFact / Profile extractors (re-exports boundary as Chat/WorkspaceMemCellExtractor).
5
+ Project-URL: Homepage, https://github.com/EverMind-AI/EverAlgo
6
+ Project-URL: Repository, https://github.com/EverMind-AI/EverAlgo
7
+ Project-URL: Issues, https://github.com/EverMind-AI/EverAlgo/issues
8
+ Project-URL: Documentation, https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-user-memory
9
+ Project-URL: Changelog, https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-user-memory/CHANGELOG.md
10
+ Author: EverMind
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: asgiref>=3.0
18
+ Requires-Dist: everalgo-boundary<2.0.0,>=0.1.0
19
+ Requires-Dist: everalgo-core<2.0.0,>=0.1.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # everalgo-user-memory
23
+
24
+ User-side memory products for EverAlgo — four LLM-backed extractors (`EpisodeExtractor`, `ForesightExtractor`, `AtomicFactExtractor`, `ProfileExtractor`) plus a `BoundaryDetector` class facade that wraps `everalgo-boundary`.
25
+
26
+ See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install everalgo-user-memory
32
+ # Auto-pulls: everalgo-core, everalgo-boundary
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ All extractors are stateless classes; pass `llm=` at construction time. The `sender_id` argument is always required and is not inferred from the conversation.
38
+
39
+ ```python
40
+ import asyncio
41
+ import json
42
+
43
+ from everalgo.llm.types import ChatResponse
44
+ from everalgo.testing.fake_llm import FakeLLMClient
45
+ from everalgo.types import ChatMessage, MemCell
46
+ from everalgo.user_memory import (
47
+ BoundaryDetector,
48
+ EpisodeExtractor,
49
+ ForesightExtractor,
50
+ AtomicFactExtractor,
51
+ ProfileExtractor,
52
+ )
53
+
54
+ _BOUNDARY_JSON = json.dumps({"reasoning": "single topic", "boundaries": [], "should_wait": False})
55
+ _EPISODE_JSON = json.dumps({"title": "Alice asks about async retries", "content": "Alice explored async retry patterns."})
56
+ _FORE_JSON = json.dumps([{"content": "Alice will read the follow-up doc", "evidence": "assistant promised a doc", "start_time": "2023-11-14", "end_time": "2023-11-21", "duration_days": 7}])
57
+ _FACT_JSON = json.dumps({"atomic_facts": {"time": "Nov 14 2023", "atomic_fact": ["Alice is learning Python async."]}})
58
+ _PROFILE_JSON = json.dumps({"explicit_info": [], "implicit_traits": [{"category": "Technical", "description": "Python developer."}]})
59
+
60
+
61
+ async def main() -> None:
62
+ messages = [
63
+ ChatMessage(id="m1", role="user", content="I want to learn Python async retry patterns.", timestamp=1_700_000_000_000, sender_id="u_alice", sender_name="Alice"),
64
+ ChatMessage(id="m2", role="assistant", content="Sure — I'll send a follow-up doc next week.", timestamp=1_700_000_001_000, sender_id="assistant"),
65
+ ]
66
+
67
+ fake = FakeLLMClient(responses=[
68
+ ChatResponse(content=_BOUNDARY_JSON, model="fake"),
69
+ ChatResponse(content=_EPISODE_JSON, model="fake"),
70
+ ChatResponse(content=_FORE_JSON, model="fake"),
71
+ ChatResponse(content=_FACT_JSON, model="fake"),
72
+ ChatResponse(content=_PROFILE_JSON, model="fake"),
73
+ ])
74
+
75
+ # Step 1: boundary detection → MemCell
76
+ result = await BoundaryDetector(llm=fake).adetect(messages, is_final=True)
77
+ mc = result.cells[0]
78
+
79
+ # Step 2–4: user-memory extractors
80
+ episode = await EpisodeExtractor(llm=fake).aextract(mc, sender_id="u_alice")
81
+ foresights = await ForesightExtractor(llm=fake).aextract(mc, sender_id="u_alice")
82
+ facts = await AtomicFactExtractor(llm=fake).aextract(mc, sender_id="u_alice")
83
+
84
+ # Step 5: Profile takes a chronological Sequence[MemCell]; last is most recent
85
+ profile = await ProfileExtractor(llm=fake).aextract([mc], sender_id="u_alice")
86
+
87
+ print(episode.subject, profile.summary)
88
+
89
+
90
+ asyncio.run(main())
91
+ ```
92
+
93
+ See [`examples/06_full_user_memory_pipeline.py`](../../examples/06_full_user_memory_pipeline.py) for the complete end-to-end example including geometry clustering.
94
+
95
+ ## Customising prompts
96
+
97
+ Each extractor accepts a `prompt=` override per call, or the module-level constant can be monkey-patched at startup for a global override:
98
+
99
+ ```python
100
+ # Per-call: use the bundled Chinese variant
101
+ from everalgo.user_memory.prompts.zh.episode import EPISODE_EXTRACT_PROMPT_ZH
102
+ episode = await EpisodeExtractor(llm=client).aextract(mc, sender_id="u_alice", prompt=EPISODE_EXTRACT_PROMPT_ZH)
103
+
104
+ # Global: replace the default English prompt at startup
105
+ import everalgo.user_memory.prompts.en.foresight as _fs
106
+ _fs.FORESIGHT_GENERATION_PROMPT = my_custom_prompt
107
+ ```
108
+
109
+ ## API surface
110
+
111
+ ```python
112
+ class BoundaryDetector:
113
+ def __init__(self, *, llm: LLMClient) -> None: ...
114
+ async def adetect(
115
+ self, messages: list[ChatMessage], *, is_final: bool = False, prompt: str | None = None
116
+ ) -> DetectionResult: ...
117
+
118
+ class EpisodeExtractor:
119
+ def __init__(self, *, llm: LLMClient) -> None: ...
120
+ async def aextract(
121
+ self, memcell: MemCell, *,
122
+ sender_id: str | None, # None → generic whole-memcell episode (cheaper)
123
+ prompt: str | None = None,
124
+ custom_instructions: str | None = None,
125
+ ) -> Episode: ...
126
+
127
+ class ForesightExtractor:
128
+ def __init__(self, *, llm: LLMClient) -> None: ...
129
+ async def aextract(
130
+ self, memcell: MemCell, *, sender_id: str, prompt: str | None = None
131
+ ) -> list[Foresight]: ...
132
+
133
+ class AtomicFactExtractor:
134
+ def __init__(self, *, llm: LLMClient) -> None: ...
135
+ async def aextract(
136
+ self, memcell: MemCell, *,
137
+ sender_id: str | None, # None → generic facts not bound to any user
138
+ prompt: str | None = None,
139
+ ) -> list[AtomicFact]: ...
140
+
141
+ class ProfileExtractor:
142
+ def __init__(self, *, llm: LLMClient) -> None: ...
143
+ async def aextract(
144
+ self, memcells: Sequence[MemCell], *,
145
+ sender_id: str,
146
+ old_profile: Profile | None = None, # None → INIT mode; present → UPDATE mode
147
+ prompt: str | None = None,
148
+ ) -> Profile: ...
149
+ ```
150
+
151
+ `EpisodeExtractor` has two modes: pass `sender_id=str` to extract a user-focused episode (uses `USER_EPISODE_GENERATION_PROMPT`); pass `sender_id=None` for a generic whole-memcell episode (uses `EPISODE_GENERATION_PROMPT`).
152
+
153
+ `ProfileExtractor` has two modes: `old_profile=None` triggers INIT extraction; passing an existing profile triggers UPDATE (LLM emits add/update/delete ops). When the merged profile exceeds an internal item count threshold a second compact LLM pass runs automatically — this is transparent to the caller.
154
+
155
+ All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)` — only for non-event-loop callers (CLI scripts, plain unit tests).
156
+
157
+ ## Testing
158
+
159
+ ```python
160
+ from everalgo.testing import FakeLLMClient, assert_episode_shape
161
+
162
+ fake = FakeLLMClient(responses=[ChatResponse(content=_EPISODE_JSON, model="fake")])
163
+ episode = await EpisodeExtractor(llm=fake).aextract(mc, sender_id="u_alice")
164
+ assert_episode_shape(episode)
165
+ ```
166
+
167
+ See the integration test pattern in [`tests/integration/`](../../tests/integration/).
168
+
169
+ ## Related distributions
170
+
171
+ - [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `BoundaryDetector`
172
+ - [`everalgo-clustering`](../everalgo-clustering/) — geometry / LLM clustering for grouping MemCells before `ProfileExtractor`
173
+ - [`everalgo-rank`](../everalgo-rank/) — ranks `Episode`, `AtomicFact`, `Profile` candidates at read time
@@ -0,0 +1,152 @@
1
+ # everalgo-user-memory
2
+
3
+ User-side memory products for EverAlgo — four LLM-backed extractors (`EpisodeExtractor`, `ForesightExtractor`, `AtomicFactExtractor`, `ProfileExtractor`) plus a `BoundaryDetector` class facade that wraps `everalgo-boundary`.
4
+
5
+ See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install everalgo-user-memory
11
+ # Auto-pulls: everalgo-core, everalgo-boundary
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ All extractors are stateless classes; pass `llm=` at construction time. The `sender_id` argument is always required and is not inferred from the conversation.
17
+
18
+ ```python
19
+ import asyncio
20
+ import json
21
+
22
+ from everalgo.llm.types import ChatResponse
23
+ from everalgo.testing.fake_llm import FakeLLMClient
24
+ from everalgo.types import ChatMessage, MemCell
25
+ from everalgo.user_memory import (
26
+ BoundaryDetector,
27
+ EpisodeExtractor,
28
+ ForesightExtractor,
29
+ AtomicFactExtractor,
30
+ ProfileExtractor,
31
+ )
32
+
33
+ _BOUNDARY_JSON = json.dumps({"reasoning": "single topic", "boundaries": [], "should_wait": False})
34
+ _EPISODE_JSON = json.dumps({"title": "Alice asks about async retries", "content": "Alice explored async retry patterns."})
35
+ _FORE_JSON = json.dumps([{"content": "Alice will read the follow-up doc", "evidence": "assistant promised a doc", "start_time": "2023-11-14", "end_time": "2023-11-21", "duration_days": 7}])
36
+ _FACT_JSON = json.dumps({"atomic_facts": {"time": "Nov 14 2023", "atomic_fact": ["Alice is learning Python async."]}})
37
+ _PROFILE_JSON = json.dumps({"explicit_info": [], "implicit_traits": [{"category": "Technical", "description": "Python developer."}]})
38
+
39
+
40
+ async def main() -> None:
41
+ messages = [
42
+ ChatMessage(id="m1", role="user", content="I want to learn Python async retry patterns.", timestamp=1_700_000_000_000, sender_id="u_alice", sender_name="Alice"),
43
+ ChatMessage(id="m2", role="assistant", content="Sure — I'll send a follow-up doc next week.", timestamp=1_700_000_001_000, sender_id="assistant"),
44
+ ]
45
+
46
+ fake = FakeLLMClient(responses=[
47
+ ChatResponse(content=_BOUNDARY_JSON, model="fake"),
48
+ ChatResponse(content=_EPISODE_JSON, model="fake"),
49
+ ChatResponse(content=_FORE_JSON, model="fake"),
50
+ ChatResponse(content=_FACT_JSON, model="fake"),
51
+ ChatResponse(content=_PROFILE_JSON, model="fake"),
52
+ ])
53
+
54
+ # Step 1: boundary detection → MemCell
55
+ result = await BoundaryDetector(llm=fake).adetect(messages, is_final=True)
56
+ mc = result.cells[0]
57
+
58
+ # Step 2–4: user-memory extractors
59
+ episode = await EpisodeExtractor(llm=fake).aextract(mc, sender_id="u_alice")
60
+ foresights = await ForesightExtractor(llm=fake).aextract(mc, sender_id="u_alice")
61
+ facts = await AtomicFactExtractor(llm=fake).aextract(mc, sender_id="u_alice")
62
+
63
+ # Step 5: Profile takes a chronological Sequence[MemCell]; last is most recent
64
+ profile = await ProfileExtractor(llm=fake).aextract([mc], sender_id="u_alice")
65
+
66
+ print(episode.subject, profile.summary)
67
+
68
+
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ See [`examples/06_full_user_memory_pipeline.py`](../../examples/06_full_user_memory_pipeline.py) for the complete end-to-end example including geometry clustering.
73
+
74
+ ## Customising prompts
75
+
76
+ Each extractor accepts a `prompt=` override per call, or the module-level constant can be monkey-patched at startup for a global override:
77
+
78
+ ```python
79
+ # Per-call: use the bundled Chinese variant
80
+ from everalgo.user_memory.prompts.zh.episode import EPISODE_EXTRACT_PROMPT_ZH
81
+ episode = await EpisodeExtractor(llm=client).aextract(mc, sender_id="u_alice", prompt=EPISODE_EXTRACT_PROMPT_ZH)
82
+
83
+ # Global: replace the default English prompt at startup
84
+ import everalgo.user_memory.prompts.en.foresight as _fs
85
+ _fs.FORESIGHT_GENERATION_PROMPT = my_custom_prompt
86
+ ```
87
+
88
+ ## API surface
89
+
90
+ ```python
91
+ class BoundaryDetector:
92
+ def __init__(self, *, llm: LLMClient) -> None: ...
93
+ async def adetect(
94
+ self, messages: list[ChatMessage], *, is_final: bool = False, prompt: str | None = None
95
+ ) -> DetectionResult: ...
96
+
97
+ class EpisodeExtractor:
98
+ def __init__(self, *, llm: LLMClient) -> None: ...
99
+ async def aextract(
100
+ self, memcell: MemCell, *,
101
+ sender_id: str | None, # None → generic whole-memcell episode (cheaper)
102
+ prompt: str | None = None,
103
+ custom_instructions: str | None = None,
104
+ ) -> Episode: ...
105
+
106
+ class ForesightExtractor:
107
+ def __init__(self, *, llm: LLMClient) -> None: ...
108
+ async def aextract(
109
+ self, memcell: MemCell, *, sender_id: str, prompt: str | None = None
110
+ ) -> list[Foresight]: ...
111
+
112
+ class AtomicFactExtractor:
113
+ def __init__(self, *, llm: LLMClient) -> None: ...
114
+ async def aextract(
115
+ self, memcell: MemCell, *,
116
+ sender_id: str | None, # None → generic facts not bound to any user
117
+ prompt: str | None = None,
118
+ ) -> list[AtomicFact]: ...
119
+
120
+ class ProfileExtractor:
121
+ def __init__(self, *, llm: LLMClient) -> None: ...
122
+ async def aextract(
123
+ self, memcells: Sequence[MemCell], *,
124
+ sender_id: str,
125
+ old_profile: Profile | None = None, # None → INIT mode; present → UPDATE mode
126
+ prompt: str | None = None,
127
+ ) -> Profile: ...
128
+ ```
129
+
130
+ `EpisodeExtractor` has two modes: pass `sender_id=str` to extract a user-focused episode (uses `USER_EPISODE_GENERATION_PROMPT`); pass `sender_id=None` for a generic whole-memcell episode (uses `EPISODE_GENERATION_PROMPT`).
131
+
132
+ `ProfileExtractor` has two modes: `old_profile=None` triggers INIT extraction; passing an existing profile triggers UPDATE (LLM emits add/update/delete ops). When the merged profile exceeds an internal item count threshold a second compact LLM pass runs automatically — this is transparent to the caller.
133
+
134
+ All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)` — only for non-event-loop callers (CLI scripts, plain unit tests).
135
+
136
+ ## Testing
137
+
138
+ ```python
139
+ from everalgo.testing import FakeLLMClient, assert_episode_shape
140
+
141
+ fake = FakeLLMClient(responses=[ChatResponse(content=_EPISODE_JSON, model="fake")])
142
+ episode = await EpisodeExtractor(llm=fake).aextract(mc, sender_id="u_alice")
143
+ assert_episode_shape(episode)
144
+ ```
145
+
146
+ See the integration test pattern in [`tests/integration/`](../../tests/integration/).
147
+
148
+ ## Related distributions
149
+
150
+ - [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `BoundaryDetector`
151
+ - [`everalgo-clustering`](../everalgo-clustering/) — geometry / LLM clustering for grouping MemCells before `ProfileExtractor`
152
+ - [`everalgo-rank`](../everalgo-rank/) — ranks `Episode`, `AtomicFact`, `Profile` candidates at read time
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "everalgo-user-memory"
7
+ version = "0.1.0"
8
+ description = "EverAlgo user memory: Episode / Foresight / AtomicFact / Profile extractors (re-exports boundary as Chat/WorkspaceMemCellExtractor)."
9
+ requires-python = ">=3.12"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ authors = [{ name = "EverMind" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.12",
17
+ ]
18
+ dependencies = [
19
+ "asgiref>=3.0",
20
+ "everalgo-boundary>=0.1.0,<2.0.0",
21
+ "everalgo-core>=0.1.0,<2.0.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/EverMind-AI/EverAlgo"
26
+ Repository = "https://github.com/EverMind-AI/EverAlgo"
27
+ Issues = "https://github.com/EverMind-AI/EverAlgo/issues"
28
+ Documentation = "https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-user-memory"
29
+ Changelog = "https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-user-memory/CHANGELOG.md"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/everalgo"]
@@ -0,0 +1,28 @@
1
+ """User-side memory extractors — 4 Extractors + boundary facade + boundary re-exports.
2
+
3
+ The user-scenario boundary facade (:class:`BoundaryDetector`) lives here; agent-trajectory
4
+ boundary detection will be added to ``everalgo.agent_memory`` in Stage 4.
5
+ """
6
+
7
+ import logging
8
+
9
+ from everalgo.boundary import DetectionResult
10
+ from everalgo.boundary.workspace import WorkspaceMemCellExtractor
11
+ from everalgo.user_memory.atomic_fact import AtomicFactExtractor
12
+ from everalgo.user_memory.boundary import BoundaryDetector
13
+ from everalgo.user_memory.episode import EpisodeExtractor
14
+ from everalgo.user_memory.foresight import ForesightExtractor
15
+ from everalgo.user_memory.profile import ProfileExtractor
16
+
17
+ __all__ = [
18
+ "AtomicFactExtractor",
19
+ "BoundaryDetector",
20
+ "DetectionResult",
21
+ "EpisodeExtractor",
22
+ "ForesightExtractor",
23
+ "ProfileExtractor",
24
+ "WorkspaceMemCellExtractor",
25
+ ]
26
+
27
+ # Library logging setup (ADR-013): NullHandler on each subpackage logger.
28
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,28 @@
1
+ """Shared rendering helpers for user-memory extractors.
2
+
3
+ This module is internal (``_render``-prefixed) — not part of the public API of ``everalgo.user_memory``.
4
+ ``render_content`` is re-exported from ``everalgo.types._render`` (moved to core in Stage 4 so that
5
+ ``everalgo-agent-memory`` can import it without creating a cross-package dependency on user-memory).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from everalgo.types._render import render_content
13
+ from everalgo.types.chat import ChatMessage
14
+
15
+ if TYPE_CHECKING:
16
+ from everalgo.types.conversation import MemCell
17
+
18
+ __all__ = ["chat_messages", "render_content"]
19
+
20
+
21
+ def chat_messages(memcell: MemCell) -> list[ChatMessage]:
22
+ """Filter ``memcell.items`` to ``ChatMessage`` entries only.
23
+
24
+ User-memory extractors silently skip ``ToolCallRequest`` / ``ToolCallResult`` — this is an
25
+ explicit contract supporting the agent → user-memory pipeline (see EverAlgo AGENTS.md). The
26
+ caller need not pre-filter; an AgentMemCell-shaped ``MemCell`` is acceptable input.
27
+ """
28
+ return [item for item in memcell.items if isinstance(item, ChatMessage)]