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.
- everalgo_user_memory-0.1.0/.gitignore +58 -0
- everalgo_user_memory-0.1.0/CHANGELOG.md +27 -0
- everalgo_user_memory-0.1.0/LICENSE +21 -0
- everalgo_user_memory-0.1.0/PKG-INFO +173 -0
- everalgo_user_memory-0.1.0/README.md +152 -0
- everalgo_user_memory-0.1.0/pyproject.toml +32 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/__init__.py +28 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/_render.py +28 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/atomic_fact.py +158 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/boundary.py +57 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/episode.py +159 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/foresight.py +227 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/profile.py +326 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/__init__.py +5 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/__init__.py +1 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/atomic_fact.py +88 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/episode.py +189 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/foresight.py +156 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/en/profile.py +217 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/__init__.py +1 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/atomic_fact.py +85 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/episode.py +189 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/foresight.py +148 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/prompts/zh/profile.py +196 -0
- everalgo_user_memory-0.1.0/src/everalgo/user_memory/py.typed +0 -0
- everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_atomic_fact.py +299 -0
- everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_episode.py +462 -0
- everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_foresight.py +378 -0
- everalgo_user_memory-0.1.0/tests/user_memory/test_user_memory_profile.py +530 -0
- 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)]
|