everalgo-agent-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_agent_memory-0.1.0/.gitignore +58 -0
- everalgo_agent_memory-0.1.0/CHANGELOG.md +26 -0
- everalgo_agent_memory-0.1.0/LICENSE +21 -0
- everalgo_agent_memory-0.1.0/PKG-INFO +153 -0
- everalgo_agent_memory-0.1.0/README.md +132 -0
- everalgo_agent_memory-0.1.0/pyproject.toml +32 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/__init__.py +16 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/_text.py +52 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/boundary.py +129 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/case.py +595 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/__init__.py +7 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/case_compress.py +76 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/case_filter.py +37 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_failure.py +87 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_maturity.py +52 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_success.py +106 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/tool_pre_compress.py +42 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/py.typed +0 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/skill.py +246 -0
- everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/skill_ops.py +482 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/__init__.py +0 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_boundary.py +221 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_case.py +1097 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_public_api.py +34 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_skill.py +1032 -0
- everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_text.py +118 -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,26 @@
|
|
|
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
|
+
- `AgentBoundaryDetector`: facade class that filters a `list[ConversationItem]` to only tool-call-carrying turns, calls `everalgo.boundary.detect_boundaries` on the result, then remaps indices back to the original sequence.
|
|
12
|
+
- `AgentCaseExtractor`: 11-step pipeline — heuristic skip check, tool-call-round counting, message trimming / pre-compression, `AgentCase` extraction via LLM. Accepts `llm=` at construction time.
|
|
13
|
+
- `AgentSkillExtractor`: add / update / retire operations with `cluster_id` binding; accepts `llm=` at construction time. `SkillConfig` pydantic model exposes maturity and retire thresholds.
|
|
14
|
+
- English prompts for case and skill extraction under `agent_memory/prompts/en/`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `AgentCaseExtractor` and `AgentSkillExtractor` rewired to the unified `MemCell` contract (replacing the previous `AgentMemCell` type).
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- Broad `except Exception → None` handler in `_evaluate_maturity`: the function now propagates exceptions from the LLM call.
|
|
23
|
+
- `asyncio.gather(return_exceptions=True)` in `_pre_compress_to_list`: individual compression errors now propagate instead of being swallowed.
|
|
24
|
+
- Fail-open path in `_is_worth_extracting`: the function raises on LLM error instead of returning `True` unconditionally.
|
|
25
|
+
|
|
26
|
+
[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,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: everalgo-agent-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EverAlgo agent memory: AgentCase / AgentSkill extractors and AgentBoundaryDetector facade.
|
|
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-agent-memory
|
|
9
|
+
Project-URL: Changelog, https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-agent-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: everalgo-boundary<2.0.0,>=0.1.0
|
|
18
|
+
Requires-Dist: everalgo-clustering<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-agent-memory
|
|
23
|
+
|
|
24
|
+
Agent-side memory products for EverAlgo — `AgentCaseExtractor` distils an agent trajectory `MemCell` into one `AgentCase`; `AgentSkillExtractor` maintains a cluster's reusable skill set from accumulated cases. `AgentBoundaryDetector` handles boundary detection over mixed `ConversationItem` trajectories (chat + tool calls).
|
|
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-agent-memory
|
|
32
|
+
# Auto-pulls: everalgo-core, everalgo-boundary, everalgo-clustering
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What this distribution provides
|
|
36
|
+
|
|
37
|
+
| Symbol | Role |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `AgentBoundaryDetector` | Boundary detection on agent trajectories (filter → detect → remap for mixed `ConversationItem` lists) |
|
|
40
|
+
| `AgentCaseExtractor` | Distils one agent-trajectory `MemCell` into `[] \| [AgentCase]` (11-step pipeline) |
|
|
41
|
+
| `AgentSkillExtractor` | Aggregates one new `AgentCase` into incremental skill operations for a cluster; returns add / update / retire entries |
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
import json
|
|
48
|
+
|
|
49
|
+
from everalgo.agent_memory.case import AgentCaseExtractor
|
|
50
|
+
from everalgo.llm.types import ChatResponse
|
|
51
|
+
from everalgo.testing.fake_llm import FakeLLMClient
|
|
52
|
+
from everalgo.types import AgentCase, ChatMessage, MemCell, ToolCall, ToolCallFunction, ToolCallRequest, ToolCallResult
|
|
53
|
+
|
|
54
|
+
_CASE_JSON = json.dumps({
|
|
55
|
+
"task_intent": "Search for Python async retry libraries",
|
|
56
|
+
"approach": "1. Search. 2. Filter. 3. Summarise.",
|
|
57
|
+
"quality_score": 0.82,
|
|
58
|
+
"key_insight": "Use tenacity AsyncRetrying for native async back-off.",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
async def main() -> None:
|
|
62
|
+
fake = FakeLLMClient(responses=[ChatResponse(content=_CASE_JSON, model="fake")])
|
|
63
|
+
mc = MemCell(
|
|
64
|
+
items=[
|
|
65
|
+
ChatMessage(id="u1", role="user", content="Best async retry libs?", timestamp=1_700_000_000_000, sender_id="user"),
|
|
66
|
+
ToolCallRequest(tool_calls=[ToolCall(id="c1", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_100, sender_id="assistant"),
|
|
67
|
+
ToolCallResult(tool_call_id="c1", content="Found: tenacity.", timestamp=1_700_000_000_200),
|
|
68
|
+
ToolCallRequest(tool_calls=[ToolCall(id="c2", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_300, sender_id="assistant"),
|
|
69
|
+
ToolCallResult(tool_call_id="c2", content="tenacity supports async.", timestamp=1_700_000_000_400),
|
|
70
|
+
ChatMessage(id="a1", role="assistant", content="Use tenacity AsyncRetrying.", timestamp=1_700_000_000_500, sender_id="assistant"),
|
|
71
|
+
],
|
|
72
|
+
timestamp=1_700_000_000_500,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
cases: list[AgentCase] = await AgentCaseExtractor(llm=fake).aextract(mc)
|
|
76
|
+
if cases:
|
|
77
|
+
print(cases[0].task_intent)
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
See [`examples/04_agent_memory_case.py`](../../examples/04_agent_memory_case.py) for the full runnable example.
|
|
83
|
+
|
|
84
|
+
## API surface
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class AgentBoundaryDetector:
|
|
88
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
89
|
+
async def adetect(
|
|
90
|
+
self, items: list[ConversationItem], *, is_final: bool = False, prompt: str | None = None
|
|
91
|
+
) -> DetectionResult: ...
|
|
92
|
+
|
|
93
|
+
class AgentCaseExtractor:
|
|
94
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
95
|
+
async def aextract(
|
|
96
|
+
self, memcell: MemCell, *,
|
|
97
|
+
prompt_filter: str | None = None,
|
|
98
|
+
prompt_compress: str | None = None,
|
|
99
|
+
prompt_tool_pre_compress: str | None = None,
|
|
100
|
+
) -> list[AgentCase]: ... # length 0 (filtered) or 1
|
|
101
|
+
|
|
102
|
+
class AgentSkillExtractor:
|
|
103
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
104
|
+
async def aextract(
|
|
105
|
+
self,
|
|
106
|
+
case: AgentCase,
|
|
107
|
+
*,
|
|
108
|
+
existing_relevant_skills: Sequence[AgentSkill],
|
|
109
|
+
supporting_cases: Sequence[AgentCase],
|
|
110
|
+
prompt_success: str | None = None,
|
|
111
|
+
prompt_failure: str | None = None,
|
|
112
|
+
prompt_maturity: str | None = None,
|
|
113
|
+
skip_quality_threshold: float = 0.2,
|
|
114
|
+
skip_maturity_scoring: bool = True,
|
|
115
|
+
maturity_threshold: float = 0.6,
|
|
116
|
+
retire_confidence: float = 0.1,
|
|
117
|
+
failure_quality_threshold: float = 0.5,
|
|
118
|
+
max_case_history: int = 9,
|
|
119
|
+
max_description_tokens: int = 400,
|
|
120
|
+
max_content_tokens: int = 5000,
|
|
121
|
+
maturity_trivial_change_ratio: float = 0.2,
|
|
122
|
+
maturity_reeval_change_ratio: float = 0.4,
|
|
123
|
+
) -> list[AgentSkill]: ...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)`.
|
|
127
|
+
|
|
128
|
+
### AgentCaseExtractor pipeline
|
|
129
|
+
|
|
130
|
+
The extractor runs an 11-step pipeline: strip-before-first-user → structural pre-filter → heuristic trim → over-size bail → LLM filter (skipped when ≥2 tool rounds) → tool pre-compress → LLM compress → parse → validate → build `AgentCase`. Returns `[]` when the trajectory is filtered out; returns `[AgentCase]` on success.
|
|
131
|
+
|
|
132
|
+
### AgentSkillExtractor return contract
|
|
133
|
+
|
|
134
|
+
Pass the new `AgentCase` and the pre-filtered `existing_relevant_skills` (e.g. top-K cosine from the caller's store) plus the associated `supporting_cases` (cases referenced by existing skills). The caller decodes add / update / retire by checking whether `skill.id` is already in `existing_relevant_skills` and whether `skill.confidence < retire_confidence`.
|
|
135
|
+
|
|
136
|
+
Cases with `quality_score < skip_quality_threshold` (default 0.2) short-circuit to `[]` without calling the LLM.
|
|
137
|
+
|
|
138
|
+
`AgentSkill.cluster_id` is always `""` on extraction — the caller stamps the cluster identity after persisting.
|
|
139
|
+
|
|
140
|
+
### Customising prompts
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import everalgo.agent_memory.prompts.case_filter as _cf
|
|
144
|
+
_cf.AGENT_CASE_FILTER_PROMPT = my_custom_filter_prompt # global override
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Or per-call: pass `prompt_filter=` / `prompt_compress=` / `prompt_tool_pre_compress=` to `aextract`.
|
|
148
|
+
|
|
149
|
+
## Related distributions
|
|
150
|
+
|
|
151
|
+
- [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `AgentBoundaryDetector`
|
|
152
|
+
- [`everalgo-clustering`](../everalgo-clustering/) — `cluster_by_llm` for skill-cluster assignment
|
|
153
|
+
- [`everalgo-rank`](../everalgo-rank/) — `CaseRanker` / `SkillRanker` for read-time ranking
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# everalgo-agent-memory
|
|
2
|
+
|
|
3
|
+
Agent-side memory products for EverAlgo — `AgentCaseExtractor` distils an agent trajectory `MemCell` into one `AgentCase`; `AgentSkillExtractor` maintains a cluster's reusable skill set from accumulated cases. `AgentBoundaryDetector` handles boundary detection over mixed `ConversationItem` trajectories (chat + tool calls).
|
|
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-agent-memory
|
|
11
|
+
# Auto-pulls: everalgo-core, everalgo-boundary, everalgo-clustering
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this distribution provides
|
|
15
|
+
|
|
16
|
+
| Symbol | Role |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `AgentBoundaryDetector` | Boundary detection on agent trajectories (filter → detect → remap for mixed `ConversationItem` lists) |
|
|
19
|
+
| `AgentCaseExtractor` | Distils one agent-trajectory `MemCell` into `[] \| [AgentCase]` (11-step pipeline) |
|
|
20
|
+
| `AgentSkillExtractor` | Aggregates one new `AgentCase` into incremental skill operations for a cluster; returns add / update / retire entries |
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
|
|
28
|
+
from everalgo.agent_memory.case import AgentCaseExtractor
|
|
29
|
+
from everalgo.llm.types import ChatResponse
|
|
30
|
+
from everalgo.testing.fake_llm import FakeLLMClient
|
|
31
|
+
from everalgo.types import AgentCase, ChatMessage, MemCell, ToolCall, ToolCallFunction, ToolCallRequest, ToolCallResult
|
|
32
|
+
|
|
33
|
+
_CASE_JSON = json.dumps({
|
|
34
|
+
"task_intent": "Search for Python async retry libraries",
|
|
35
|
+
"approach": "1. Search. 2. Filter. 3. Summarise.",
|
|
36
|
+
"quality_score": 0.82,
|
|
37
|
+
"key_insight": "Use tenacity AsyncRetrying for native async back-off.",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async def main() -> None:
|
|
41
|
+
fake = FakeLLMClient(responses=[ChatResponse(content=_CASE_JSON, model="fake")])
|
|
42
|
+
mc = MemCell(
|
|
43
|
+
items=[
|
|
44
|
+
ChatMessage(id="u1", role="user", content="Best async retry libs?", timestamp=1_700_000_000_000, sender_id="user"),
|
|
45
|
+
ToolCallRequest(tool_calls=[ToolCall(id="c1", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_100, sender_id="assistant"),
|
|
46
|
+
ToolCallResult(tool_call_id="c1", content="Found: tenacity.", timestamp=1_700_000_000_200),
|
|
47
|
+
ToolCallRequest(tool_calls=[ToolCall(id="c2", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_300, sender_id="assistant"),
|
|
48
|
+
ToolCallResult(tool_call_id="c2", content="tenacity supports async.", timestamp=1_700_000_000_400),
|
|
49
|
+
ChatMessage(id="a1", role="assistant", content="Use tenacity AsyncRetrying.", timestamp=1_700_000_000_500, sender_id="assistant"),
|
|
50
|
+
],
|
|
51
|
+
timestamp=1_700_000_000_500,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
cases: list[AgentCase] = await AgentCaseExtractor(llm=fake).aextract(mc)
|
|
55
|
+
if cases:
|
|
56
|
+
print(cases[0].task_intent)
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [`examples/04_agent_memory_case.py`](../../examples/04_agent_memory_case.py) for the full runnable example.
|
|
62
|
+
|
|
63
|
+
## API surface
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
class AgentBoundaryDetector:
|
|
67
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
68
|
+
async def adetect(
|
|
69
|
+
self, items: list[ConversationItem], *, is_final: bool = False, prompt: str | None = None
|
|
70
|
+
) -> DetectionResult: ...
|
|
71
|
+
|
|
72
|
+
class AgentCaseExtractor:
|
|
73
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
74
|
+
async def aextract(
|
|
75
|
+
self, memcell: MemCell, *,
|
|
76
|
+
prompt_filter: str | None = None,
|
|
77
|
+
prompt_compress: str | None = None,
|
|
78
|
+
prompt_tool_pre_compress: str | None = None,
|
|
79
|
+
) -> list[AgentCase]: ... # length 0 (filtered) or 1
|
|
80
|
+
|
|
81
|
+
class AgentSkillExtractor:
|
|
82
|
+
def __init__(self, *, llm: LLMClient) -> None: ...
|
|
83
|
+
async def aextract(
|
|
84
|
+
self,
|
|
85
|
+
case: AgentCase,
|
|
86
|
+
*,
|
|
87
|
+
existing_relevant_skills: Sequence[AgentSkill],
|
|
88
|
+
supporting_cases: Sequence[AgentCase],
|
|
89
|
+
prompt_success: str | None = None,
|
|
90
|
+
prompt_failure: str | None = None,
|
|
91
|
+
prompt_maturity: str | None = None,
|
|
92
|
+
skip_quality_threshold: float = 0.2,
|
|
93
|
+
skip_maturity_scoring: bool = True,
|
|
94
|
+
maturity_threshold: float = 0.6,
|
|
95
|
+
retire_confidence: float = 0.1,
|
|
96
|
+
failure_quality_threshold: float = 0.5,
|
|
97
|
+
max_case_history: int = 9,
|
|
98
|
+
max_description_tokens: int = 400,
|
|
99
|
+
max_content_tokens: int = 5000,
|
|
100
|
+
maturity_trivial_change_ratio: float = 0.2,
|
|
101
|
+
maturity_reeval_change_ratio: float = 0.4,
|
|
102
|
+
) -> list[AgentSkill]: ...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)`.
|
|
106
|
+
|
|
107
|
+
### AgentCaseExtractor pipeline
|
|
108
|
+
|
|
109
|
+
The extractor runs an 11-step pipeline: strip-before-first-user → structural pre-filter → heuristic trim → over-size bail → LLM filter (skipped when ≥2 tool rounds) → tool pre-compress → LLM compress → parse → validate → build `AgentCase`. Returns `[]` when the trajectory is filtered out; returns `[AgentCase]` on success.
|
|
110
|
+
|
|
111
|
+
### AgentSkillExtractor return contract
|
|
112
|
+
|
|
113
|
+
Pass the new `AgentCase` and the pre-filtered `existing_relevant_skills` (e.g. top-K cosine from the caller's store) plus the associated `supporting_cases` (cases referenced by existing skills). The caller decodes add / update / retire by checking whether `skill.id` is already in `existing_relevant_skills` and whether `skill.confidence < retire_confidence`.
|
|
114
|
+
|
|
115
|
+
Cases with `quality_score < skip_quality_threshold` (default 0.2) short-circuit to `[]` without calling the LLM.
|
|
116
|
+
|
|
117
|
+
`AgentSkill.cluster_id` is always `""` on extraction — the caller stamps the cluster identity after persisting.
|
|
118
|
+
|
|
119
|
+
### Customising prompts
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import everalgo.agent_memory.prompts.case_filter as _cf
|
|
123
|
+
_cf.AGENT_CASE_FILTER_PROMPT = my_custom_filter_prompt # global override
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or per-call: pass `prompt_filter=` / `prompt_compress=` / `prompt_tool_pre_compress=` to `aextract`.
|
|
127
|
+
|
|
128
|
+
## Related distributions
|
|
129
|
+
|
|
130
|
+
- [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `AgentBoundaryDetector`
|
|
131
|
+
- [`everalgo-clustering`](../everalgo-clustering/) — `cluster_by_llm` for skill-cluster assignment
|
|
132
|
+
- [`everalgo-rank`](../everalgo-rank/) — `CaseRanker` / `SkillRanker` for read-time ranking
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "everalgo-agent-memory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "EverAlgo agent memory: AgentCase / AgentSkill extractors and AgentBoundaryDetector facade."
|
|
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
|
+
"everalgo-core>=0.1.0,<2.0.0",
|
|
20
|
+
"everalgo-boundary>=0.1.0,<2.0.0",
|
|
21
|
+
"everalgo-clustering>=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-agent-memory"
|
|
29
|
+
Changelog = "https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-agent-memory/CHANGELOG.md"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/everalgo"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Agent-side memory extractors — 2 Extractors + boundary facade."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from everalgo.agent_memory.boundary import AgentBoundaryDetector
|
|
6
|
+
from everalgo.agent_memory.case import AgentCaseExtractor
|
|
7
|
+
from everalgo.agent_memory.skill import AgentSkillExtractor
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AgentBoundaryDetector",
|
|
11
|
+
"AgentCaseExtractor",
|
|
12
|
+
"AgentSkillExtractor",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Library logging setup (ADR-013): NullHandler on each subpackage logger.
|
|
16
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Token-aware text utilities for agent_memory extractors (head/tail truncation, json_default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from everalgo._tokenize import _get_tokenizer, count_tokens
|
|
9
|
+
|
|
10
|
+
__all__ = ["count_tokens", "json_default", "truncate_text"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def truncate_text(
|
|
14
|
+
text: str,
|
|
15
|
+
max_tokens: int,
|
|
16
|
+
*,
|
|
17
|
+
head_ratio: float = 0.7,
|
|
18
|
+
suffix: str | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
r"""Truncate ``text`` to ``max_tokens`` tokens, preserving head + tail with a marker.
|
|
21
|
+
|
|
22
|
+
When ``suffix`` is ``None``: keep ``head_ratio`` of tokens from the start and the remainder from
|
|
23
|
+
the end, joined by ``"\\n[... trimmed N tokens ...]\\n"`` (or ``"..."`` when ``head_ratio == 1.0``).
|
|
24
|
+
When ``suffix`` is set: keep the head only and append ``suffix``.
|
|
25
|
+
Short / empty inputs are returned unchanged.
|
|
26
|
+
"""
|
|
27
|
+
if not text:
|
|
28
|
+
return text
|
|
29
|
+
tokenizer = _get_tokenizer()
|
|
30
|
+
tokens = tokenizer.encode(text)
|
|
31
|
+
if len(tokens) <= max_tokens:
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
if suffix is not None:
|
|
35
|
+
head = tokenizer.decode(tokens[:max_tokens])
|
|
36
|
+
return head.rstrip() + suffix
|
|
37
|
+
|
|
38
|
+
head_count = int(max_tokens * head_ratio)
|
|
39
|
+
tail_count = max_tokens - head_count
|
|
40
|
+
head_text = tokenizer.decode(tokens[:head_count])
|
|
41
|
+
if tail_count <= 0:
|
|
42
|
+
return head_text.rstrip() + "..."
|
|
43
|
+
tail_text = tokenizer.decode(tokens[-tail_count:])
|
|
44
|
+
trimmed = len(tokens) - max_tokens
|
|
45
|
+
return f"{head_text}\n[... trimmed {trimmed} tokens ...]\n{tail_text}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def json_default(obj: Any) -> Any:
|
|
49
|
+
"""``json.dumps(default=...)`` fallback: datetime → ISO string, anything else → ``str``."""
|
|
50
|
+
if isinstance(obj, datetime):
|
|
51
|
+
return obj.isoformat()
|
|
52
|
+
return str(obj)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Agent trajectory boundary detection facade.
|
|
2
|
+
|
|
3
|
+
Wraps the low-level ``everalgo.boundary.detect_boundaries`` primitive with a filter → detect → remap
|
|
4
|
+
pipeline to handle mixed trajectories (``ChatMessage`` + ``ToolCallRequest`` + ``ToolCallResult``).
|
|
5
|
+
|
|
6
|
+
Tool items are filtered out before the LLM detection step so the LLM sees a clean conversation
|
|
7
|
+
view; boundary indices from the chat-only result are then remapped back to the original
|
|
8
|
+
``ConversationItem`` index space so that the output ``MemCell.items`` preserves the full
|
|
9
|
+
trajectory (including tool calls).
|
|
10
|
+
|
|
11
|
+
Uses explicit composition rather than inheritance: filter → detect → remap layers are separate
|
|
12
|
+
functions rather than an extended class hierarchy.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from asgiref.sync import async_to_sync
|
|
21
|
+
|
|
22
|
+
from everalgo.boundary import DetectionResult, detect_boundaries
|
|
23
|
+
from everalgo.types import ChatMessage, MemCell
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from everalgo.llm.protocols import LLMClient
|
|
27
|
+
from everalgo.types import ConversationItem
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
__all__ = ["AgentBoundaryDetector"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentBoundaryDetector:
|
|
35
|
+
"""Boundary detection on an agent trajectory (mixed ``ConversationItem`` list).
|
|
36
|
+
|
|
37
|
+
The filter → detect → remap pipeline:
|
|
38
|
+
|
|
39
|
+
1. **Filter** — extract only ``ChatMessage`` items and remember their original indices.
|
|
40
|
+
2. **Detect** — run ``everalgo.boundary.detect_boundaries`` on the chat-only view.
|
|
41
|
+
3. **Remap** — translate chat-space cell boundaries back to the original ``ConversationItem``
|
|
42
|
+
index space; ``ToolCallRequest`` / ``ToolCallResult`` items between adjacent chat
|
|
43
|
+
messages are folded into the cell that contains the preceding chat message.
|
|
44
|
+
|
|
45
|
+
Invariant: ``sum(len(c.items) for c in result.cells) + len(result.tail) == len(items)``.
|
|
46
|
+
|
|
47
|
+
The LLM client is bound to the instance at construction time.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, llm: LLMClient) -> None:
|
|
51
|
+
self._llm = llm
|
|
52
|
+
|
|
53
|
+
async def adetect(
|
|
54
|
+
self,
|
|
55
|
+
items: list[ConversationItem],
|
|
56
|
+
*,
|
|
57
|
+
is_final: bool = False,
|
|
58
|
+
prompt: str | None = None,
|
|
59
|
+
) -> DetectionResult:
|
|
60
|
+
"""Detect boundaries on a mixed agent trajectory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
items: Ordered ``ConversationItem`` list for one trajectory slice — may contain
|
|
64
|
+
``ChatMessage``, ``ToolCallRequest``, and ``ToolCallResult`` in any mix.
|
|
65
|
+
is_final: When ``True``, the caller guarantees no more items will arrive; trailing tool items
|
|
66
|
+
after the last cell's final chat message are folded into that cell rather than the tail.
|
|
67
|
+
When ``False`` (default), trailing tool items go to the tail.
|
|
68
|
+
prompt: Per-call boundary detection prompt override; ``None`` uses the built-in default.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
``cells`` — remapped ``MemCell`` list preserving tool calls inside each cell.
|
|
72
|
+
``tail`` — remainder items (not yet committed to a cell) for the non-final case.
|
|
73
|
+
"""
|
|
74
|
+
chat_items: list[ChatMessage] = []
|
|
75
|
+
chat_to_orig: list[int] = []
|
|
76
|
+
for orig_idx, item in enumerate(items):
|
|
77
|
+
if isinstance(item, ChatMessage):
|
|
78
|
+
chat_items.append(item)
|
|
79
|
+
chat_to_orig.append(orig_idx)
|
|
80
|
+
|
|
81
|
+
if not chat_items:
|
|
82
|
+
# No chat messages → no LLM signal; everything goes to tail per should_wait semantics.
|
|
83
|
+
logger.debug("adetect: no ChatMessage items in trajectory (n_items=%d)", len(items))
|
|
84
|
+
return DetectionResult(cells=[], tail=list(items)) # type: ignore[arg-type]
|
|
85
|
+
|
|
86
|
+
chat_result = await detect_boundaries(chat_items, llm=self._llm, is_final=is_final, prompt=prompt)
|
|
87
|
+
|
|
88
|
+
remapped_cells: list[MemCell] = []
|
|
89
|
+
chat_cursor = 0
|
|
90
|
+
for cell in chat_result.cells:
|
|
91
|
+
cell_chat_count = len(cell.items)
|
|
92
|
+
start_chat_idx = chat_cursor
|
|
93
|
+
end_chat_idx = chat_cursor + cell_chat_count - 1
|
|
94
|
+
chat_cursor += cell_chat_count
|
|
95
|
+
|
|
96
|
+
start_orig = chat_to_orig[start_chat_idx]
|
|
97
|
+
|
|
98
|
+
if chat_cursor < len(chat_to_orig):
|
|
99
|
+
# More cells follow: include everything up to (exclusive) the next cell's first chat.
|
|
100
|
+
end_orig_exclusive = chat_to_orig[chat_cursor]
|
|
101
|
+
elif is_final:
|
|
102
|
+
# Last cell, caller says stream is done: absorb all remaining items.
|
|
103
|
+
end_orig_exclusive = len(items)
|
|
104
|
+
else:
|
|
105
|
+
# Last cell, stream is ongoing: include only up to (inclusive) the last chat in
|
|
106
|
+
# this cell; trailing tool items go to the tail so they can be re-processed.
|
|
107
|
+
end_orig_exclusive = chat_to_orig[end_chat_idx] + 1
|
|
108
|
+
|
|
109
|
+
remapped_cells.append(MemCell(items=list(items[start_orig:end_orig_exclusive]), timestamp=cell.timestamp))
|
|
110
|
+
|
|
111
|
+
# Tail: items after the last consumed original index.
|
|
112
|
+
if remapped_cells:
|
|
113
|
+
if is_final:
|
|
114
|
+
last_consumed_exclusive = len(items)
|
|
115
|
+
elif chat_cursor < len(chat_to_orig):
|
|
116
|
+
last_consumed_exclusive = chat_to_orig[chat_cursor]
|
|
117
|
+
else:
|
|
118
|
+
# chat_cursor == len(chat_to_orig): all chats consumed; last cell boundary already
|
|
119
|
+
# set to chat_to_orig[end_chat_idx] + 1, so tail starts right after.
|
|
120
|
+
last_consumed_exclusive = chat_to_orig[-1] + 1
|
|
121
|
+
tail = list(items[last_consumed_exclusive:])
|
|
122
|
+
else:
|
|
123
|
+
# detect_boundaries returned no cells — everything is tail.
|
|
124
|
+
tail = list(items)
|
|
125
|
+
|
|
126
|
+
return DetectionResult(cells=remapped_cells, tail=tail) # type: ignore[arg-type]
|
|
127
|
+
|
|
128
|
+
detect = async_to_sync(adetect)
|
|
129
|
+
"""Sync bridge — only callable from non-event-loop contexts."""
|