engrava 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engrava/__init__.py +213 -0
- engrava/benchmarks/__init__.py +6 -0
- engrava/benchmarks/longmemeval/README.md +179 -0
- engrava/benchmarks/longmemeval/__init__.py +30 -0
- engrava/benchmarks/longmemeval/__main__.py +10 -0
- engrava/benchmarks/longmemeval/cli.py +178 -0
- engrava/benchmarks/longmemeval/dataset_loader.py +282 -0
- engrava/benchmarks/longmemeval/evaluate.py +240 -0
- engrava/benchmarks/longmemeval/runner.py +512 -0
- engrava/benchmarks/synthetic/README.md +38 -0
- engrava/benchmarks/synthetic/__init__.py +19 -0
- engrava/benchmarks/synthetic/__main__.py +10 -0
- engrava/benchmarks/synthetic/datasets/__init__.py +7 -0
- engrava/benchmarks/synthetic/datasets/synthetic-v1.json +17021 -0
- engrava/benchmarks/synthetic/evaluate.py +759 -0
- engrava/benchmarks/synthetic/generate.py +686 -0
- engrava/benchmarks/synthetic/runner.py +805 -0
- engrava/benchmarks/synthetic/scenarios.py +718 -0
- engrava/cli/__init__.py +1 -0
- engrava/cli/config.py +68 -0
- engrava/cli/main.py +1078 -0
- engrava/config.py +1973 -0
- engrava/domain/__init__.py +1 -0
- engrava/domain/enums.py +287 -0
- engrava/domain/exceptions.py +192 -0
- engrava/domain/manifest.py +43 -0
- engrava/domain/models/__init__.py +31 -0
- engrava/domain/models/action.py +91 -0
- engrava/domain/models/edge.py +56 -0
- engrava/domain/models/embedding.py +81 -0
- engrava/domain/models/journal.py +82 -0
- engrava/domain/models/metrics.py +63 -0
- engrava/domain/models/mutation_type.py +28 -0
- engrava/domain/models/search.py +42 -0
- engrava/domain/models/thought.py +254 -0
- engrava/domain/models/ttl.py +51 -0
- engrava/domain/protocols/__init__.py +17 -0
- engrava/domain/protocols/embedding_provider.py +71 -0
- engrava/domain/protocols/engrava_core.py +366 -0
- engrava/domain/protocols/hooks.py +205 -0
- engrava/embeddings/__init__.py +4 -0
- engrava/embeddings/callback.py +93 -0
- engrava/embeddings/huggingface.py +158 -0
- engrava/embeddings/ollama.py +166 -0
- engrava/embeddings/openai_compatible.py +183 -0
- engrava/embeddings/sentence_transformer.py +150 -0
- engrava/extensions/__init__.py +6 -0
- engrava/extensions/discovery.py +80 -0
- engrava/extensions/dreaming.py +1535 -0
- engrava/extensions/dreaming_cluster_quality.py +512 -0
- engrava/extensions/dreaming_keyphrases.py +664 -0
- engrava/extensions/dreaming_reflection_content.py +455 -0
- engrava/extensions/dreaming_signals.py +312 -0
- engrava/extensions/vector_sqlite_vec.py +223 -0
- engrava/infrastructure/__init__.py +1 -0
- engrava/infrastructure/read_only_store.py +268 -0
- engrava/infrastructure/service_manager.py +325 -0
- engrava/infrastructure/sqlite/__init__.py +7 -0
- engrava/infrastructure/sqlite/centroid.py +58 -0
- engrava/infrastructure/sqlite/engrava_core.py +4008 -0
- engrava/infrastructure/sqlite/extension_migrations.py +235 -0
- engrava/infrastructure/sqlite/journal_writer.py +308 -0
- engrava/infrastructure/sqlite/schema_core.sql +185 -0
- engrava/metadata.py +135 -0
- engrava/mindql/__init__.py +1 -0
- engrava/mindql/executor.py +287 -0
- engrava/mindql/parser.py +293 -0
- engrava/py.typed +0 -0
- engrava-0.3.0.dist-info/METADATA +287 -0
- engrava-0.3.0.dist-info/RECORD +74 -0
- engrava-0.3.0.dist-info/WHEEL +5 -0
- engrava-0.3.0.dist-info/entry_points.txt +2 -0
- engrava-0.3.0.dist-info/licenses/LICENSE +21 -0
- engrava-0.3.0.dist-info/top_level.txt +1 -0
engrava/__init__.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""engrava — standalone thought-graph persistence engine."""
|
|
2
|
+
|
|
3
|
+
from engrava.config import (
|
|
4
|
+
ConfigError,
|
|
5
|
+
DreamingConfig,
|
|
6
|
+
DreamingGates,
|
|
7
|
+
EmbeddingConfig,
|
|
8
|
+
EngravaConfig,
|
|
9
|
+
JournalConfig,
|
|
10
|
+
MetricsConfig,
|
|
11
|
+
SearchConfig,
|
|
12
|
+
ServiceConfig,
|
|
13
|
+
ServicesConfig,
|
|
14
|
+
TTLConfig,
|
|
15
|
+
load_config,
|
|
16
|
+
resolve_embedding_provider,
|
|
17
|
+
resolve_hooks,
|
|
18
|
+
resolve_manifests,
|
|
19
|
+
)
|
|
20
|
+
from engrava.domain.enums import (
|
|
21
|
+
ActionStatus,
|
|
22
|
+
ActionType,
|
|
23
|
+
EdgeType,
|
|
24
|
+
KnowledgeSource,
|
|
25
|
+
LifecycleStatus,
|
|
26
|
+
Priority,
|
|
27
|
+
ThoughtType,
|
|
28
|
+
ThoughtVisibility,
|
|
29
|
+
VerificationStatus,
|
|
30
|
+
)
|
|
31
|
+
from engrava.domain.exceptions import (
|
|
32
|
+
EmbeddingModelMismatchError,
|
|
33
|
+
EngravaError,
|
|
34
|
+
ExtensionMigrationError,
|
|
35
|
+
InvalidTransitionError,
|
|
36
|
+
ReadOnlyViolationError,
|
|
37
|
+
StaleDataError,
|
|
38
|
+
ThoughtNotFoundError,
|
|
39
|
+
)
|
|
40
|
+
from engrava.domain.manifest import ExtensionManifest
|
|
41
|
+
from engrava.domain.models.action import ActionRecord
|
|
42
|
+
from engrava.domain.models.edge import EdgeRecord
|
|
43
|
+
from engrava.domain.models.embedding import EmbeddingRecord
|
|
44
|
+
from engrava.domain.models.journal import JournalEntry, JournalIntegrityResult
|
|
45
|
+
from engrava.domain.models.metrics import (
|
|
46
|
+
EdgeCounts,
|
|
47
|
+
EngravaMetrics,
|
|
48
|
+
LatencyHistogram,
|
|
49
|
+
StorageFootprint,
|
|
50
|
+
ThoughtCounts,
|
|
51
|
+
)
|
|
52
|
+
from engrava.domain.models.mutation_type import MutationType
|
|
53
|
+
from engrava.domain.models.search import HybridSearchResult
|
|
54
|
+
from engrava.domain.models.thought import ThoughtRecord
|
|
55
|
+
from engrava.domain.models.thought import ThoughtRecord as CoreThoughtRecord
|
|
56
|
+
from engrava.domain.models.ttl import CleanupResult, CleanupStrategy
|
|
57
|
+
from engrava.domain.protocols.embedding_provider import EmbeddingProviderProtocol
|
|
58
|
+
from engrava.domain.protocols.engrava_core import EngravaCoreProtocol
|
|
59
|
+
from engrava.domain.protocols.hooks import (
|
|
60
|
+
DefaultEngravaHooks,
|
|
61
|
+
EngravaHooksProtocol,
|
|
62
|
+
MindQLExtension,
|
|
63
|
+
ScoringContext,
|
|
64
|
+
)
|
|
65
|
+
from engrava.embeddings.callback import CallbackProvider
|
|
66
|
+
from engrava.embeddings.huggingface import HuggingFaceProvider
|
|
67
|
+
from engrava.embeddings.ollama import OllamaProvider
|
|
68
|
+
from engrava.embeddings.openai_compatible import OpenAICompatibleProvider
|
|
69
|
+
from engrava.embeddings.sentence_transformer import SentenceTransformerProvider
|
|
70
|
+
from engrava.extensions.discovery import discover_manifests
|
|
71
|
+
from engrava.extensions.dreaming import ConsolidationResult, DreamingExtension
|
|
72
|
+
from engrava.extensions.dreaming_signals import (
|
|
73
|
+
ConfidenceSignal,
|
|
74
|
+
ConfirmationSignal,
|
|
75
|
+
DreamingContext,
|
|
76
|
+
DreamingSignalProtocol,
|
|
77
|
+
FrequencySignal,
|
|
78
|
+
RecencySignal,
|
|
79
|
+
StalenessSignal,
|
|
80
|
+
)
|
|
81
|
+
from engrava.extensions.vector_sqlite_vec import SqliteVecSearchBackend
|
|
82
|
+
from engrava.infrastructure.read_only_store import ReadOnlyEngrava
|
|
83
|
+
from engrava.infrastructure.service_manager import EngravaManager
|
|
84
|
+
from engrava.infrastructure.sqlite.engrava_core import SqliteEngravaCore
|
|
85
|
+
from engrava.infrastructure.sqlite.journal_writer import JournalWriter
|
|
86
|
+
from engrava.metadata import percept, thought, utterance
|
|
87
|
+
from engrava.mindql.executor import MindQLExecutor, MindQLResult
|
|
88
|
+
from engrava.mindql.parser import MindQLCommand, MindQLParseError, MindQLQuery, parse
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"ActionRecord",
|
|
92
|
+
"ActionStatus",
|
|
93
|
+
"ActionType",
|
|
94
|
+
"CallbackProvider",
|
|
95
|
+
"CleanupResult",
|
|
96
|
+
"CleanupStrategy",
|
|
97
|
+
"ConfidenceSignal",
|
|
98
|
+
"ConfigError",
|
|
99
|
+
"ConfirmationSignal",
|
|
100
|
+
"ConsolidationResult",
|
|
101
|
+
"CoreThoughtRecord",
|
|
102
|
+
"DefaultEngravaHooks",
|
|
103
|
+
"DefaultMindStoreHooks",
|
|
104
|
+
"DreamingConfig",
|
|
105
|
+
"DreamingContext",
|
|
106
|
+
"DreamingExtension",
|
|
107
|
+
"DreamingGates",
|
|
108
|
+
"DreamingSignalProtocol",
|
|
109
|
+
"EdgeCounts",
|
|
110
|
+
"EdgeRecord",
|
|
111
|
+
"EdgeType",
|
|
112
|
+
"EmbeddingConfig",
|
|
113
|
+
"EmbeddingModelMismatchError",
|
|
114
|
+
"EmbeddingProviderProtocol",
|
|
115
|
+
"EmbeddingRecord",
|
|
116
|
+
"EngravaConfig",
|
|
117
|
+
"EngravaCoreProtocol",
|
|
118
|
+
"EngravaError",
|
|
119
|
+
"EngravaHooksProtocol",
|
|
120
|
+
"EngravaManager",
|
|
121
|
+
"EngravaMetrics",
|
|
122
|
+
"ExtensionManifest",
|
|
123
|
+
"ExtensionMigrationError",
|
|
124
|
+
"FrequencySignal",
|
|
125
|
+
"HuggingFaceProvider",
|
|
126
|
+
"HybridSearchResult",
|
|
127
|
+
"InvalidTransitionError",
|
|
128
|
+
"JournalConfig",
|
|
129
|
+
"JournalEntry",
|
|
130
|
+
"JournalIntegrityResult",
|
|
131
|
+
"JournalWriter",
|
|
132
|
+
"KnowledgeSource",
|
|
133
|
+
"LatencyHistogram",
|
|
134
|
+
"LifecycleStatus",
|
|
135
|
+
"MetricsConfig",
|
|
136
|
+
"MindQLCommand",
|
|
137
|
+
"MindQLExecutor",
|
|
138
|
+
"MindQLExtension",
|
|
139
|
+
"MindQLParseError",
|
|
140
|
+
"MindQLQuery",
|
|
141
|
+
"MindQLResult",
|
|
142
|
+
"MindStoreConfig",
|
|
143
|
+
"MindStoreCoreProtocol",
|
|
144
|
+
"MindStoreError",
|
|
145
|
+
"MindStoreHooksProtocol",
|
|
146
|
+
"MindStoreManager",
|
|
147
|
+
"MutationType",
|
|
148
|
+
"OllamaProvider",
|
|
149
|
+
"OpenAICompatibleProvider",
|
|
150
|
+
"Priority",
|
|
151
|
+
"ReadOnlyEngrava",
|
|
152
|
+
"ReadOnlyMindStore",
|
|
153
|
+
"ReadOnlyViolationError",
|
|
154
|
+
"RecencySignal",
|
|
155
|
+
"ScoringContext",
|
|
156
|
+
"SearchConfig",
|
|
157
|
+
"SentenceTransformerProvider",
|
|
158
|
+
"ServiceConfig",
|
|
159
|
+
"ServicesConfig",
|
|
160
|
+
"SqliteEngravaCore",
|
|
161
|
+
"SqliteMindStoreCore",
|
|
162
|
+
"SqliteVecSearchBackend",
|
|
163
|
+
"StaleDataError",
|
|
164
|
+
"StalenessSignal",
|
|
165
|
+
"StorageFootprint",
|
|
166
|
+
"TTLConfig",
|
|
167
|
+
"ThoughtCounts",
|
|
168
|
+
"ThoughtNotFoundError",
|
|
169
|
+
"ThoughtRecord",
|
|
170
|
+
"ThoughtType",
|
|
171
|
+
"ThoughtVisibility",
|
|
172
|
+
"VerificationStatus",
|
|
173
|
+
"discover_manifests",
|
|
174
|
+
"load_config",
|
|
175
|
+
"parse",
|
|
176
|
+
"percept",
|
|
177
|
+
"resolve_embedding_provider",
|
|
178
|
+
"resolve_hooks",
|
|
179
|
+
"resolve_manifests",
|
|
180
|
+
"thought",
|
|
181
|
+
"utterance",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Backward-compatibility aliases — deprecated, remove in v0.4
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
import warnings as _warnings
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def __getattr__(name: str) -> object:
|
|
192
|
+
"""Lazy deprecation aliases for renamed symbols."""
|
|
193
|
+
_aliases: dict[str, object] = {
|
|
194
|
+
"SqliteMindStoreCore": SqliteEngravaCore,
|
|
195
|
+
"MindStoreManager": EngravaManager,
|
|
196
|
+
"MindStoreConfig": EngravaConfig,
|
|
197
|
+
"MindStoreError": EngravaError,
|
|
198
|
+
"MindStoreCoreProtocol": EngravaCoreProtocol,
|
|
199
|
+
"MindStoreHooksProtocol": EngravaHooksProtocol,
|
|
200
|
+
"DefaultMindStoreHooks": DefaultEngravaHooks,
|
|
201
|
+
"ReadOnlyMindStore": ReadOnlyEngrava,
|
|
202
|
+
}
|
|
203
|
+
if name in _aliases:
|
|
204
|
+
target = _aliases[name]
|
|
205
|
+
target_name = getattr(target, "__name__", str(target))
|
|
206
|
+
_warnings.warn(
|
|
207
|
+
f"{name} is deprecated, use {target_name} instead",
|
|
208
|
+
DeprecationWarning,
|
|
209
|
+
stacklevel=2,
|
|
210
|
+
)
|
|
211
|
+
return target
|
|
212
|
+
msg = f"module 'engrava' has no attribute {name!r}"
|
|
213
|
+
raise AttributeError(msg)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# LongMemEval — Free harness
|
|
2
|
+
|
|
3
|
+
A reproducible engrava-side runner for the public **LongMemEval**
|
|
4
|
+
memory-evaluation benchmark. Use it to measure end-to-end retrieval
|
|
5
|
+
quality against the published dataset on any laptop, without API keys
|
|
6
|
+
in the default configuration.
|
|
7
|
+
|
|
8
|
+
## Attribution
|
|
9
|
+
|
|
10
|
+
LongMemEval was introduced by:
|
|
11
|
+
|
|
12
|
+
> Di Wu, Hongwei Wang, Wenhao Yu, Yuwei Zhang, Kai-Wei Chang, Dong Yu.
|
|
13
|
+
> *LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive
|
|
14
|
+
> Memory.* ICLR 2025. arXiv:2410.10813.
|
|
15
|
+
|
|
16
|
+
The original dataset, evaluation protocol, and corpus are the work of
|
|
17
|
+
the LongMemEval authors. This package only wraps the dataset for
|
|
18
|
+
engrava-side ingestion + scoring.
|
|
19
|
+
|
|
20
|
+
## License + redistribution
|
|
21
|
+
|
|
22
|
+
- The upstream LongMemEval code repository is **MIT-licensed**
|
|
23
|
+
(`github.com/xiaowu0162/LongMemEval`).
|
|
24
|
+
- The dataset is distributed via the upstream HuggingFace mirror under
|
|
25
|
+
a permissive license.
|
|
26
|
+
- This package **does NOT bundle the dataset.** Engrava ships only the
|
|
27
|
+
loader, runner, evaluator, and a small hand-authored test fixture
|
|
28
|
+
(in `tests/benchmarks/fixtures/`) that mirrors the upstream schema
|
|
29
|
+
but contains no upstream content.
|
|
30
|
+
- The loader downloads the requested variant at runtime to
|
|
31
|
+
`~/.engrava/benchmarks/longmemeval/`. Cached files are reused on
|
|
32
|
+
subsequent invocations.
|
|
33
|
+
|
|
34
|
+
If you need to operate offline, manually place the upstream JSON files
|
|
35
|
+
in that cache directory; the loader will skip the download.
|
|
36
|
+
|
|
37
|
+
## Download instructions
|
|
38
|
+
|
|
39
|
+
The loader handles this automatically on first invocation. If you
|
|
40
|
+
prefer to fetch the files manually:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
mkdir -p ~/.engrava/benchmarks/longmemeval/
|
|
44
|
+
cd ~/.engrava/benchmarks/longmemeval/
|
|
45
|
+
|
|
46
|
+
# Oracle variant (smallest — recommended for smoke runs):
|
|
47
|
+
curl -L -o longmemeval_oracle.json \
|
|
48
|
+
https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_oracle.json
|
|
49
|
+
|
|
50
|
+
# S variant:
|
|
51
|
+
curl -L -o longmemeval_s_cleaned.json \
|
|
52
|
+
https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json
|
|
53
|
+
|
|
54
|
+
# M variant (largest):
|
|
55
|
+
curl -L -o longmemeval_m_cleaned.json \
|
|
56
|
+
https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_m_cleaned.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Running the harness
|
|
60
|
+
|
|
61
|
+
The harness ships with engrava's embeddings extras. Install with:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install 'engrava[embeddings-local]'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Default invocation (oracle variant, substring mode, dreaming OFF):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m engrava.benchmarks.longmemeval
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Cosine-mode against the oracle variant with dreaming ON:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m engrava.benchmarks.longmemeval --eval-mode=cosine --dreaming
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Subset filter (one question type only):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m engrava.benchmarks.longmemeval --subset=single-session-recall
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Evaluation modes
|
|
86
|
+
|
|
87
|
+
| Mode | Deterministic | LLM required | When to use |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| `substring` (default) | yes | no | Reference for the Free-side quality gate. |
|
|
90
|
+
| `cosine` | yes (given fixed provider) | no | Catches paraphrased matches the substring check misses. |
|
|
91
|
+
| `llm` | depends on judge | yes (user-supplied) | Closest to the canonical LongMemEval scoring; opt-in. |
|
|
92
|
+
|
|
93
|
+
The CLI exposes `substring` and `cosine`. The `llm` mode is
|
|
94
|
+
deliberately **not** wired through the CLI — argparse rejects
|
|
95
|
+
`--eval-mode=llm`. To use the LLM judge, drive `run_longmemeval`
|
|
96
|
+
directly from Python and pass an `llm_judge` argument that implements
|
|
97
|
+
the `engrava.benchmarks.longmemeval.evaluate.LLMJudgeClient` protocol.
|
|
98
|
+
SDK wiring varies per provider and engrava does not pin one — see the
|
|
99
|
+
snippet below.
|
|
100
|
+
|
|
101
|
+
### Storage semantics
|
|
102
|
+
|
|
103
|
+
By default every question runs in its own `:memory:` SQLite database
|
|
104
|
+
(LongMemEval forbids cross-question state). Passing `db_path=<dir>` to
|
|
105
|
+
`run_longmemeval` switches to on-disk storage: the runner creates one
|
|
106
|
+
DB file per `question_id` inside that directory, so on-disk runs also
|
|
107
|
+
keep the haystacks fully isolated.
|
|
108
|
+
|
|
109
|
+
## Programmatic usage
|
|
110
|
+
|
|
111
|
+
Default (substring mode, in-memory store per question):
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
|
|
116
|
+
from engrava.benchmarks.longmemeval import load_dataset
|
|
117
|
+
from engrava.benchmarks.longmemeval.runner import run_longmemeval
|
|
118
|
+
from engrava.benchmarks.synthetic.evaluate import (
|
|
119
|
+
resolve_embedding_provider_or_exit,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def main() -> None:
|
|
124
|
+
questions = load_dataset(variant="oracle")
|
|
125
|
+
provider = resolve_embedding_provider_or_exit()
|
|
126
|
+
result = await run_longmemeval(
|
|
127
|
+
questions,
|
|
128
|
+
dreaming_enabled=False,
|
|
129
|
+
embedding_provider=provider,
|
|
130
|
+
eval_mode="substring",
|
|
131
|
+
)
|
|
132
|
+
print(f"aggregate score: {result.aggregate_score:.4f}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
asyncio.run(main())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
LLM-judge mode (caller supplies any client matching `LLMJudgeClient`):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import asyncio
|
|
142
|
+
|
|
143
|
+
from engrava.benchmarks.longmemeval import load_dataset
|
|
144
|
+
from engrava.benchmarks.longmemeval.runner import run_longmemeval
|
|
145
|
+
from engrava.benchmarks.synthetic.evaluate import (
|
|
146
|
+
resolve_embedding_provider_or_exit,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MyJudge:
|
|
151
|
+
"""Thin wrapper around any LLM SDK (Anthropic, OpenAI, local)."""
|
|
152
|
+
|
|
153
|
+
async def judge(self, *, system: str, user: str) -> str:
|
|
154
|
+
# Call your SDK here; return the model's reply.
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def main() -> None:
|
|
159
|
+
questions = load_dataset(variant="oracle")
|
|
160
|
+
provider = resolve_embedding_provider_or_exit()
|
|
161
|
+
result = await run_longmemeval(
|
|
162
|
+
questions,
|
|
163
|
+
dreaming_enabled=False,
|
|
164
|
+
embedding_provider=provider,
|
|
165
|
+
eval_mode="llm",
|
|
166
|
+
llm_judge=MyJudge(),
|
|
167
|
+
)
|
|
168
|
+
print(f"aggregate score: {result.aggregate_score:.4f}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
asyncio.run(main())
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Determinism
|
|
175
|
+
|
|
176
|
+
Substring mode is byte-deterministic across runs given the same dataset
|
|
177
|
+
and the same engrava configuration. Cosine mode is deterministic given
|
|
178
|
+
a fixed embedding provider. LLM mode is non-deterministic by design
|
|
179
|
+
(provider sampling), so its results carry an informational role.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""LongMemEval Free harness — public benchmark runner for engrava.
|
|
2
|
+
|
|
3
|
+
Uses the publicly distributed LongMemEval dataset (Wu et al., ICLR 2025,
|
|
4
|
+
arXiv:2410.10813). The dataset is **not bundled** with engrava; users
|
|
5
|
+
download it from the upstream HuggingFace distribution at runtime via
|
|
6
|
+
``load_dataset``.
|
|
7
|
+
|
|
8
|
+
The CLI exposes two deterministic evaluation modes (``substring`` and
|
|
9
|
+
``cosine``); both run without LLM API keys. LLM-judge mode is opt-in
|
|
10
|
+
and reachable only by driving ``run_longmemeval`` from Python with a
|
|
11
|
+
caller-supplied ``LLMJudgeClient`` — see
|
|
12
|
+
``benchmarks/longmemeval/README.md`` for a code snippet, attribution,
|
|
13
|
+
license, and download instructions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from engrava.benchmarks.longmemeval.dataset_loader import (
|
|
19
|
+
LongMemEvalQuestion,
|
|
20
|
+
LongMemEvalSession,
|
|
21
|
+
LongMemEvalTurn,
|
|
22
|
+
load_dataset,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"LongMemEvalQuestion",
|
|
27
|
+
"LongMemEvalSession",
|
|
28
|
+
"LongMemEvalTurn",
|
|
29
|
+
"load_dataset",
|
|
30
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Command-line front-end for the LongMemEval Free harness.
|
|
2
|
+
|
|
3
|
+
Invoke as ``python -m engrava.benchmarks.longmemeval``. The default
|
|
4
|
+
configuration runs the ``oracle`` variant in substring-evaluation mode
|
|
5
|
+
without any LLM dependency. Flags expose the cosine and LLM modes, the
|
|
6
|
+
variant choice, dreaming OFF/ON arms, and an optional subset filter on
|
|
7
|
+
``question_type``.
|
|
8
|
+
|
|
9
|
+
Exit codes:
|
|
10
|
+
|
|
11
|
+
* ``0`` — at least one question was evaluated, no fatal error.
|
|
12
|
+
* ``1`` — a runtime error fired during the run (download, parsing,
|
|
13
|
+
unsupported configuration).
|
|
14
|
+
* ``2`` — the embeddings extras are missing (re-uses the synthetic
|
|
15
|
+
benchmark's clean-exit convention).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from engrava.benchmarks.longmemeval.dataset_loader import (
|
|
25
|
+
DatasetDownloadError,
|
|
26
|
+
LongMemEvalQuestion,
|
|
27
|
+
load_dataset,
|
|
28
|
+
)
|
|
29
|
+
from engrava.benchmarks.longmemeval.runner import (
|
|
30
|
+
DEFAULT_TOP_K,
|
|
31
|
+
EvalMode,
|
|
32
|
+
LongMemEvalResults,
|
|
33
|
+
run_longmemeval_sync,
|
|
34
|
+
)
|
|
35
|
+
from engrava.benchmarks.synthetic.evaluate import (
|
|
36
|
+
resolve_embedding_provider_or_exit,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Sequence
|
|
41
|
+
|
|
42
|
+
__all__ = ["main"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_VALID_VARIANTS = ("oracle", "s", "m")
|
|
46
|
+
# CLI offers the two deterministic Free-side modes only. The LLM-judge
|
|
47
|
+
# mode requires a caller-supplied client implementing
|
|
48
|
+
# :class:`engrava.benchmarks.longmemeval.evaluate.LLMJudgeClient` and is
|
|
49
|
+
# accessible by driving :func:`run_longmemeval` from Python — see the
|
|
50
|
+
# README for a code snippet.
|
|
51
|
+
_CLI_MODES: tuple[EvalMode, ...] = ("substring", "cosine")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
55
|
+
"""Drive the LongMemEval harness from command-line arguments.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
argv: Argument vector; defaults to :data:`sys.argv` when ``None``.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Process exit code (see module docstring).
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
args = _parse_args(argv)
|
|
65
|
+
try:
|
|
66
|
+
questions = load_dataset(variant=args.variant)
|
|
67
|
+
except DatasetDownloadError as exc:
|
|
68
|
+
sys.stderr.write(f"LongMemEval: {exc}\n")
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
if args.subset:
|
|
72
|
+
questions = [q for q in questions if q.question_type == args.subset]
|
|
73
|
+
if args.limit is not None:
|
|
74
|
+
questions = questions[: args.limit]
|
|
75
|
+
if not questions:
|
|
76
|
+
sys.stderr.write(
|
|
77
|
+
"LongMemEval: no questions to evaluate after applying subset/limit filters.\n",
|
|
78
|
+
)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
provider = resolve_embedding_provider_or_exit()
|
|
82
|
+
try:
|
|
83
|
+
results = run_longmemeval_sync(
|
|
84
|
+
questions,
|
|
85
|
+
dreaming_enabled=args.dreaming,
|
|
86
|
+
embedding_provider=provider,
|
|
87
|
+
eval_mode=args.eval_mode,
|
|
88
|
+
retrieval_top_k=args.top_k,
|
|
89
|
+
)
|
|
90
|
+
except DatasetDownloadError as exc:
|
|
91
|
+
sys.stderr.write(f"LongMemEval: {exc}\n")
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
_emit_report(results, questions=questions)
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
|
|
99
|
+
parser = argparse.ArgumentParser(
|
|
100
|
+
prog="python -m engrava.benchmarks.longmemeval",
|
|
101
|
+
description=(
|
|
102
|
+
"Run the LongMemEval public benchmark against engrava. The "
|
|
103
|
+
"default invocation uses the oracle variant in substring "
|
|
104
|
+
"mode (no LLM required, deterministic)."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--variant",
|
|
109
|
+
choices=_VALID_VARIANTS,
|
|
110
|
+
default="oracle",
|
|
111
|
+
help="Dataset variant to load (default: %(default)s).",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--eval-mode",
|
|
115
|
+
choices=_CLI_MODES,
|
|
116
|
+
default="substring",
|
|
117
|
+
help=(
|
|
118
|
+
"Evaluation mode (default: %(default)s). The LLM-judge mode "
|
|
119
|
+
"is reachable only from Python; see README.md."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--subset",
|
|
124
|
+
default=None,
|
|
125
|
+
help=(
|
|
126
|
+
"Filter questions by question_type (e.g. single-session-recall). "
|
|
127
|
+
"Omit to evaluate every question in the loaded variant."
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--limit",
|
|
132
|
+
type=int,
|
|
133
|
+
default=None,
|
|
134
|
+
help="Cap the number of questions evaluated (default: no cap).",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--top-k",
|
|
138
|
+
type=int,
|
|
139
|
+
default=DEFAULT_TOP_K,
|
|
140
|
+
help="Retrieval top-K passed to search_hybrid (default: %(default)s).",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--dreaming",
|
|
144
|
+
dest="dreaming",
|
|
145
|
+
action="store_true",
|
|
146
|
+
help="Run one dreaming consolidation cycle per question.",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"--no-dreaming",
|
|
150
|
+
dest="dreaming",
|
|
151
|
+
action="store_false",
|
|
152
|
+
help="Disable dreaming for this run (default).",
|
|
153
|
+
)
|
|
154
|
+
parser.set_defaults(dreaming=False)
|
|
155
|
+
return parser.parse_args(argv)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _emit_report(
|
|
159
|
+
results: LongMemEvalResults,
|
|
160
|
+
*,
|
|
161
|
+
questions: list[LongMemEvalQuestion],
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Print a human-readable summary of the run."""
|
|
164
|
+
sys.stdout.write("LongMemEval Free harness\n")
|
|
165
|
+
sys.stdout.write("=" * 60 + "\n")
|
|
166
|
+
sys.stdout.write(f"variant questions evaluated: {results.total_questions}\n")
|
|
167
|
+
sys.stdout.write(f"dreaming enabled: {results.dreaming_enabled}\n")
|
|
168
|
+
sys.stdout.write(f"top_k: {results.top_k}\n")
|
|
169
|
+
sys.stdout.write(f"eval_mode: {results.eval_mode}\n")
|
|
170
|
+
sys.stdout.write("-" * 60 + "\n")
|
|
171
|
+
sys.stdout.write(f"aggregate score : {results.aggregate_score:.4f}\n")
|
|
172
|
+
sys.stdout.write(f"raw signal mean : {results.aggregate_raw_signal:.4f}\n")
|
|
173
|
+
if results.per_type:
|
|
174
|
+
sys.stdout.write("\nper question_type:\n")
|
|
175
|
+
for qtype, score in sorted(results.per_type.items()):
|
|
176
|
+
count = sum(1 for q in questions if q.question_type == qtype)
|
|
177
|
+
sys.stdout.write(f" {qtype:<32} ({count:>3}): {score:.4f}\n")
|
|
178
|
+
sys.stdout.write("=" * 60 + "\n")
|