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.
Files changed (74) hide show
  1. engrava/__init__.py +213 -0
  2. engrava/benchmarks/__init__.py +6 -0
  3. engrava/benchmarks/longmemeval/README.md +179 -0
  4. engrava/benchmarks/longmemeval/__init__.py +30 -0
  5. engrava/benchmarks/longmemeval/__main__.py +10 -0
  6. engrava/benchmarks/longmemeval/cli.py +178 -0
  7. engrava/benchmarks/longmemeval/dataset_loader.py +282 -0
  8. engrava/benchmarks/longmemeval/evaluate.py +240 -0
  9. engrava/benchmarks/longmemeval/runner.py +512 -0
  10. engrava/benchmarks/synthetic/README.md +38 -0
  11. engrava/benchmarks/synthetic/__init__.py +19 -0
  12. engrava/benchmarks/synthetic/__main__.py +10 -0
  13. engrava/benchmarks/synthetic/datasets/__init__.py +7 -0
  14. engrava/benchmarks/synthetic/datasets/synthetic-v1.json +17021 -0
  15. engrava/benchmarks/synthetic/evaluate.py +759 -0
  16. engrava/benchmarks/synthetic/generate.py +686 -0
  17. engrava/benchmarks/synthetic/runner.py +805 -0
  18. engrava/benchmarks/synthetic/scenarios.py +718 -0
  19. engrava/cli/__init__.py +1 -0
  20. engrava/cli/config.py +68 -0
  21. engrava/cli/main.py +1078 -0
  22. engrava/config.py +1973 -0
  23. engrava/domain/__init__.py +1 -0
  24. engrava/domain/enums.py +287 -0
  25. engrava/domain/exceptions.py +192 -0
  26. engrava/domain/manifest.py +43 -0
  27. engrava/domain/models/__init__.py +31 -0
  28. engrava/domain/models/action.py +91 -0
  29. engrava/domain/models/edge.py +56 -0
  30. engrava/domain/models/embedding.py +81 -0
  31. engrava/domain/models/journal.py +82 -0
  32. engrava/domain/models/metrics.py +63 -0
  33. engrava/domain/models/mutation_type.py +28 -0
  34. engrava/domain/models/search.py +42 -0
  35. engrava/domain/models/thought.py +254 -0
  36. engrava/domain/models/ttl.py +51 -0
  37. engrava/domain/protocols/__init__.py +17 -0
  38. engrava/domain/protocols/embedding_provider.py +71 -0
  39. engrava/domain/protocols/engrava_core.py +366 -0
  40. engrava/domain/protocols/hooks.py +205 -0
  41. engrava/embeddings/__init__.py +4 -0
  42. engrava/embeddings/callback.py +93 -0
  43. engrava/embeddings/huggingface.py +158 -0
  44. engrava/embeddings/ollama.py +166 -0
  45. engrava/embeddings/openai_compatible.py +183 -0
  46. engrava/embeddings/sentence_transformer.py +150 -0
  47. engrava/extensions/__init__.py +6 -0
  48. engrava/extensions/discovery.py +80 -0
  49. engrava/extensions/dreaming.py +1535 -0
  50. engrava/extensions/dreaming_cluster_quality.py +512 -0
  51. engrava/extensions/dreaming_keyphrases.py +664 -0
  52. engrava/extensions/dreaming_reflection_content.py +455 -0
  53. engrava/extensions/dreaming_signals.py +312 -0
  54. engrava/extensions/vector_sqlite_vec.py +223 -0
  55. engrava/infrastructure/__init__.py +1 -0
  56. engrava/infrastructure/read_only_store.py +268 -0
  57. engrava/infrastructure/service_manager.py +325 -0
  58. engrava/infrastructure/sqlite/__init__.py +7 -0
  59. engrava/infrastructure/sqlite/centroid.py +58 -0
  60. engrava/infrastructure/sqlite/engrava_core.py +4008 -0
  61. engrava/infrastructure/sqlite/extension_migrations.py +235 -0
  62. engrava/infrastructure/sqlite/journal_writer.py +308 -0
  63. engrava/infrastructure/sqlite/schema_core.sql +185 -0
  64. engrava/metadata.py +135 -0
  65. engrava/mindql/__init__.py +1 -0
  66. engrava/mindql/executor.py +287 -0
  67. engrava/mindql/parser.py +293 -0
  68. engrava/py.typed +0 -0
  69. engrava-0.3.0.dist-info/METADATA +287 -0
  70. engrava-0.3.0.dist-info/RECORD +74 -0
  71. engrava-0.3.0.dist-info/WHEEL +5 -0
  72. engrava-0.3.0.dist-info/entry_points.txt +2 -0
  73. engrava-0.3.0.dist-info/licenses/LICENSE +21 -0
  74. 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,6 @@
1
+ """Benchmark suites for engrava.
2
+
3
+ This commit adds only the synthetic scenario library; the generator,
4
+ evaluator and CLI runner ship in subsequent commits on the same
5
+ feature branch.
6
+ """
@@ -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,10 @@
1
+ """``python -m engrava.benchmarks.longmemeval`` entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from engrava.benchmarks.longmemeval.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -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")