dandelion-memory 1.0.0rc15__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.
- benchmarks/__init__.py +3 -0
- benchmarks/adapters/__init__.py +29 -0
- benchmarks/adapters/ablations.py +406 -0
- benchmarks/adapters/base.py +72 -0
- benchmarks/adapters/controlled/__init__.py +13 -0
- benchmarks/adapters/controlled/core_archival.py +115 -0
- benchmarks/adapters/controlled/extract_resolve.py +130 -0
- benchmarks/adapters/controlled/hybrid_rag.py +120 -0
- benchmarks/adapters/controlled/no_late_binding.py +113 -0
- benchmarks/adapters/dandelion.py +94 -0
- benchmarks/adapters/native/__init__.py +31 -0
- benchmarks/adapters/native/base.py +45 -0
- benchmarks/adapters/native/dandelion_native.py +164 -0
- benchmarks/adapters/native/dandelion_native_b0.py +165 -0
- benchmarks/adapters/native/dandelion_native_b1.py +170 -0
- benchmarks/adapters/native/dandelion_native_b2.py +169 -0
- benchmarks/adapters/native/dandelion_native_b3.py +171 -0
- benchmarks/adapters/native/dandelion_native_c4.py +261 -0
- benchmarks/adapters/native/dandelion_native_c4_1.py +281 -0
- benchmarks/adapters/native/dandelion_native_c4_2.py +435 -0
- benchmarks/adapters/native/event_evidence_b2.py +616 -0
- benchmarks/adapters/native/evidence_b2.py +609 -0
- benchmarks/adapters/native/letta_native.py +253 -0
- benchmarks/adapters/native/mem0_native.py +219 -0
- benchmarks/adapters/native/store_vector_b0.py +792 -0
- benchmarks/adapters/native/store_vector_b2.py +808 -0
- benchmarks/agent_adapter_stdio_acceptance.py +86 -0
- benchmarks/bootstrap_cross_stack.py +218 -0
- benchmarks/canonical_profile_instrumentation.py +167 -0
- benchmarks/canonical_public_baseline.py +288 -0
- benchmarks/cli.py +525 -0
- benchmarks/collector_public30_offline.py +1653 -0
- benchmarks/datasets/__init__.py +45 -0
- benchmarks/datasets/conversational_generator.py +238 -0
- benchmarks/datasets/generator.py +283 -0
- benchmarks/datasets/holdout_topology_generator.py +290 -0
- benchmarks/datasets/holdout_topology_generator_v2.py +294 -0
- benchmarks/datasets/multi_world.py +1643 -0
- benchmarks/datasets/scale_generator.py +289 -0
- benchmarks/datasets/schema.py +57 -0
- benchmarks/deep_collision_stress.py +199 -0
- benchmarks/evaluators/__init__.py +12 -0
- benchmarks/evaluators/manifest.py +142 -0
- benchmarks/evaluators/metrics.py +277 -0
- benchmarks/evaluators/runner.py +258 -0
- benchmarks/f1_step0/build_dev_measurement_manifest_v1.py +127 -0
- benchmarks/f1_step0/build_dev_measurement_manifest_v2.py +127 -0
- benchmarks/f1_step0/build_dev_measurement_manifest_v3.py +146 -0
- benchmarks/f1_step0/build_ledger.py +143 -0
- benchmarks/f1_step0/build_ledger_v2.py +296 -0
- benchmarks/f1_step0/compare_dev52_readiness_v1.py +106 -0
- benchmarks/f1_step0/locator_public_smoke.py +172 -0
- benchmarks/f1_step0/prepare_dev52_owner_v1.py +133 -0
- benchmarks/f1_step0/prepare_dev52_owner_v2.py +133 -0
- benchmarks/f1_step0/public_identity_probe.py +191 -0
- benchmarks/f1_step0/public_response_mapper.py +101 -0
- benchmarks/f1_step0/real_source_public_preflight.py +244 -0
- benchmarks/f1_step0/run_exact_fusion_dev_experiment_v1.py +83 -0
- benchmarks/f1_step0/scorer.py +110 -0
- benchmarks/f1_step0/source_aware_dev_measurement_v1.py +627 -0
- benchmarks/f1_step0/source_aware_dev_measurement_v2.py +659 -0
- benchmarks/f1_step0/source_aware_dev_measurement_v3.py +776 -0
- benchmarks/f1_step0/test_public_response_mapper.py +110 -0
- benchmarks/f1_step0/test_real_source_public_preflight.py +25 -0
- benchmarks/f1_step0/test_scorer.py +158 -0
- benchmarks/f1_step0/test_source_aware_dev_measurement.py +189 -0
- benchmarks/f1_step0/test_source_aware_dev_measurement_v2.py +263 -0
- benchmarks/f1_step0/test_source_aware_dev_measurement_v3.py +334 -0
- benchmarks/f1_step0/typed_readback_public_smoke.py +181 -0
- benchmarks/forensic_c4_1_dev.py +258 -0
- benchmarks/forensic_c4_dev.py +255 -0
- benchmarks/forensic_deep25_c4_1.py +478 -0
- benchmarks/legacy_migration_acceptance.py +29 -0
- benchmarks/longmemeval_actor.py +497 -0
- benchmarks/longmemeval_agent_judge.py +593 -0
- benchmarks/longmemeval_budget.py +139 -0
- benchmarks/longmemeval_full.py +410 -0
- benchmarks/longmemeval_judge.py +327 -0
- benchmarks/longmemeval_pilot.py +316 -0
- benchmarks/longmemeval_s.py +318 -0
- benchmarks/memory_bench.py +248 -0
- benchmarks/memory_formation/__init__.py +1 -0
- benchmarks/memory_formation/audit.py +801 -0
- benchmarks/memory_formation/real_audit.py +688 -0
- benchmarks/memory_session_acceptance.py +279 -0
- benchmarks/native_codex_host.py +513 -0
- benchmarks/owner_dense_acceptance.py +126 -0
- benchmarks/pilot_seeker_vault.py +266 -0
- benchmarks/prepare_public30_ripgrep_range_owner.py +115 -0
- benchmarks/profile_canonical.py +373 -0
- benchmarks/profile_canonical_lanes.py +405 -0
- benchmarks/profile_search_chunks.py +351 -0
- benchmarks/public_ranges_faq_proof.py +121 -0
- benchmarks/public_repos/build_history_scope_v3.py +330 -0
- benchmarks/public_repos/build_oracle.py +108 -0
- benchmarks/public_repos/build_oracle_v2.py +169 -0
- benchmarks/public_repos/build_oracle_v3.py +176 -0
- benchmarks/public_repos/map_public_v2.py +296 -0
- benchmarks/public_repos/project_prompts_v2.py +31 -0
- benchmarks/public_repos/project_prompts_v3.py +29 -0
- benchmarks/public_repos/ready_admin_v2.py +214 -0
- benchmarks/public_repos/score_public_v2.py +258 -0
- benchmarks/public_repos/test_map_public_v2.py +171 -0
- benchmarks/public_repos/test_public_repo_v2.py +44 -0
- benchmarks/public_repos/test_public_repo_v3.py +46 -0
- benchmarks/public_repos/test_ready_admin_v2.py +117 -0
- benchmarks/public_repos/test_score_public_v2.py +163 -0
- benchmarks/public_repos/test_wire_v2.py +292 -0
- benchmarks/public_repos/wire_v2.py +603 -0
- benchmarks/real_blocker_fix_acceptance.py +155 -0
- benchmarks/real_scan_rescan_acceptance.py +41 -0
- benchmarks/recall_v2_20k_smoke.py +112 -0
- benchmarks/recall_v2_e1_retrieve.py +70 -0
- benchmarks/recall_v2_e1_score.py +271 -0
- benchmarks/recall_v2_e2_prepare.py +69 -0
- benchmarks/recall_v2_e2_public_score.py +91 -0
- benchmarks/recall_v2_e2_score.py +108 -0
- benchmarks/recall_v2_navigation_mcp_replay.py +308 -0
- benchmarks/recall_v2_reader_capture.py +118 -0
- benchmarks/recall_v2_reader_replay.py +136 -0
- benchmarks/run_blind_h10_holdout.py +284 -0
- benchmarks/run_blind_h9_holdout.py +275 -0
- benchmarks/run_c4_2_nightly_scale.py +446 -0
- benchmarks/run_context_scale_simulation.py +189 -0
- benchmarks/run_conversational_bench.py +170 -0
- benchmarks/run_h11_evaluation.py +316 -0
- benchmarks/run_h11_v2_evaluation.py +431 -0
- benchmarks/run_native_fixed12.py +469 -0
- benchmarks/run_native_preflight.py +280 -0
- benchmarks/run_scale_stress.py +225 -0
- benchmarks/solanazation_brownfield_01/build_oracle.py +970 -0
- benchmarks/solanazation_brownfield_01/check_level_a.py +78 -0
- benchmarks/solanazation_brownfield_01/debug_scope.py +13 -0
- benchmarks/solanazation_brownfield_01/evaluate_bootstrap_02.py +570 -0
- benchmarks/solanazation_brownfield_01/evaluate_first_pass.py +306 -0
- benchmarks/solanazation_brownfield_01/generate_dryrun_report.py +222 -0
- benchmarks/solanazation_brownfield_01/generate_manifests.py +146 -0
- benchmarks/solanazation_brownfield_01/inspect_misses.py +29 -0
- benchmarks/solanazation_brownfield_01/run_miss_forensic.py +206 -0
- benchmarks/solanazation_brownfield_01/test_facts.py +15 -0
- benchmarks/statistics/__init__.py +15 -0
- benchmarks/statistics/significance.py +326 -0
- benchmarks/test_native_mcp_preflight.py +229 -0
- benchmarks/test_prepare_public30_range_owner.py +35 -0
- benchmarks/test_public30_source_reference.py +698 -0
- benchmarks/test_recall_v2_e1_reader_input.py +89 -0
- benchmarks/test_recall_v2_navigation_mcp_replay.py +155 -0
- benchmarks/test_recall_v2_reader_replay.py +41 -0
- dandelion/__init__.py +3 -0
- dandelion/__main__.py +11 -0
- dandelion/agent_adapter.py +2369 -0
- dandelion/bootstrap/__init__.py +872 -0
- dandelion/bootstrap/alias_policy.py +50 -0
- dandelion/bootstrap/authority.py +225 -0
- dandelion/bootstrap/canonical_history.py +412 -0
- dandelion/bootstrap/classifier.py +403 -0
- dandelion/bootstrap/component_graph.py +280 -0
- dandelion/bootstrap/discovery.py +279 -0
- dandelion/bootstrap/domain_induction.py +180 -0
- dandelion/bootstrap/domain_topology.py +673 -0
- dandelion/bootstrap/evidence_envelope.py +132 -0
- dandelion/bootstrap/extractors/base.py +171 -0
- dandelion/bootstrap/extractors/exact.py +1234 -0
- dandelion/bootstrap/extractors/generic.py +100 -0
- dandelion/bootstrap/extractors/git_temporal.py +111 -0
- dandelion/bootstrap/extractors/registry.py +345 -0
- dandelion/bootstrap/extractors/relations.py +89 -0
- dandelion/bootstrap/extractors/semantic.py +424 -0
- dandelion/bootstrap/extractors/status.py +218 -0
- dandelion/bootstrap/git_sweeper.py +135 -0
- dandelion/bootstrap/manifest.py +246 -0
- dandelion/bootstrap/quarantine.py +264 -0
- dandelion/bootstrap/report.py +259 -0
- dandelion/bootstrap/root_resolver.py +221 -0
- dandelion/bootstrap/tech_profiler.py +125 -0
- dandelion/bootstrap/topology.py +75 -0
- dandelion/bootstrap/topology_index.py +110 -0
- dandelion/bootstrap/transaction.py +315 -0
- dandelion/canonical_bootstrap.py +257 -0
- dandelion/canonical_memory.py +4130 -0
- dandelion/captured_input.py +295 -0
- dandelion/cli.py +767 -0
- dandelion/config.py +40 -0
- dandelion/derived_index.py +745 -0
- dandelion/domains.py +706 -0
- dandelion/embedder.py +229 -0
- dandelion/event_evidence.py +571 -0
- dandelion/evidence.py +812 -0
- dandelion/evidence_response.py +453 -0
- dandelion/evidence_workspace.py +2360 -0
- dandelion/exact_context.py +129 -0
- dandelion/hydrator.py +124 -0
- dandelion/inspector.py +442 -0
- dandelion/inspector_assets/dandelion-mark.svg +33 -0
- dandelion/inspector_assets/dandelion_graph.js +361 -0
- dandelion/inspector_assets/inspector.css +223 -0
- dandelion/inspector_assets/inspector.html +135 -0
- dandelion/inspector_assets/inspector.js +1059 -0
- dandelion/legacy_import.py +431 -0
- dandelion/live_source_grants.py +133 -0
- dandelion/local_owner.py +114 -0
- dandelion/memory_client.py +252 -0
- dandelion/memory_contract.py +107 -0
- dandelion/memory_owner.py +560 -0
- dandelion/reader_providers.py +118 -0
- dandelion/recall_v2.py +767 -0
- dandelion/reconciler.py +219 -0
- dandelion/request_budget.py +125 -0
- dandelion/retained_basis.py +223 -0
- dandelion/retrieval.py +485 -0
- dandelion/router.py +405 -0
- dandelion/search_terms.py +53 -0
- dandelion/security.py +124 -0
- dandelion/server.py +810 -0
- dandelion/source_content_fts.py +219 -0
- dandelion/source_delivery.py +89 -0
- dandelion/source_history.py +241 -0
- dandelion/source_identity.py +88 -0
- dandelion/source_navigator.py +5 -0
- dandelion/source_reader.py +444 -0
- dandelion/source_references.py +379 -0
- dandelion/source_sections.py +265 -0
- dandelion/store_kv.py +684 -0
- dandelion/store_vector.py +1289 -0
- dandelion/studio.py +325 -0
- dandelion/wal.py +227 -0
- dandelion_memory-1.0.0rc15.dist-info/METADATA +48 -0
- dandelion_memory-1.0.0rc15.dist-info/RECORD +232 -0
- dandelion_memory-1.0.0rc15.dist-info/WHEEL +5 -0
- dandelion_memory-1.0.0rc15.dist-info/entry_points.txt +5 -0
- dandelion_memory-1.0.0rc15.dist-info/licenses/LICENSE +21 -0
- dandelion_memory-1.0.0rc15.dist-info/top_level.txt +2 -0
benchmarks/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Adapters module for DandelionBench."""
|
|
2
|
+
|
|
3
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
4
|
+
from benchmarks.adapters.dandelion import DandelionAdapter
|
|
5
|
+
from benchmarks.adapters.ablations import (
|
|
6
|
+
DenseOnlyAdapter,
|
|
7
|
+
NoSnapshotAdapter,
|
|
8
|
+
NoReverseAdapter,
|
|
9
|
+
NoLexicalAdapter,
|
|
10
|
+
)
|
|
11
|
+
from benchmarks.adapters.controlled import (
|
|
12
|
+
HybridRAGAdapter,
|
|
13
|
+
ExtractResolveAdapter,
|
|
14
|
+
CoreArchivalAdapter,
|
|
15
|
+
NoLateBindingAdapter,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BaseMemoryAdapter",
|
|
20
|
+
"DandelionAdapter",
|
|
21
|
+
"DenseOnlyAdapter",
|
|
22
|
+
"NoSnapshotAdapter",
|
|
23
|
+
"NoReverseAdapter",
|
|
24
|
+
"NoLexicalAdapter",
|
|
25
|
+
"HybridRAGAdapter",
|
|
26
|
+
"ExtractResolveAdapter",
|
|
27
|
+
"CoreArchivalAdapter",
|
|
28
|
+
"NoLateBindingAdapter",
|
|
29
|
+
]
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Ablations and baseline adapters for DandelionBench."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
import dandelion.store_kv
|
|
6
|
+
from dandelion.store_kv import KVStore
|
|
7
|
+
from dandelion.store_vector import VectorStore
|
|
8
|
+
from dandelion.hydrator import hydrate_template
|
|
9
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DenseOnlyAdapter(BaseMemoryAdapter):
|
|
13
|
+
"""Standard Dense Vector Memory baseline (e.g. naive vector DB / Mem0 / LangChain RAG).
|
|
14
|
+
|
|
15
|
+
Characteristics:
|
|
16
|
+
- Pure vector embeddings (FastEmbed cosine similarity).
|
|
17
|
+
- Eager binding: facts are inlined directly into text at ingestion time.
|
|
18
|
+
- No symbolic late-binding: when facts mutate, stored memories remain stale.
|
|
19
|
+
- No reverse reference index: cannot trace fact values back to referencing memories.
|
|
20
|
+
- No secret redaction policy: sensitive tokens are stored and recalled in plaintext.
|
|
21
|
+
- No identifier / BM25 lexical boost.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self.facts: dict[str, str] = {}
|
|
26
|
+
self.vec: Optional[VectorStore] = None
|
|
27
|
+
self.reset()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return "dense_only"
|
|
32
|
+
|
|
33
|
+
def reset(self) -> None:
|
|
34
|
+
self.facts.clear()
|
|
35
|
+
self.vec = VectorStore(in_memory=True)
|
|
36
|
+
# Clear global KV store to prevent state leakage from prior adapters
|
|
37
|
+
import dandelion.store_kv
|
|
38
|
+
dandelion.store_kv._store = None
|
|
39
|
+
|
|
40
|
+
def ingest_fact(
|
|
41
|
+
self,
|
|
42
|
+
key: str,
|
|
43
|
+
value: str,
|
|
44
|
+
timestamp: float,
|
|
45
|
+
domain: str = "general",
|
|
46
|
+
source: str = "config_read",
|
|
47
|
+
) -> None:
|
|
48
|
+
self.facts[key] = value
|
|
49
|
+
# Also index fact as plain passage in vector store
|
|
50
|
+
fact_sentence = f"Configuration fact '{key}' is set to '{value}' in domain '{domain}'."
|
|
51
|
+
self.vec.save_chunk(
|
|
52
|
+
domain=domain,
|
|
53
|
+
template=fact_sentence,
|
|
54
|
+
source=source,
|
|
55
|
+
created_at=timestamp,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def ingest_memory(
|
|
59
|
+
self,
|
|
60
|
+
event_id: str,
|
|
61
|
+
template: str,
|
|
62
|
+
domain: str,
|
|
63
|
+
timestamp: float,
|
|
64
|
+
supersedes: Optional[str] = None,
|
|
65
|
+
source: str = "tool_observed",
|
|
66
|
+
) -> str:
|
|
67
|
+
# Eager resolution at store time: inlines current fact value directly
|
|
68
|
+
def _resolve_eager(m: re.Match) -> str:
|
|
69
|
+
raw_key = m.group(1).split("@rev:")[0]
|
|
70
|
+
# Try domain-qualified first
|
|
71
|
+
if domain:
|
|
72
|
+
qual_key = f"{domain}.{raw_key}"
|
|
73
|
+
if qual_key in self.facts:
|
|
74
|
+
return self.facts[qual_key]
|
|
75
|
+
return self.facts.get(raw_key, f"[MISSING: {raw_key}]")
|
|
76
|
+
|
|
77
|
+
inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_eager, template)
|
|
78
|
+
if domain:
|
|
79
|
+
inlined = inlined.replace("{{domain}}", domain)
|
|
80
|
+
|
|
81
|
+
return self.vec.save_chunk(
|
|
82
|
+
domain=domain,
|
|
83
|
+
template=inlined,
|
|
84
|
+
supersedes=supersedes,
|
|
85
|
+
source=source,
|
|
86
|
+
created_at=timestamp,
|
|
87
|
+
event_id=event_id,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def recall(
|
|
91
|
+
self,
|
|
92
|
+
query: str,
|
|
93
|
+
domain: Optional[str] = None,
|
|
94
|
+
limit: int = 3,
|
|
95
|
+
) -> list[dict[str, Any]]:
|
|
96
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
97
|
+
# Dense vector search only: no lexical rerank, no reverse index
|
|
98
|
+
raw_results = self.vec.search_chunks(
|
|
99
|
+
query=query,
|
|
100
|
+
domains=domains,
|
|
101
|
+
limit=limit,
|
|
102
|
+
use_reverse_index=False,
|
|
103
|
+
use_lexical_rerank=False,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
out = []
|
|
107
|
+
for r in raw_results:
|
|
108
|
+
out.append({
|
|
109
|
+
"id": r["id"],
|
|
110
|
+
"event_id": r.get("event_id", r["id"]),
|
|
111
|
+
"text": r["template"],
|
|
112
|
+
"template": r["template"],
|
|
113
|
+
"domain": r["domain"],
|
|
114
|
+
"score": r["score"],
|
|
115
|
+
})
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
def get_fact(
|
|
119
|
+
self,
|
|
120
|
+
key: str,
|
|
121
|
+
domain: Optional[str] = None,
|
|
122
|
+
at: Optional[float] = None,
|
|
123
|
+
) -> Optional[str]:
|
|
124
|
+
if domain:
|
|
125
|
+
scoped = f"{domain}.{key}"
|
|
126
|
+
if scoped in self.facts:
|
|
127
|
+
return self.facts[scoped]
|
|
128
|
+
return self.facts.get(key)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class NoSnapshotAdapter(BaseMemoryAdapter):
|
|
132
|
+
"""Ablation: Dandelion without Snapshot Pinning.
|
|
133
|
+
|
|
134
|
+
All {{snapshot:key}} references are treated as live {{ref:key}}.
|
|
135
|
+
When a fact mutates, historical memories also resolve to the new value,
|
|
136
|
+
violating temporal fidelity (tests Hypothesis H2).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
self.kv: Optional[KVStore] = None
|
|
141
|
+
self.vec: Optional[VectorStore] = None
|
|
142
|
+
self.reset()
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def name(self) -> str:
|
|
146
|
+
return "no_snapshot"
|
|
147
|
+
|
|
148
|
+
def reset(self) -> None:
|
|
149
|
+
self.kv = KVStore(path=None)
|
|
150
|
+
dandelion.store_kv._store = self.kv
|
|
151
|
+
self.vec = VectorStore(in_memory=True)
|
|
152
|
+
|
|
153
|
+
def ingest_fact(
|
|
154
|
+
self,
|
|
155
|
+
key: str,
|
|
156
|
+
value: str,
|
|
157
|
+
timestamp: float,
|
|
158
|
+
domain: str = "general",
|
|
159
|
+
source: str = "config_read",
|
|
160
|
+
) -> None:
|
|
161
|
+
self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
|
|
162
|
+
|
|
163
|
+
def ingest_memory(
|
|
164
|
+
self,
|
|
165
|
+
event_id: str,
|
|
166
|
+
template: str,
|
|
167
|
+
domain: str,
|
|
168
|
+
timestamp: float,
|
|
169
|
+
supersedes: Optional[str] = None,
|
|
170
|
+
source: str = "tool_observed",
|
|
171
|
+
) -> str:
|
|
172
|
+
# Intentionally convert snapshot to live ref instead of pinning
|
|
173
|
+
unpinned = re.sub(r"\{\{snapshot:([\w.:\-]+)\}\}", r"{{ref:\1}}", template)
|
|
174
|
+
return self.vec.save_chunk(
|
|
175
|
+
domain=domain,
|
|
176
|
+
template=unpinned,
|
|
177
|
+
supersedes=supersedes,
|
|
178
|
+
source=source,
|
|
179
|
+
created_at=timestamp,
|
|
180
|
+
event_id=event_id,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def recall(
|
|
184
|
+
self,
|
|
185
|
+
query: str,
|
|
186
|
+
domain: Optional[str] = None,
|
|
187
|
+
limit: int = 3,
|
|
188
|
+
) -> list[dict[str, Any]]:
|
|
189
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
190
|
+
raw_results = self.vec.search_chunks(query=query, domains=domains, limit=limit)
|
|
191
|
+
|
|
192
|
+
out = []
|
|
193
|
+
for r in raw_results:
|
|
194
|
+
hydrated = hydrate_template(
|
|
195
|
+
r["template"],
|
|
196
|
+
kv_resolver=self.kv.resolve_ref,
|
|
197
|
+
chunk_domain=r["domain"],
|
|
198
|
+
allow_secrets=False,
|
|
199
|
+
)
|
|
200
|
+
out.append({
|
|
201
|
+
"id": r["id"],
|
|
202
|
+
"event_id": r.get("event_id", r["id"]),
|
|
203
|
+
"text": hydrated,
|
|
204
|
+
"template": r["template"],
|
|
205
|
+
"domain": r["domain"],
|
|
206
|
+
"score": r["score"],
|
|
207
|
+
})
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
def get_fact(
|
|
211
|
+
self,
|
|
212
|
+
key: str,
|
|
213
|
+
domain: Optional[str] = None,
|
|
214
|
+
at: Optional[float] = None,
|
|
215
|
+
) -> Optional[str]:
|
|
216
|
+
if at is not None:
|
|
217
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
218
|
+
return self.kv.resolve_ref(key, domain=domain)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class NoReverseAdapter(BaseMemoryAdapter):
|
|
222
|
+
"""Ablation: Dandelion without Fact Reverse-Lookup Index.
|
|
223
|
+
|
|
224
|
+
Querying for fact values (e.g. '5433') relies solely on dense/lexical
|
|
225
|
+
matching without checking which chunks reference that fact key.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self) -> None:
|
|
229
|
+
self.kv: Optional[KVStore] = None
|
|
230
|
+
self.vec: Optional[VectorStore] = None
|
|
231
|
+
self.reset()
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def name(self) -> str:
|
|
235
|
+
return "no_reverse"
|
|
236
|
+
|
|
237
|
+
def reset(self) -> None:
|
|
238
|
+
self.kv = KVStore(path=None)
|
|
239
|
+
dandelion.store_kv._store = self.kv
|
|
240
|
+
self.vec = VectorStore(in_memory=True)
|
|
241
|
+
|
|
242
|
+
def ingest_fact(
|
|
243
|
+
self,
|
|
244
|
+
key: str,
|
|
245
|
+
value: str,
|
|
246
|
+
timestamp: float,
|
|
247
|
+
domain: str = "general",
|
|
248
|
+
source: str = "config_read",
|
|
249
|
+
) -> None:
|
|
250
|
+
self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
|
|
251
|
+
|
|
252
|
+
def ingest_memory(
|
|
253
|
+
self,
|
|
254
|
+
event_id: str,
|
|
255
|
+
template: str,
|
|
256
|
+
domain: str,
|
|
257
|
+
timestamp: float,
|
|
258
|
+
supersedes: Optional[str] = None,
|
|
259
|
+
source: str = "tool_observed",
|
|
260
|
+
) -> str:
|
|
261
|
+
from dandelion.hydrator import pin_snapshots
|
|
262
|
+
pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
|
|
263
|
+
return self.vec.save_chunk(
|
|
264
|
+
domain=domain,
|
|
265
|
+
template=pinned,
|
|
266
|
+
supersedes=supersedes,
|
|
267
|
+
source=source,
|
|
268
|
+
created_at=timestamp,
|
|
269
|
+
event_id=event_id,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def recall(
|
|
273
|
+
self,
|
|
274
|
+
query: str,
|
|
275
|
+
domain: Optional[str] = None,
|
|
276
|
+
limit: int = 3,
|
|
277
|
+
) -> list[dict[str, Any]]:
|
|
278
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
279
|
+
# Explicitly disable reverse index in hybrid search
|
|
280
|
+
raw_results = self.vec.search_chunks(
|
|
281
|
+
query=query,
|
|
282
|
+
domains=domains,
|
|
283
|
+
limit=limit,
|
|
284
|
+
use_reverse_index=False,
|
|
285
|
+
use_lexical_rerank=True,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
out = []
|
|
289
|
+
for r in raw_results:
|
|
290
|
+
hydrated = hydrate_template(
|
|
291
|
+
r["template"],
|
|
292
|
+
kv_resolver=self.kv.resolve_ref,
|
|
293
|
+
chunk_domain=r["domain"],
|
|
294
|
+
allow_secrets=False,
|
|
295
|
+
)
|
|
296
|
+
out.append({
|
|
297
|
+
"id": r["id"],
|
|
298
|
+
"event_id": r.get("event_id", r["id"]),
|
|
299
|
+
"text": hydrated,
|
|
300
|
+
"template": r["template"],
|
|
301
|
+
"domain": r["domain"],
|
|
302
|
+
"score": r["score"],
|
|
303
|
+
})
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
def get_fact(
|
|
307
|
+
self,
|
|
308
|
+
key: str,
|
|
309
|
+
domain: Optional[str] = None,
|
|
310
|
+
at: Optional[float] = None,
|
|
311
|
+
) -> Optional[str]:
|
|
312
|
+
if at is not None:
|
|
313
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
314
|
+
return self.kv.resolve_ref(key, domain=domain)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class NoLexicalAdapter(BaseMemoryAdapter):
|
|
318
|
+
"""Ablation: Dandelion without BM25 / Lexical Identifier Matching."""
|
|
319
|
+
|
|
320
|
+
def __init__(self) -> None:
|
|
321
|
+
self.kv: Optional[KVStore] = None
|
|
322
|
+
self.vec: Optional[VectorStore] = None
|
|
323
|
+
self.reset()
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def name(self) -> str:
|
|
327
|
+
return "no_lexical"
|
|
328
|
+
|
|
329
|
+
def reset(self) -> None:
|
|
330
|
+
self.kv = KVStore(path=None)
|
|
331
|
+
dandelion.store_kv._store = self.kv
|
|
332
|
+
self.vec = VectorStore(in_memory=True)
|
|
333
|
+
|
|
334
|
+
def ingest_fact(
|
|
335
|
+
self,
|
|
336
|
+
key: str,
|
|
337
|
+
value: str,
|
|
338
|
+
timestamp: float,
|
|
339
|
+
domain: str = "general",
|
|
340
|
+
source: str = "config_read",
|
|
341
|
+
) -> None:
|
|
342
|
+
self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
|
|
343
|
+
|
|
344
|
+
def ingest_memory(
|
|
345
|
+
self,
|
|
346
|
+
event_id: str,
|
|
347
|
+
template: str,
|
|
348
|
+
domain: str,
|
|
349
|
+
timestamp: float,
|
|
350
|
+
supersedes: Optional[str] = None,
|
|
351
|
+
source: str = "tool_observed",
|
|
352
|
+
) -> str:
|
|
353
|
+
from dandelion.hydrator import pin_snapshots
|
|
354
|
+
pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
|
|
355
|
+
return self.vec.save_chunk(
|
|
356
|
+
domain=domain,
|
|
357
|
+
template=pinned,
|
|
358
|
+
supersedes=supersedes,
|
|
359
|
+
source=source,
|
|
360
|
+
created_at=timestamp,
|
|
361
|
+
event_id=event_id,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def recall(
|
|
365
|
+
self,
|
|
366
|
+
query: str,
|
|
367
|
+
domain: Optional[str] = None,
|
|
368
|
+
limit: int = 3,
|
|
369
|
+
) -> list[dict[str, Any]]:
|
|
370
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
371
|
+
# Explicitly disable lexical reranking
|
|
372
|
+
raw_results = self.vec.search_chunks(
|
|
373
|
+
query=query,
|
|
374
|
+
domains=domains,
|
|
375
|
+
limit=limit,
|
|
376
|
+
use_reverse_index=True,
|
|
377
|
+
use_lexical_rerank=False,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
out = []
|
|
381
|
+
for r in raw_results:
|
|
382
|
+
hydrated = hydrate_template(
|
|
383
|
+
r["template"],
|
|
384
|
+
kv_resolver=self.kv.resolve_ref,
|
|
385
|
+
chunk_domain=r["domain"],
|
|
386
|
+
allow_secrets=False,
|
|
387
|
+
)
|
|
388
|
+
out.append({
|
|
389
|
+
"id": r["id"],
|
|
390
|
+
"event_id": r.get("event_id", r["id"]),
|
|
391
|
+
"text": hydrated,
|
|
392
|
+
"template": r["template"],
|
|
393
|
+
"domain": r["domain"],
|
|
394
|
+
"score": r["score"],
|
|
395
|
+
})
|
|
396
|
+
return out
|
|
397
|
+
|
|
398
|
+
def get_fact(
|
|
399
|
+
self,
|
|
400
|
+
key: str,
|
|
401
|
+
domain: Optional[str] = None,
|
|
402
|
+
at: Optional[float] = None,
|
|
403
|
+
) -> Optional[str]:
|
|
404
|
+
if at is not None:
|
|
405
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
406
|
+
return self.kv.resolve_ref(key, domain=domain)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Base interface for agent memory benchmark adapters."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseMemoryAdapter(ABC):
|
|
8
|
+
"""Abstract interface for memory systems under evaluation."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def name(self) -> str:
|
|
13
|
+
"""System or ablation name (e.g. 'dandelion', 'dense_only')."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def reset(self) -> None:
|
|
18
|
+
"""Clear all internal memory stores."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def ingest_fact(
|
|
23
|
+
self,
|
|
24
|
+
key: str,
|
|
25
|
+
value: str,
|
|
26
|
+
timestamp: float,
|
|
27
|
+
domain: str = "general",
|
|
28
|
+
source: str = "config_read",
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Store or mutate a deterministic fact."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def ingest_memory(
|
|
35
|
+
self,
|
|
36
|
+
event_id: str,
|
|
37
|
+
template: str,
|
|
38
|
+
domain: str,
|
|
39
|
+
timestamp: float,
|
|
40
|
+
supersedes: Optional[str] = None,
|
|
41
|
+
source: str = "tool_observed",
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Store an episodic/semantic memory event."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def recall(
|
|
48
|
+
self,
|
|
49
|
+
query: str,
|
|
50
|
+
domain: Optional[str] = None,
|
|
51
|
+
limit: int = 3,
|
|
52
|
+
) -> list[dict[str, Any]]:
|
|
53
|
+
"""Retrieve top candidate memories for query.
|
|
54
|
+
|
|
55
|
+
Each returned dict MUST contain at least:
|
|
56
|
+
- 'id': str
|
|
57
|
+
- 'event_id': str
|
|
58
|
+
- 'text': str (hydrated or inlined content shown to agent)
|
|
59
|
+
- 'domain': str
|
|
60
|
+
- 'score': float
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def get_fact(
|
|
66
|
+
self,
|
|
67
|
+
key: str,
|
|
68
|
+
domain: Optional[str] = None,
|
|
69
|
+
at: Optional[float] = None,
|
|
70
|
+
) -> Optional[str]:
|
|
71
|
+
"""Directly query a deterministic fact."""
|
|
72
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Controlled architectural baseline adapters for DandelionBench Track A."""
|
|
2
|
+
|
|
3
|
+
from benchmarks.adapters.controlled.hybrid_rag import HybridRAGAdapter
|
|
4
|
+
from benchmarks.adapters.controlled.extract_resolve import ExtractResolveAdapter
|
|
5
|
+
from benchmarks.adapters.controlled.core_archival import CoreArchivalAdapter
|
|
6
|
+
from benchmarks.adapters.controlled.no_late_binding import NoLateBindingAdapter
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"HybridRAGAdapter",
|
|
10
|
+
"ExtractResolveAdapter",
|
|
11
|
+
"CoreArchivalAdapter",
|
|
12
|
+
"NoLateBindingAdapter",
|
|
13
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Controlled Architectural Baseline: Core + Archival Memory (MemGPT / Letta paradigm).
|
|
2
|
+
|
|
3
|
+
Characteristics:
|
|
4
|
+
- Two-tier memory hierarchy:
|
|
5
|
+
1. In-context Core Memory (structured blocks, editable in place, always visible).
|
|
6
|
+
2. Long-term Archival Memory (dense vector search over past event logs).
|
|
7
|
+
- Facts immediately update Core Memory blocks.
|
|
8
|
+
- Episodic memories are stored into Archival Memory as static passages.
|
|
9
|
+
- Evaluates the effect of in-context core state versus late-bound symbolic hydration.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
from dandelion.store_vector import VectorStore
|
|
15
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CoreArchivalAdapter(BaseMemoryAdapter):
|
|
19
|
+
"""Core + Archival memory baseline representing two-tier agent memory systems."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.core_memory: dict[str, str] = {}
|
|
23
|
+
self.archival_vec: Optional[VectorStore] = None
|
|
24
|
+
self.reset()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return "core_archival"
|
|
29
|
+
|
|
30
|
+
def reset(self) -> None:
|
|
31
|
+
self.core_memory.clear()
|
|
32
|
+
self.archival_vec = VectorStore(in_memory=True)
|
|
33
|
+
import dandelion.store_kv
|
|
34
|
+
dandelion.store_kv._store = None
|
|
35
|
+
|
|
36
|
+
def ingest_fact(
|
|
37
|
+
self,
|
|
38
|
+
key: str,
|
|
39
|
+
value: str,
|
|
40
|
+
timestamp: float,
|
|
41
|
+
domain: str = "general",
|
|
42
|
+
source: str = "config_read",
|
|
43
|
+
) -> None:
|
|
44
|
+
# Core Memory update (in-place state edit)
|
|
45
|
+
self.core_memory[key] = value
|
|
46
|
+
|
|
47
|
+
def ingest_memory(
|
|
48
|
+
self,
|
|
49
|
+
event_id: str,
|
|
50
|
+
template: str,
|
|
51
|
+
domain: str,
|
|
52
|
+
timestamp: float,
|
|
53
|
+
supersedes: Optional[str] = None,
|
|
54
|
+
source: str = "tool_observed",
|
|
55
|
+
) -> str:
|
|
56
|
+
# Archival memory stores static passages resolved against current Core Memory
|
|
57
|
+
def _resolve_core(m: re.Match) -> str:
|
|
58
|
+
raw_key = m.group(1).split("@rev:")[0]
|
|
59
|
+
if domain:
|
|
60
|
+
qual_key = f"{domain}.{raw_key}"
|
|
61
|
+
if qual_key in self.core_memory:
|
|
62
|
+
return self.core_memory[qual_key]
|
|
63
|
+
return self.core_memory.get(raw_key, f"[MISSING: {raw_key}]")
|
|
64
|
+
|
|
65
|
+
inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_core, template)
|
|
66
|
+
if domain:
|
|
67
|
+
inlined = inlined.replace("{{domain}}", domain)
|
|
68
|
+
|
|
69
|
+
return self.archival_vec.save_chunk(
|
|
70
|
+
domain=domain,
|
|
71
|
+
template=inlined,
|
|
72
|
+
supersedes=supersedes,
|
|
73
|
+
source=source,
|
|
74
|
+
created_at=timestamp,
|
|
75
|
+
event_id=event_id,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def recall(
|
|
79
|
+
self,
|
|
80
|
+
query: str,
|
|
81
|
+
domain: Optional[str] = None,
|
|
82
|
+
limit: int = 3,
|
|
83
|
+
) -> list[dict[str, Any]]:
|
|
84
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
85
|
+
# Archival vector search
|
|
86
|
+
raw_results = self.archival_vec.search_chunks(
|
|
87
|
+
query=query,
|
|
88
|
+
domains=domains,
|
|
89
|
+
limit=limit,
|
|
90
|
+
use_reverse_index=False,
|
|
91
|
+
use_lexical_rerank=False,
|
|
92
|
+
)
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
"id": r["id"],
|
|
96
|
+
"event_id": r.get("event_id", ""),
|
|
97
|
+
"text": r["template"],
|
|
98
|
+
"domain": r["domain"],
|
|
99
|
+
"score": r.get("score", 0.0),
|
|
100
|
+
"vector_score": r.get("vector_score", 0.0),
|
|
101
|
+
}
|
|
102
|
+
for r in raw_results
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def get_fact(
|
|
106
|
+
self,
|
|
107
|
+
key: str,
|
|
108
|
+
domain: Optional[str] = None,
|
|
109
|
+
at: Optional[float] = None,
|
|
110
|
+
) -> Optional[str]:
|
|
111
|
+
if domain:
|
|
112
|
+
qual_key = f"{domain}.{key}"
|
|
113
|
+
if qual_key in self.core_memory:
|
|
114
|
+
return self.core_memory[qual_key]
|
|
115
|
+
return self.core_memory.get(key)
|