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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Controlled Architectural Baseline: Extract & Resolve (Mem0 Legacy / Entity-RAG paradigm).
|
|
2
|
+
|
|
3
|
+
Characteristics:
|
|
4
|
+
- Extracts discrete facts into an Entity Store with conflict resolution (ADD / UPDATE).
|
|
5
|
+
- Episodic memory passages are stored as static text strings.
|
|
6
|
+
- When a fact updates, its entity record updates, but past episodic texts remain unchanged.
|
|
7
|
+
- Demonstrates why updating facts alone does not fix historical timeline rewriting or stale state in episodic memory.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
from dandelion.store_vector import VectorStore
|
|
13
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExtractResolveAdapter(BaseMemoryAdapter):
|
|
17
|
+
"""Extract & Resolve baseline representing entity extraction with fact overwrite."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.entity_facts: dict[str, dict[str, Any]] = {}
|
|
21
|
+
self.vec: Optional[VectorStore] = None
|
|
22
|
+
self.reset()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
return "extract_resolve"
|
|
27
|
+
|
|
28
|
+
def reset(self) -> None:
|
|
29
|
+
self.entity_facts.clear()
|
|
30
|
+
self.vec = VectorStore(in_memory=True)
|
|
31
|
+
import dandelion.store_kv
|
|
32
|
+
dandelion.store_kv._store = None
|
|
33
|
+
|
|
34
|
+
def ingest_fact(
|
|
35
|
+
self,
|
|
36
|
+
key: str,
|
|
37
|
+
value: str,
|
|
38
|
+
timestamp: float,
|
|
39
|
+
domain: str = "general",
|
|
40
|
+
source: str = "config_read",
|
|
41
|
+
) -> None:
|
|
42
|
+
# Conflict resolution: update entity record with latest observation
|
|
43
|
+
self.entity_facts[key] = {
|
|
44
|
+
"value": value,
|
|
45
|
+
"domain": domain,
|
|
46
|
+
"updated_at": timestamp,
|
|
47
|
+
"source": source,
|
|
48
|
+
}
|
|
49
|
+
# Also index fact representation in vector store
|
|
50
|
+
fact_repr = f"Fact: {key} is {value} in domain {domain}."
|
|
51
|
+
self.vec.save_chunk(
|
|
52
|
+
domain=domain,
|
|
53
|
+
template=fact_repr,
|
|
54
|
+
source=source,
|
|
55
|
+
created_at=timestamp,
|
|
56
|
+
event_id=f"fact_{key}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def ingest_memory(
|
|
60
|
+
self,
|
|
61
|
+
event_id: str,
|
|
62
|
+
template: str,
|
|
63
|
+
domain: str,
|
|
64
|
+
timestamp: float,
|
|
65
|
+
supersedes: Optional[str] = None,
|
|
66
|
+
source: str = "tool_observed",
|
|
67
|
+
) -> str:
|
|
68
|
+
# Resolve entities eagerly against the current entity store at ingestion time
|
|
69
|
+
def _resolve(m: re.Match) -> str:
|
|
70
|
+
raw_key = m.group(1).split("@rev:")[0]
|
|
71
|
+
if domain:
|
|
72
|
+
qual_key = f"{domain}.{raw_key}"
|
|
73
|
+
if qual_key in self.entity_facts:
|
|
74
|
+
return str(self.entity_facts[qual_key]["value"])
|
|
75
|
+
if raw_key in self.entity_facts:
|
|
76
|
+
return str(self.entity_facts[raw_key]["value"])
|
|
77
|
+
return f"[UNKNOWN: {raw_key}]"
|
|
78
|
+
|
|
79
|
+
inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve, template)
|
|
80
|
+
if domain:
|
|
81
|
+
inlined = inlined.replace("{{domain}}", domain)
|
|
82
|
+
|
|
83
|
+
return self.vec.save_chunk(
|
|
84
|
+
domain=domain,
|
|
85
|
+
template=inlined,
|
|
86
|
+
supersedes=supersedes,
|
|
87
|
+
source=source,
|
|
88
|
+
created_at=timestamp,
|
|
89
|
+
event_id=event_id,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def recall(
|
|
93
|
+
self,
|
|
94
|
+
query: str,
|
|
95
|
+
domain: Optional[str] = None,
|
|
96
|
+
limit: int = 3,
|
|
97
|
+
) -> list[dict[str, Any]]:
|
|
98
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
99
|
+
raw_results = self.vec.search_chunks(
|
|
100
|
+
query=query,
|
|
101
|
+
domains=domains,
|
|
102
|
+
limit=limit,
|
|
103
|
+
use_reverse_index=False,
|
|
104
|
+
use_lexical_rerank=False,
|
|
105
|
+
)
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
"id": r["id"],
|
|
109
|
+
"event_id": r.get("event_id", ""),
|
|
110
|
+
"text": r["template"],
|
|
111
|
+
"domain": r["domain"],
|
|
112
|
+
"score": r.get("score", 0.0),
|
|
113
|
+
"vector_score": r.get("vector_score", 0.0),
|
|
114
|
+
}
|
|
115
|
+
for r in raw_results
|
|
116
|
+
]
|
|
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
|
+
qual_key = f"{domain}.{key}"
|
|
126
|
+
if qual_key in self.entity_facts:
|
|
127
|
+
return str(self.entity_facts[qual_key]["value"])
|
|
128
|
+
if key in self.entity_facts:
|
|
129
|
+
return str(self.entity_facts[key]["value"])
|
|
130
|
+
return None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Controlled Architectural Baseline: Hybrid RAG (Dense + BM25 Lexical + RRF).
|
|
2
|
+
|
|
3
|
+
Characteristics:
|
|
4
|
+
- Full state-of-the-art hybrid search (Dense FastEmbed ONNX + Lexical tokens via RRF K=60).
|
|
5
|
+
- Eager static text chunks: facts are inlined directly at ingestion time.
|
|
6
|
+
- No symbolic fact store, no late-binding hydration, no reverse fact index.
|
|
7
|
+
- Raw retrieved content returned without server-enforced secret redaction.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
from dandelion.store_vector import VectorStore
|
|
13
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HybridRAGAdapter(BaseMemoryAdapter):
|
|
17
|
+
"""Hybrid RAG baseline combining Dense vector search and BM25-style lexical search."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.facts: dict[str, str] = {}
|
|
21
|
+
self.vec: Optional[VectorStore] = None
|
|
22
|
+
self.reset()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
return "hybrid_rag"
|
|
27
|
+
|
|
28
|
+
def reset(self) -> None:
|
|
29
|
+
self.facts.clear()
|
|
30
|
+
self.vec = VectorStore(in_memory=True)
|
|
31
|
+
import dandelion.store_kv
|
|
32
|
+
dandelion.store_kv._store = None
|
|
33
|
+
|
|
34
|
+
def ingest_fact(
|
|
35
|
+
self,
|
|
36
|
+
key: str,
|
|
37
|
+
value: str,
|
|
38
|
+
timestamp: float,
|
|
39
|
+
domain: str = "general",
|
|
40
|
+
source: str = "config_read",
|
|
41
|
+
) -> None:
|
|
42
|
+
self.facts[key] = value
|
|
43
|
+
# Index fact as a standalone passage in hybrid store
|
|
44
|
+
fact_sentence = f"Configuration fact '{key}' is set to '{value}' in domain '{domain}'."
|
|
45
|
+
self.vec.save_chunk(
|
|
46
|
+
domain=domain,
|
|
47
|
+
template=fact_sentence,
|
|
48
|
+
source=source,
|
|
49
|
+
created_at=timestamp,
|
|
50
|
+
event_id=f"fact_{key}",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def ingest_memory(
|
|
54
|
+
self,
|
|
55
|
+
event_id: str,
|
|
56
|
+
template: str,
|
|
57
|
+
domain: str,
|
|
58
|
+
timestamp: float,
|
|
59
|
+
supersedes: Optional[str] = None,
|
|
60
|
+
source: str = "tool_observed",
|
|
61
|
+
) -> str:
|
|
62
|
+
def _resolve_eager(m: re.Match) -> str:
|
|
63
|
+
raw_key = m.group(1).split("@rev:")[0]
|
|
64
|
+
if domain:
|
|
65
|
+
qual_key = f"{domain}.{raw_key}"
|
|
66
|
+
if qual_key in self.facts:
|
|
67
|
+
return self.facts[qual_key]
|
|
68
|
+
return self.facts.get(raw_key, f"[MISSING: {raw_key}]")
|
|
69
|
+
|
|
70
|
+
inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_eager, template)
|
|
71
|
+
if domain:
|
|
72
|
+
inlined = inlined.replace("{{domain}}", domain)
|
|
73
|
+
|
|
74
|
+
return self.vec.save_chunk(
|
|
75
|
+
domain=domain,
|
|
76
|
+
template=inlined,
|
|
77
|
+
supersedes=supersedes,
|
|
78
|
+
source=source,
|
|
79
|
+
created_at=timestamp,
|
|
80
|
+
event_id=event_id,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def recall(
|
|
84
|
+
self,
|
|
85
|
+
query: str,
|
|
86
|
+
domain: Optional[str] = None,
|
|
87
|
+
limit: int = 3,
|
|
88
|
+
) -> list[dict[str, Any]]:
|
|
89
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
90
|
+
# Full hybrid search (Dense + Lexical via RRF) without Dandelion reverse lookup
|
|
91
|
+
raw_results = self.vec.search_chunks(
|
|
92
|
+
query=query,
|
|
93
|
+
domains=domains,
|
|
94
|
+
limit=limit,
|
|
95
|
+
use_reverse_index=False,
|
|
96
|
+
use_lexical_rerank=True,
|
|
97
|
+
)
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
"id": r["id"],
|
|
101
|
+
"event_id": r.get("event_id", ""),
|
|
102
|
+
"text": r["template"],
|
|
103
|
+
"domain": r["domain"],
|
|
104
|
+
"score": r.get("score", 0.0),
|
|
105
|
+
"vector_score": r.get("vector_score", 0.0),
|
|
106
|
+
}
|
|
107
|
+
for r in raw_results
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
def get_fact(
|
|
111
|
+
self,
|
|
112
|
+
key: str,
|
|
113
|
+
domain: Optional[str] = None,
|
|
114
|
+
at: Optional[float] = None,
|
|
115
|
+
) -> Optional[str]:
|
|
116
|
+
if domain:
|
|
117
|
+
qual_key = f"{domain}.{key}"
|
|
118
|
+
if qual_key in self.facts:
|
|
119
|
+
return self.facts[qual_key]
|
|
120
|
+
return self.facts.get(key)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Controlled Architectural Ablation: NoLateBinding (Eager Dandelion).
|
|
2
|
+
|
|
3
|
+
Characteristics:
|
|
4
|
+
- Identical primitives to full Dandelion: KVStore, FastEmbed ONNX, Lexical matching, Reverse Index, RRF.
|
|
5
|
+
- Identical server-side secret filtering policy.
|
|
6
|
+
- ONLY difference: {{ref:key}} and {{snapshot:key}} are resolved and inlined at write-time (ingest_memory)
|
|
7
|
+
rather than hydrated at read-time (recall).
|
|
8
|
+
- Isolates the exact marginal causal lift of Late-Binding Hydration.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
from dandelion.store_kv import KVStore
|
|
14
|
+
from dandelion.store_vector import VectorStore
|
|
15
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NoLateBindingAdapter(BaseMemoryAdapter):
|
|
19
|
+
"""Full Dandelion architecture with late-binding hydration disabled (eager inlining)."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.kv: Optional[KVStore] = None
|
|
23
|
+
self.vec: Optional[VectorStore] = None
|
|
24
|
+
self.reset()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return "no_late_binding"
|
|
29
|
+
|
|
30
|
+
def reset(self) -> None:
|
|
31
|
+
import dandelion.store_kv
|
|
32
|
+
dandelion.store_kv._store = None
|
|
33
|
+
self.kv = KVStore(path=None)
|
|
34
|
+
dandelion.store_kv._store = self.kv
|
|
35
|
+
self.vec = VectorStore(in_memory=True)
|
|
36
|
+
|
|
37
|
+
def ingest_fact(
|
|
38
|
+
self,
|
|
39
|
+
key: str,
|
|
40
|
+
value: str,
|
|
41
|
+
timestamp: float,
|
|
42
|
+
domain: str = "general",
|
|
43
|
+
source: str = "config_read",
|
|
44
|
+
) -> None:
|
|
45
|
+
self.kv.set_fact(key, value, source=source, confidence=1.0, timestamp=timestamp)
|
|
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
|
+
# Eager resolution at ingestion time
|
|
57
|
+
def _resolve_eager(m: re.Match) -> str:
|
|
58
|
+
raw_key = m.group(1).split("@rev:")[0]
|
|
59
|
+
val = None
|
|
60
|
+
if domain:
|
|
61
|
+
val = self.kv.get_fact(f"{domain}.{raw_key}", at_timestamp=timestamp)
|
|
62
|
+
if val is None:
|
|
63
|
+
val = self.kv.get_fact(raw_key, at_timestamp=timestamp)
|
|
64
|
+
return str(val) if val is not None else f"[UNKNOWN: {raw_key}]"
|
|
65
|
+
|
|
66
|
+
inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_eager, template)
|
|
67
|
+
if domain:
|
|
68
|
+
inlined = inlined.replace("{{domain}}", domain)
|
|
69
|
+
|
|
70
|
+
return self.vec.save_chunk(
|
|
71
|
+
domain=domain,
|
|
72
|
+
template=inlined,
|
|
73
|
+
supersedes=supersedes,
|
|
74
|
+
source=source,
|
|
75
|
+
created_at=timestamp,
|
|
76
|
+
event_id=event_id,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def recall(
|
|
80
|
+
self,
|
|
81
|
+
query: str,
|
|
82
|
+
domain: Optional[str] = None,
|
|
83
|
+
limit: int = 3,
|
|
84
|
+
) -> list[dict[str, Any]]:
|
|
85
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
86
|
+
raw_results = self.vec.search_chunks(
|
|
87
|
+
query=query,
|
|
88
|
+
domains=domains,
|
|
89
|
+
limit=limit,
|
|
90
|
+
use_reverse_index=True,
|
|
91
|
+
use_lexical_rerank=True,
|
|
92
|
+
)
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
"id": r["id"],
|
|
96
|
+
"event_id": r.get("event_id", ""),
|
|
97
|
+
"text": r["template"], # Raw static text, no runtime hydration
|
|
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 at is not None:
|
|
112
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
113
|
+
return self.kv.resolve_ref(key, domain=domain)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Dandelion adapter for DandelionBench evaluation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import dandelion.store_kv
|
|
5
|
+
from dandelion.store_kv import KVStore
|
|
6
|
+
from dandelion.store_vector import VectorStore
|
|
7
|
+
from dandelion.hydrator import hydrate_template, pin_snapshots
|
|
8
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DandelionAdapter(BaseMemoryAdapter):
|
|
12
|
+
"""Full Dandelion adapter with symbolic KV, RRF, snapshots, and reverse index."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.kv: Optional[KVStore] = None
|
|
16
|
+
self.vec: Optional[VectorStore] = None
|
|
17
|
+
self.reset()
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "dandelion"
|
|
22
|
+
|
|
23
|
+
def reset(self) -> None:
|
|
24
|
+
"""Reset internal stores to empty state in memory."""
|
|
25
|
+
self.kv = KVStore(path=None)
|
|
26
|
+
dandelion.store_kv._store = self.kv
|
|
27
|
+
self.vec = VectorStore(in_memory=True)
|
|
28
|
+
|
|
29
|
+
def ingest_fact(
|
|
30
|
+
self,
|
|
31
|
+
key: str,
|
|
32
|
+
value: str,
|
|
33
|
+
timestamp: float,
|
|
34
|
+
domain: str = "general",
|
|
35
|
+
source: str = "config_read",
|
|
36
|
+
) -> None:
|
|
37
|
+
self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
|
|
38
|
+
|
|
39
|
+
def ingest_memory(
|
|
40
|
+
self,
|
|
41
|
+
event_id: str,
|
|
42
|
+
template: str,
|
|
43
|
+
domain: str,
|
|
44
|
+
timestamp: float,
|
|
45
|
+
supersedes: Optional[str] = None,
|
|
46
|
+
source: str = "tool_observed",
|
|
47
|
+
) -> str:
|
|
48
|
+
# Pin snapshots at remember-time to lock historical facts
|
|
49
|
+
pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
|
|
50
|
+
return self.vec.save_chunk(
|
|
51
|
+
domain=domain,
|
|
52
|
+
template=pinned,
|
|
53
|
+
supersedes=supersedes,
|
|
54
|
+
source=source,
|
|
55
|
+
created_at=timestamp,
|
|
56
|
+
event_id=event_id,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def recall(
|
|
60
|
+
self,
|
|
61
|
+
query: str,
|
|
62
|
+
domain: Optional[str] = None,
|
|
63
|
+
limit: int = 3,
|
|
64
|
+
) -> list[dict[str, Any]]:
|
|
65
|
+
domains = [domain] if domain and not domain.endswith(".*") else None
|
|
66
|
+
raw_results = self.vec.search_chunks(query=query, domains=domains, limit=limit)
|
|
67
|
+
|
|
68
|
+
out = []
|
|
69
|
+
for r in raw_results:
|
|
70
|
+
hydrated = hydrate_template(
|
|
71
|
+
r["template"],
|
|
72
|
+
kv_resolver=self.kv.resolve_ref,
|
|
73
|
+
chunk_domain=r["domain"],
|
|
74
|
+
allow_secrets=False,
|
|
75
|
+
)
|
|
76
|
+
out.append({
|
|
77
|
+
"id": r["id"],
|
|
78
|
+
"event_id": r.get("event_id", r["id"]),
|
|
79
|
+
"text": hydrated,
|
|
80
|
+
"template": r["template"],
|
|
81
|
+
"domain": r["domain"],
|
|
82
|
+
"score": r["score"],
|
|
83
|
+
})
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
def get_fact(
|
|
87
|
+
self,
|
|
88
|
+
key: str,
|
|
89
|
+
domain: Optional[str] = None,
|
|
90
|
+
at: Optional[float] = None,
|
|
91
|
+
) -> Optional[str]:
|
|
92
|
+
if at is not None:
|
|
93
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
94
|
+
return self.kv.resolve_ref(key, domain=domain)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Native competitor adapters for DandelionBench Track B.
|
|
2
|
+
|
|
3
|
+
Adapters in this module wrap upstream third-party SDKs (mem0ai, letta-client)
|
|
4
|
+
using pinned library versions, recorded configuration metadata, and dual
|
|
5
|
+
evaluation surfaces (Native Retrieval Surface & Native End-to-End Surface).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from benchmarks.adapters.native.base import NativeMemoryAdapter
|
|
9
|
+
from benchmarks.adapters.native.dandelion_native import DandelionNativeAdapter
|
|
10
|
+
from benchmarks.adapters.native.dandelion_native_b0 import DandelionNativeB0Adapter
|
|
11
|
+
from benchmarks.adapters.native.dandelion_native_b1 import DandelionNativeB1Adapter
|
|
12
|
+
from benchmarks.adapters.native.dandelion_native_b2 import DandelionNativeB2Adapter
|
|
13
|
+
from benchmarks.adapters.native.dandelion_native_b3 import DandelionNativeB3Adapter
|
|
14
|
+
from benchmarks.adapters.native.dandelion_native_c4 import DandelionNativeAdapterC4
|
|
15
|
+
from benchmarks.adapters.native.dandelion_native_c4_1 import DandelionNativeAdapterC41
|
|
16
|
+
from benchmarks.adapters.native.mem0_native import Mem0NativeAdapter
|
|
17
|
+
from benchmarks.adapters.native.letta_native import LettaNativeAdapter
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"NativeMemoryAdapter",
|
|
21
|
+
"DandelionNativeAdapter",
|
|
22
|
+
"DandelionNativeB0Adapter",
|
|
23
|
+
"DandelionNativeB1Adapter",
|
|
24
|
+
"DandelionNativeB2Adapter",
|
|
25
|
+
"DandelionNativeB3Adapter",
|
|
26
|
+
"DandelionNativeAdapterC4",
|
|
27
|
+
"DandelionNativeAdapterC41",
|
|
28
|
+
"Mem0NativeAdapter",
|
|
29
|
+
"LettaNativeAdapter",
|
|
30
|
+
]
|
|
31
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Base interface and telemetry structures for Track B Native Competitor Adapters."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
from benchmarks.adapters.base import BaseMemoryAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NativeMemoryAdapter(BaseMemoryAdapter):
|
|
9
|
+
"""Abstract interface for Track B native third-party memory systems."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def package_version(self) -> str:
|
|
14
|
+
"""Upstream library package version (e.g. 'mem0ai==2.0.20')."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def configuration_metadata(self) -> dict[str, Any]:
|
|
20
|
+
"""Hardware, LLM, embedding model, and storage configuration dictionary."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def telemetry(self) -> dict[str, Any]:
|
|
26
|
+
"""Runtime execution telemetry (tokens, LLM calls, API calls, cost estimate)."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def answer(
|
|
31
|
+
self,
|
|
32
|
+
query: str,
|
|
33
|
+
domain: Optional[str] = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""End-to-End answering surface.
|
|
36
|
+
|
|
37
|
+
Returns a dictionary containing:
|
|
38
|
+
- 'answer': str (final generated or synthesized answer)
|
|
39
|
+
- 'retrieved_context': str
|
|
40
|
+
- 'context_tokens': int
|
|
41
|
+
- 'llm_calls': int
|
|
42
|
+
- 'api_calls': int
|
|
43
|
+
- 'cost_usd': float
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Dandelion native adapter for DandelionBench Track B."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import dandelion.store_kv
|
|
5
|
+
from dandelion.store_kv import KVStore
|
|
6
|
+
from dandelion.store_vector import VectorStore
|
|
7
|
+
from dandelion.hydrator import hydrate_template, pin_snapshots
|
|
8
|
+
from benchmarks.adapters.native.base import NativeMemoryAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DandelionNativeAdapter(NativeMemoryAdapter):
|
|
12
|
+
"""Full Dandelion native memory engine with Hub & Spoke architecture."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.kv: Optional[KVStore] = None
|
|
16
|
+
self.vec: Optional[VectorStore] = None
|
|
17
|
+
self.query_count = 0
|
|
18
|
+
self.total_tokens = 0
|
|
19
|
+
self.reset()
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "dandelion_native"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def package_version(self) -> str:
|
|
27
|
+
return "dandelion-0.2.0 (local)"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def configuration_metadata(self) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"system": "dandelion",
|
|
33
|
+
"architecture": "hub_and_spoke_symbolic_semantic",
|
|
34
|
+
"embedding_model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
|
35
|
+
"llm_used": "none (deterministic symbolic hydration)",
|
|
36
|
+
"vector_store": "qdrant_in_memory",
|
|
37
|
+
"hydration_policy": "read_time_late_binding",
|
|
38
|
+
"security_policy": "deny_by_default_secret_masking",
|
|
39
|
+
"reverse_index": "enabled_exact_match",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def telemetry(self) -> dict[str, Any]:
|
|
44
|
+
avg_tok = (self.total_tokens / max(1, self.query_count)) if self.query_count else 0
|
|
45
|
+
return {
|
|
46
|
+
"query_count": self.query_count,
|
|
47
|
+
"llm_calls_total": 0,
|
|
48
|
+
"llm_calls_per_query": 0.0,
|
|
49
|
+
"api_calls_total": 0,
|
|
50
|
+
"api_calls_per_query": 0.0,
|
|
51
|
+
"cost_total_usd": 0.0,
|
|
52
|
+
"cost_per_1k_queries_usd": 0.0,
|
|
53
|
+
"mean_memory_context_tokens": round(avg_tok, 1),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def reset(self) -> None:
|
|
57
|
+
self.kv = KVStore(path=None)
|
|
58
|
+
dandelion.store_kv._store = self.kv
|
|
59
|
+
self.vec = VectorStore(in_memory=True)
|
|
60
|
+
self.query_count = 0
|
|
61
|
+
self.total_tokens = 0
|
|
62
|
+
|
|
63
|
+
def ingest_fact(
|
|
64
|
+
self,
|
|
65
|
+
key: str,
|
|
66
|
+
value: str,
|
|
67
|
+
timestamp: float,
|
|
68
|
+
domain: str = "general",
|
|
69
|
+
source: str = "config_read",
|
|
70
|
+
) -> None:
|
|
71
|
+
self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
|
|
72
|
+
|
|
73
|
+
def ingest_memory(
|
|
74
|
+
self,
|
|
75
|
+
event_id: str,
|
|
76
|
+
template: str,
|
|
77
|
+
domain: str,
|
|
78
|
+
timestamp: float,
|
|
79
|
+
supersedes: Optional[str] = None,
|
|
80
|
+
source: str = "tool_observed",
|
|
81
|
+
) -> str:
|
|
82
|
+
pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
|
|
83
|
+
return self.vec.save_chunk(
|
|
84
|
+
domain=domain,
|
|
85
|
+
template=pinned,
|
|
86
|
+
supersedes=supersedes,
|
|
87
|
+
source=source,
|
|
88
|
+
created_at=timestamp,
|
|
89
|
+
event_id=event_id,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def recall(
|
|
93
|
+
self,
|
|
94
|
+
query: str,
|
|
95
|
+
domain: Optional[str] = None,
|
|
96
|
+
limit: int = 3,
|
|
97
|
+
) -> list[dict[str, Any]]:
|
|
98
|
+
domains = [domain] if domain else None
|
|
99
|
+
raw_results = self.vec.search_chunks(query=query, domains=domains, limit=limit)
|
|
100
|
+
hydrated: list[dict[str, Any]] = []
|
|
101
|
+
|
|
102
|
+
for r in raw_results:
|
|
103
|
+
text = hydrate_template(
|
|
104
|
+
text=r["template"],
|
|
105
|
+
kv_resolver=self.kv.resolve_ref,
|
|
106
|
+
chunk_domain=r.get("domain", "general"),
|
|
107
|
+
allow_secrets=False,
|
|
108
|
+
)
|
|
109
|
+
hydrated.append({
|
|
110
|
+
"id": r["id"],
|
|
111
|
+
"event_id": r.get("event_id", r["id"]),
|
|
112
|
+
"text": text,
|
|
113
|
+
"domain": r.get("domain", "general"),
|
|
114
|
+
"score": r.get("score", 0.0),
|
|
115
|
+
"vector_score": r.get("vector_score", 0.0),
|
|
116
|
+
"has_fact": r.get("has_fact", False),
|
|
117
|
+
"is_historical": "{{ref:" in r["template"] and "@rev:" in r["template"],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
self.query_count += 1
|
|
121
|
+
tok_sum = sum(len(h["text"].split()) for h in hydrated)
|
|
122
|
+
self.total_tokens += tok_sum
|
|
123
|
+
|
|
124
|
+
return hydrated
|
|
125
|
+
|
|
126
|
+
def answer(
|
|
127
|
+
self,
|
|
128
|
+
query: str,
|
|
129
|
+
domain: Optional[str] = None,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""End-to-end answering surface using secure context envelope."""
|
|
132
|
+
candidates = self.recall(query=query, domain=domain, limit=3)
|
|
133
|
+
if not candidates:
|
|
134
|
+
return {
|
|
135
|
+
"answer": "I have no memory regarding this topic.",
|
|
136
|
+
"retrieved_context": "",
|
|
137
|
+
"context_tokens": 0,
|
|
138
|
+
"llm_calls": 0,
|
|
139
|
+
"api_calls": 0,
|
|
140
|
+
"cost_usd": 0.0,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
context_str = "\n".join(f"[{c['event_id']}] {c['text']}" for c in candidates)
|
|
144
|
+
context_tokens = len(context_str.split())
|
|
145
|
+
|
|
146
|
+
# Construct answering payload
|
|
147
|
+
return {
|
|
148
|
+
"answer": context_str,
|
|
149
|
+
"retrieved_context": context_str,
|
|
150
|
+
"context_tokens": context_tokens,
|
|
151
|
+
"llm_calls": 0,
|
|
152
|
+
"api_calls": 0,
|
|
153
|
+
"cost_usd": 0.0,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def get_fact(
|
|
157
|
+
self,
|
|
158
|
+
key: str,
|
|
159
|
+
domain: Optional[str] = None,
|
|
160
|
+
at: Optional[float] = None,
|
|
161
|
+
) -> Optional[str]:
|
|
162
|
+
if at is not None:
|
|
163
|
+
return self.kv.get_fact(key, at_timestamp=at)
|
|
164
|
+
return self.kv.resolve_ref(key, domain=domain)
|