contextos-memory-runtime 1.0.0rc2__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.
- contextos/__init__.py +3 -0
- contextos/__main__.py +6 -0
- contextos/api/__init__.py +1 -0
- contextos/api/routes/__init__.py +1 -0
- contextos/api/routes/desktop.py +322 -0
- contextos/api/routes/ingest.py +17 -0
- contextos/api/routes/memories.py +84 -0
- contextos/api/routes/models.py +81 -0
- contextos/api/routes/retrieval.py +89 -0
- contextos/api/routes/system.py +216 -0
- contextos/api/server.py +195 -0
- contextos/benchmarks/__init__.py +1 -0
- contextos/benchmarks/compilation.py +245 -0
- contextos/benchmarks/connectors.py +423 -0
- contextos/benchmarks/explainability.py +103 -0
- contextos/benchmarks/final.py +406 -0
- contextos/benchmarks/graph.py +310 -0
- contextos/benchmarks/graph_adversarial.py +525 -0
- contextos/benchmarks/mcp.py +324 -0
- contextos/benchmarks/model_routing.py +203 -0
- contextos/benchmarks/optimization.py +305 -0
- contextos/benchmarks/rescue_integration.py +127 -0
- contextos/benchmarks/retrieval.py +266 -0
- contextos/benchmarks/temporal.py +377 -0
- contextos/benchmarks/temporal_hotpath.py +76 -0
- contextos/benchmarks/terminal.py +62 -0
- contextos/cli/__init__.py +1 -0
- contextos/cli/app.py +932 -0
- contextos/cli/dashboard.py +174 -0
- contextos/cli/formatters.py +299 -0
- contextos/config/__init__.py +1 -0
- contextos/config/settings.py +160 -0
- contextos/connectors/__init__.py +6 -0
- contextos/connectors/fake.py +11 -0
- contextos/connectors/json_import.py +125 -0
- contextos/connectors/local_files.py +102 -0
- contextos/connectors/manager.py +293 -0
- contextos/connectors/models.py +62 -0
- contextos/connectors/protocols.py +11 -0
- contextos/core/__init__.py +103 -0
- contextos/core/enums.py +489 -0
- contextos/core/exceptions.py +293 -0
- contextos/core/models.py +1147 -0
- contextos/core/protocols.py +549 -0
- contextos/daemon/__init__.py +1 -0
- contextos/daemon/manager.py +510 -0
- contextos/daemon/state.py +127 -0
- contextos/daemon/wiring.py +296 -0
- contextos/demo.py +217 -0
- contextos/embedding/__init__.py +1 -0
- contextos/embedding/deterministic.py +76 -0
- contextos/embedding/sentence_transformers.py +80 -0
- contextos/mcp/__init__.py +5 -0
- contextos/mcp/server.py +269 -0
- contextos/providers/__init__.py +13 -0
- contextos/providers/fake.py +217 -0
- contextos/providers/ollama.py +297 -0
- contextos/providers/openai_compatible.py +337 -0
- contextos/services/__init__.py +1 -0
- contextos/services/compilation.py +535 -0
- contextos/services/explainability.py +553 -0
- contextos/services/extraction.py +311 -0
- contextos/services/graph.py +524 -0
- contextos/services/graph_retrieval.py +143 -0
- contextos/services/ingestion.py +143 -0
- contextos/services/inspection.py +174 -0
- contextos/services/memory.py +291 -0
- contextos/services/model_service.py +409 -0
- contextos/services/optimization.py +426 -0
- contextos/services/privacy.py +331 -0
- contextos/services/retrieval.py +302 -0
- contextos/services/retrieval_index.py +88 -0
- contextos/services/router.py +302 -0
- contextos/services/secret_scanner.py +207 -0
- contextos/services/telemetry_query.py +102 -0
- contextos/services/temporal.py +500 -0
- contextos/services/token_counter.py +222 -0
- contextos/storage/__init__.py +1 -0
- contextos/storage/connector_repo.py +67 -0
- contextos/storage/database.py +497 -0
- contextos/storage/event_repo.py +137 -0
- contextos/storage/graph_repo.py +228 -0
- contextos/storage/lexical/__init__.py +1 -0
- contextos/storage/lexical/bm25.py +134 -0
- contextos/storage/memory_repo.py +589 -0
- contextos/storage/relation_repo.py +80 -0
- contextos/storage/telemetry_repo.py +481 -0
- contextos/storage/vector/__init__.py +1 -0
- contextos/storage/vector/in_memory.py +162 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/METADATA +143 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/RECORD +93 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/WHEEL +4 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""Deterministic, ephemeral explanations for one existing pipeline execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID, uuid4
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from contextos.core.enums import MemoryStatus, RetrievalMode, TemporalScope
|
|
13
|
+
from contextos.core.models import (
|
|
14
|
+
CompilationConfig,
|
|
15
|
+
ContextBudget,
|
|
16
|
+
Memory,
|
|
17
|
+
ProviderDispatchEvidence,
|
|
18
|
+
RetrievalQuery,
|
|
19
|
+
RetrievalResult,
|
|
20
|
+
)
|
|
21
|
+
from contextos.services.retrieval import HybridRetrievalEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExplanationRequest(BaseModel):
|
|
25
|
+
query: str = Field(min_length=1, max_length=10_000, repr=False)
|
|
26
|
+
mode: RetrievalMode = RetrievalMode.HYBRID
|
|
27
|
+
budget: int = Field(default=1000, ge=1, le=8_000)
|
|
28
|
+
limit: int = Field(default=25, ge=1, le=100)
|
|
29
|
+
graph: bool = True
|
|
30
|
+
include_content: bool = False
|
|
31
|
+
target_memory_id: UUID | None = None
|
|
32
|
+
temporal_scope: TemporalScope = TemporalScope.CURRENT
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExplanationTrace(BaseModel):
|
|
36
|
+
trace_id: str
|
|
37
|
+
query_id: str
|
|
38
|
+
strategy: str
|
|
39
|
+
stages: list[dict]
|
|
40
|
+
candidates: list[dict]
|
|
41
|
+
selected: list[str]
|
|
42
|
+
excluded: list[dict]
|
|
43
|
+
final_context: dict
|
|
44
|
+
content: str | None = None
|
|
45
|
+
evidence_scope: str = "retrieved candidates only"
|
|
46
|
+
requested_memory: dict | None = None
|
|
47
|
+
provider_dispatch: dict[str, Any] = Field(
|
|
48
|
+
default_factory=lambda: {
|
|
49
|
+
"state": "NOT_ATTEMPTED",
|
|
50
|
+
"status_message": "prepared by ContextOS; no provider dispatch attempted",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
execution_ms: float = 0.0
|
|
54
|
+
measured_pipeline_ms: float = 0.0
|
|
55
|
+
explanation_overhead_ms: float = 0.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_UNSAFE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x1b\x07]*(?:\x07|\x1b\\)|.)")
|
|
59
|
+
_BIDI = {chr(value) for value in (*range(0x202A, 0x202F), *range(0x2066, 0x206A))}
|
|
60
|
+
_SAFE_SOURCE_TYPES = {
|
|
61
|
+
"cli_input", "cli", "mcp", "api", "file", "local_file", "json_import",
|
|
62
|
+
"fake", "system", "benchmark", "manual",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def safe_text(value: object, limit: int = 160) -> str:
|
|
67
|
+
text = _UNSAFE.sub("", str(value or ""))
|
|
68
|
+
return "".join(char for char in text if char.isprintable() and char not in _BIDI)[:limit]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def safe_source_type(value: str) -> str:
|
|
72
|
+
if value in _SAFE_SOURCE_TYPES:
|
|
73
|
+
return value
|
|
74
|
+
if value.startswith("connector:") and value.removeprefix("connector:") in {"local_file", "json_import", "fake"}:
|
|
75
|
+
return value
|
|
76
|
+
return "unknown"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TemporalEvidenceResolver:
|
|
80
|
+
"""Resolve structured, non-inferred temporal evidence for memories and relations."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, memory_repo: Any, relation_repo: Any = None) -> None:
|
|
83
|
+
self.memory_repo = memory_repo
|
|
84
|
+
self.relation_repo = relation_repo
|
|
85
|
+
|
|
86
|
+
async def resolve_memory_evidence(
|
|
87
|
+
self,
|
|
88
|
+
memory: Memory,
|
|
89
|
+
retrieval_request: RetrievalQuery | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
eligible = (
|
|
92
|
+
HybridRetrievalEngine._eligible(memory, retrieval_request)
|
|
93
|
+
if retrieval_request is not None
|
|
94
|
+
else (memory.status == MemoryStatus.ACTIVE)
|
|
95
|
+
)
|
|
96
|
+
reason_code = self._reason_code(memory, eligible)
|
|
97
|
+
|
|
98
|
+
relations_evidence: list[dict[str, Any]] = []
|
|
99
|
+
if self.relation_repo is not None:
|
|
100
|
+
raw_relations = await self.relation_repo.get_relations(memory.id, direction="both")
|
|
101
|
+
for rel in raw_relations[:50]:
|
|
102
|
+
related_id = (
|
|
103
|
+
rel.target_memory_id
|
|
104
|
+
if rel.source_memory_id == memory.id
|
|
105
|
+
else rel.source_memory_id
|
|
106
|
+
)
|
|
107
|
+
related_mem = await self.memory_repo.get(related_id) if self.memory_repo else None
|
|
108
|
+
target_mem = await self.memory_repo.get(rel.target_memory_id) if self.memory_repo else None
|
|
109
|
+
related_state = (
|
|
110
|
+
"missing" if related_mem is None else
|
|
111
|
+
"deleted" if related_mem.status in {MemoryStatus.DELETED, MemoryStatus.PURGED} else
|
|
112
|
+
"present"
|
|
113
|
+
)
|
|
114
|
+
relations_evidence.append({
|
|
115
|
+
"relation_id": str(rel.id),
|
|
116
|
+
"relation_type": rel.relation_type.value,
|
|
117
|
+
"source_memory_id": str(rel.source_memory_id),
|
|
118
|
+
"target_memory_id": str(rel.target_memory_id),
|
|
119
|
+
"related_memory_id": str(related_id),
|
|
120
|
+
"confidence": rel.confidence,
|
|
121
|
+
"created_at": rel.created_at.isoformat(),
|
|
122
|
+
"related_memory_state": related_state,
|
|
123
|
+
"target_deleted": target_mem is None or target_mem.status in {
|
|
124
|
+
MemoryStatus.DELETED, MemoryStatus.PURGED,
|
|
125
|
+
},
|
|
126
|
+
"acceptance_rationale": None,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"status": memory.temporal_status.value,
|
|
131
|
+
"lifecycle": memory.status.value,
|
|
132
|
+
"temporal_status": memory.temporal_status.value,
|
|
133
|
+
"eligible": eligible,
|
|
134
|
+
"reason_code": reason_code,
|
|
135
|
+
"reason": "eligible_by_retrieval_policy" if eligible else "temporal_policy_ineligible",
|
|
136
|
+
"replacement_memory_id": str(memory.superseded_by) if memory.superseded_by else None,
|
|
137
|
+
"observed_at": memory.observed_at.isoformat() if memory.observed_at else None,
|
|
138
|
+
"valid_from": memory.valid_from.isoformat() if memory.valid_from else None,
|
|
139
|
+
"valid_to": memory.valid_to.isoformat() if memory.valid_to else None,
|
|
140
|
+
"relations": relations_evidence,
|
|
141
|
+
"acceptance_rationale": None,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _reason_code(memory: Memory, eligible: bool) -> str:
|
|
146
|
+
if memory.status == MemoryStatus.SUPERSEDED:
|
|
147
|
+
return "REPLACED_BY_CURRENT_STATE"
|
|
148
|
+
if memory.status == MemoryStatus.HISTORICAL:
|
|
149
|
+
return "HISTORICAL_RECORD"
|
|
150
|
+
if memory.status == MemoryStatus.CONTRADICTED:
|
|
151
|
+
return "CONTRADICTED_STATE"
|
|
152
|
+
if memory.status == MemoryStatus.EXPIRED:
|
|
153
|
+
return "LIFECYCLE_EXPIRED"
|
|
154
|
+
if memory.status in {MemoryStatus.DELETED, MemoryStatus.PURGED}:
|
|
155
|
+
return "LIFECYCLE_DELETED"
|
|
156
|
+
if memory.status == MemoryStatus.ACTIVE:
|
|
157
|
+
return "CURRENT_STATE" if eligible else "ACTIVE_INELIGIBLE_FOR_QUERY"
|
|
158
|
+
return "UNKNOWN_STATE"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ExplainabilityService:
|
|
162
|
+
"""Collect explanations from the same retrieval/optimization/compile results."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, services: dict) -> None:
|
|
165
|
+
self.services = services
|
|
166
|
+
self.token_counter = services.get("token_counter") or getattr(
|
|
167
|
+
services.get("optimizer"), "_token_counter", None
|
|
168
|
+
)
|
|
169
|
+
self.temporal_resolver = TemporalEvidenceResolver(
|
|
170
|
+
services.get("memory_repo"), services.get("relation_repo")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def explain(self, request: ExplanationRequest) -> ExplanationTrace:
|
|
174
|
+
trace, _, _, _ = await self.execute(request)
|
|
175
|
+
return trace
|
|
176
|
+
|
|
177
|
+
async def execute(self, request: ExplanationRequest) -> tuple[ExplanationTrace, RetrievalResult, Any, Any]:
|
|
178
|
+
"""Return the trace and its original pipeline objects for bounded inspection."""
|
|
179
|
+
started = time.perf_counter()
|
|
180
|
+
mode = request.mode
|
|
181
|
+
if not request.graph and mode in {RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH}:
|
|
182
|
+
mode = RetrievalMode.HYBRID
|
|
183
|
+
if request.graph and mode == RetrievalMode.HYBRID:
|
|
184
|
+
mode = RetrievalMode.HYBRID_GRAPH
|
|
185
|
+
retrieval_request = RetrievalQuery(
|
|
186
|
+
text=request.query,
|
|
187
|
+
mode=mode,
|
|
188
|
+
k=request.limit,
|
|
189
|
+
include_trace=True,
|
|
190
|
+
temporal_scope=request.temporal_scope,
|
|
191
|
+
)
|
|
192
|
+
retrieved = await self.services["retrieval"].retrieve(retrieval_request)
|
|
193
|
+
optimizer_started = time.perf_counter()
|
|
194
|
+
selection = self.services["optimizer"].optimize(
|
|
195
|
+
request.query, retrieved.memories, ContextBudget(max_tokens=request.budget)
|
|
196
|
+
)
|
|
197
|
+
optimizer_ms = (time.perf_counter() - optimizer_started) * 1000
|
|
198
|
+
compilation_started = time.perf_counter()
|
|
199
|
+
compiled = await self.services["compilation"].compile(
|
|
200
|
+
request.query, selection, CompilationConfig(budget=request.budget)
|
|
201
|
+
)
|
|
202
|
+
compilation_ms = (time.perf_counter() - compilation_started) * 1000
|
|
203
|
+
pipeline_ms = retrieved.trace.total_latency_ms + optimizer_ms + compilation_ms
|
|
204
|
+
|
|
205
|
+
trace = await self.build_trace(
|
|
206
|
+
request=request,
|
|
207
|
+
retrieval_request=retrieval_request,
|
|
208
|
+
retrieved=retrieved,
|
|
209
|
+
selection=selection,
|
|
210
|
+
compiled=compiled,
|
|
211
|
+
dispatch_evidence=None,
|
|
212
|
+
started_at=started,
|
|
213
|
+
pipeline_ms=pipeline_ms,
|
|
214
|
+
)
|
|
215
|
+
return trace, retrieved, selection, compiled
|
|
216
|
+
|
|
217
|
+
async def build_trace(
|
|
218
|
+
self,
|
|
219
|
+
request: ExplanationRequest,
|
|
220
|
+
retrieved: RetrievalResult,
|
|
221
|
+
selection: Any,
|
|
222
|
+
compiled: Any,
|
|
223
|
+
retrieval_request: RetrievalQuery | None = None,
|
|
224
|
+
dispatch_evidence: ProviderDispatchEvidence | None = None,
|
|
225
|
+
started_at: float | None = None,
|
|
226
|
+
pipeline_ms: float | None = None,
|
|
227
|
+
) -> ExplanationTrace:
|
|
228
|
+
trace_id = str(uuid4())
|
|
229
|
+
if retrieval_request is None:
|
|
230
|
+
retrieval_request = RetrievalQuery(
|
|
231
|
+
text=request.query,
|
|
232
|
+
mode=request.mode,
|
|
233
|
+
k=request.limit,
|
|
234
|
+
include_trace=True,
|
|
235
|
+
temporal_scope=request.temporal_scope,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
decisions = {item.memory_id: item for item in selection.trace.decisions}
|
|
239
|
+
source_content = {str(item.memory.id): item.memory.content for item in retrieved.memories}
|
|
240
|
+
compiled_memory_ids = {str(value) for value in compiled.included_memory_ids}
|
|
241
|
+
facts_by_memory: dict[str, list[dict]] = {}
|
|
242
|
+
for fact in compiled.facts[:200]:
|
|
243
|
+
for memory_id in fact.source_memory_ids:
|
|
244
|
+
facts_by_memory.setdefault(str(memory_id), []).append({
|
|
245
|
+
"fact_id": fact.fact_id,
|
|
246
|
+
"action": (
|
|
247
|
+
"MERGED" if len(fact.source_memory_ids) > 1 else
|
|
248
|
+
"RESCUED_FROM_OVERSIZED_MEMORY" if fact.input_kind.value == "oversized_rescue" else
|
|
249
|
+
"RAW_INCLUDED" if fact.text == source_content.get(str(memory_id)) else "UNKNOWN"
|
|
250
|
+
),
|
|
251
|
+
"token_cost": fact.token_cost,
|
|
252
|
+
"source_memory_ids": [str(value) for value in fact.source_memory_ids[:20]],
|
|
253
|
+
"provenance_event_ids": [str(value) for value in fact.provenance_event_ids[:20]],
|
|
254
|
+
"provenance_preserved": bool(fact.source_memory_ids),
|
|
255
|
+
})
|
|
256
|
+
for fact in compiled.excluded_facts[:200]:
|
|
257
|
+
for memory_id in fact.source_memory_ids:
|
|
258
|
+
facts_by_memory.setdefault(str(memory_id), []).append({
|
|
259
|
+
"fact_id": fact.fact_id,
|
|
260
|
+
"action": "DEDUPLICATED" if fact.reason.value == "duplicate" else "DROPPED",
|
|
261
|
+
"reason": fact.reason.value,
|
|
262
|
+
"token_cost": fact.token_cost,
|
|
263
|
+
"source_memory_ids": [str(value) for value in fact.source_memory_ids[:20]],
|
|
264
|
+
"provenance_event_ids": [],
|
|
265
|
+
"provenance_preserved": bool(fact.source_memory_ids),
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
candidate_rows: list[dict] = []
|
|
269
|
+
excluded: list[dict] = []
|
|
270
|
+
mode = retrieval_request.mode
|
|
271
|
+
|
|
272
|
+
for item in retrieved.memories[:request.limit]:
|
|
273
|
+
memory = item.memory
|
|
274
|
+
key = str(memory.id)
|
|
275
|
+
decision = decisions.get(memory.id)
|
|
276
|
+
selected = bool(decision and decision.selected)
|
|
277
|
+
|
|
278
|
+
graph_paths_data = []
|
|
279
|
+
for path in item.graph_paths[:5]:
|
|
280
|
+
p_nodes = [
|
|
281
|
+
{
|
|
282
|
+
"node_id": str(n.node_id),
|
|
283
|
+
"node_type": n.node_type.value,
|
|
284
|
+
"label": safe_text(n.label, 120) if n.label else None,
|
|
285
|
+
"project_scope": safe_text(n.project_scope, 64) if n.project_scope else None,
|
|
286
|
+
}
|
|
287
|
+
for n in getattr(path, "path_nodes", [])[:4]
|
|
288
|
+
]
|
|
289
|
+
p_edges = [
|
|
290
|
+
{
|
|
291
|
+
"edge_type": e.edge_type.value,
|
|
292
|
+
"confidence": e.confidence,
|
|
293
|
+
"supporting_memory_ids": [str(x) for x in e.supporting_memory_ids[:20]],
|
|
294
|
+
"project_scope": safe_text(e.project_scope, 64) if e.project_scope else None,
|
|
295
|
+
}
|
|
296
|
+
for e in getattr(path, "path_edges", [])[:3]
|
|
297
|
+
]
|
|
298
|
+
graph_paths_data.append({
|
|
299
|
+
"seed_node_ids": [str(value) for value in path.seed_node_ids[:20]],
|
|
300
|
+
"node_ids": [str(value) for value in path.node_ids[:4]],
|
|
301
|
+
"node_types": [value.value for value in path.node_types[:4]],
|
|
302
|
+
"hop_count": path.hop_count,
|
|
303
|
+
"relation_types": [value.value for value in path.edge_types[:3]],
|
|
304
|
+
"supporting_memory_ids": [str(value) for value in path.source_memory_ids[:20]],
|
|
305
|
+
"score": path.graph_contribution,
|
|
306
|
+
"path_nodes": p_nodes,
|
|
307
|
+
"path_edges": p_edges,
|
|
308
|
+
"scope_match": getattr(path, "scope_match", None),
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
temporal_data = await self.temporal_resolver.resolve_memory_evidence(memory, retrieval_request)
|
|
312
|
+
|
|
313
|
+
row = {
|
|
314
|
+
"memory_id": key,
|
|
315
|
+
"rank": item.rank,
|
|
316
|
+
"status": memory.status.value,
|
|
317
|
+
"confidence": memory.confidence,
|
|
318
|
+
"importance": memory.importance,
|
|
319
|
+
"token_cost": decision.token_cost if decision else None,
|
|
320
|
+
"content_tokens": decision.content_tokens if decision else None,
|
|
321
|
+
"retrieval": {
|
|
322
|
+
"origin": (
|
|
323
|
+
"graph_expanded"
|
|
324
|
+
if "graph" in item.retrieval_sources and not ("lexical" in item.retrieval_sources or "dense" in item.retrieval_sources)
|
|
325
|
+
else "direct"
|
|
326
|
+
),
|
|
327
|
+
"mode": mode.value,
|
|
328
|
+
"lexical_rank": item.lexical_rank,
|
|
329
|
+
"lexical_bm25_score": item.bm25_score,
|
|
330
|
+
"dense_rank": item.dense_rank,
|
|
331
|
+
"dense_score": item.vector_score,
|
|
332
|
+
"lexical_rrf_contribution": (
|
|
333
|
+
((1.0 / (60 + item.lexical_rank)) if item.lexical_rank else 0.0)
|
|
334
|
+
if mode not in {RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH} else None
|
|
335
|
+
),
|
|
336
|
+
"dense_rrf_contribution": (
|
|
337
|
+
((1.0 / (60 + item.dense_rank)) if item.dense_rank else 0.0)
|
|
338
|
+
if mode not in {RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH} else None
|
|
339
|
+
),
|
|
340
|
+
"base_rank": item.rrf_rank if mode in {RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH} else None,
|
|
341
|
+
"base_rrf_contribution": (1.0 / (60 + item.rrf_rank)) if item.rrf_rank else None,
|
|
342
|
+
"graph_rrf_contribution": (1.0 / (60 + item.graph_rank)) if item.graph_rank else 0.0,
|
|
343
|
+
"graph_score": item.graph_score,
|
|
344
|
+
"graph_rank": item.graph_rank,
|
|
345
|
+
"fused_score": item.final_score,
|
|
346
|
+
"metadata_adjustment": item.metadata_adjustment,
|
|
347
|
+
"sources": list(item.retrieval_sources),
|
|
348
|
+
},
|
|
349
|
+
"graph": graph_paths_data,
|
|
350
|
+
"temporal": temporal_data,
|
|
351
|
+
"optimizer": {
|
|
352
|
+
"selected": decision.selected if decision else False,
|
|
353
|
+
"reason": (
|
|
354
|
+
decision.exclusion_reason.value
|
|
355
|
+
if decision and decision.exclusion_reason
|
|
356
|
+
else ("selected" if selected else "not_available")
|
|
357
|
+
),
|
|
358
|
+
"utility": decision.marginal_utility if decision else None,
|
|
359
|
+
"redundancy": decision.redundancy if decision else None,
|
|
360
|
+
"redundant_with": str(decision.redundant_with) if decision and decision.redundant_with else None,
|
|
361
|
+
"budget": selection.trace.available_tokens,
|
|
362
|
+
"decision": decision.model_dump(mode="json") if decision else None,
|
|
363
|
+
},
|
|
364
|
+
"compiler": facts_by_memory.get(key, []),
|
|
365
|
+
"compiler_transformations": [fact["action"] for fact in facts_by_memory.get(key, [])],
|
|
366
|
+
"selected": selected,
|
|
367
|
+
"compiler_included": key in compiled_memory_ids,
|
|
368
|
+
"provenance": {
|
|
369
|
+
"source_type": safe_source_type(memory.source_type),
|
|
370
|
+
"event_id": str(memory.provenance_event_id) if memory.provenance_event_id else None,
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
if request.include_content:
|
|
374
|
+
row["content"] = safe_text(memory.content, 1000)
|
|
375
|
+
candidate_rows.append(row)
|
|
376
|
+
if not selected:
|
|
377
|
+
excluded.append({"memory_id": key, "reason": row["optimizer"]["reason"]})
|
|
378
|
+
|
|
379
|
+
selected_ids = [str(item.memory.id) for item in selection.selected_memories[:request.limit]]
|
|
380
|
+
|
|
381
|
+
requested_memory = None
|
|
382
|
+
if request.target_memory_id is not None:
|
|
383
|
+
requested_id = str(request.target_memory_id)
|
|
384
|
+
matched = next((row for row in candidate_rows if row["memory_id"] == requested_id), None)
|
|
385
|
+
if matched is not None:
|
|
386
|
+
if matched["selected"]:
|
|
387
|
+
if requested_id in compiled_memory_ids:
|
|
388
|
+
requested_memory = {
|
|
389
|
+
"memory_id": requested_id,
|
|
390
|
+
"status": "retrieved_and_selected",
|
|
391
|
+
"reason_code": "SELECTED",
|
|
392
|
+
"reason": "selected_and_compiled",
|
|
393
|
+
}
|
|
394
|
+
else:
|
|
395
|
+
requested_memory = {
|
|
396
|
+
"memory_id": requested_id,
|
|
397
|
+
"status": "optimizer_selected_not_compiled",
|
|
398
|
+
"reason_code": "COMPILER_EXCLUDED",
|
|
399
|
+
"reason": "not_in_compiled_context",
|
|
400
|
+
}
|
|
401
|
+
else:
|
|
402
|
+
requested_memory = {
|
|
403
|
+
"memory_id": requested_id,
|
|
404
|
+
"status": "retrieved_but_excluded",
|
|
405
|
+
"reason_code": "RETRIEVED_BUT_OPTIMIZER_EXCLUDED",
|
|
406
|
+
"reason": matched["optimizer"]["reason"],
|
|
407
|
+
}
|
|
408
|
+
elif requested_id in getattr(retrieved.trace, "pre_limit_candidate_ids", []):
|
|
409
|
+
requested_memory = {
|
|
410
|
+
"memory_id": requested_id,
|
|
411
|
+
"status": "retrieval_excluded",
|
|
412
|
+
"reason_code": "RESULT_LIMIT",
|
|
413
|
+
"reason": "result_limit_exceeded",
|
|
414
|
+
}
|
|
415
|
+
elif (
|
|
416
|
+
getattr(retrieved.trace, "pre_limit_candidates_truncated", False)
|
|
417
|
+
or getattr(retrieved.trace, "channel_candidates_truncated", False)
|
|
418
|
+
):
|
|
419
|
+
requested_memory = {
|
|
420
|
+
"memory_id": requested_id,
|
|
421
|
+
"status": "not_available",
|
|
422
|
+
"reason_code": "NOT_AVAILABLE",
|
|
423
|
+
"reason": "candidate_evidence_truncated",
|
|
424
|
+
}
|
|
425
|
+
else:
|
|
426
|
+
repo = self.services.get("memory_repo")
|
|
427
|
+
memory = await repo.get(request.target_memory_id) if repo is not None else None
|
|
428
|
+
if memory is None:
|
|
429
|
+
requested_memory = {
|
|
430
|
+
"memory_id": requested_id,
|
|
431
|
+
"status": "not_available",
|
|
432
|
+
"reason_code": "NOT_AVAILABLE",
|
|
433
|
+
"reason": "not_retrieved_or_not_observed",
|
|
434
|
+
}
|
|
435
|
+
elif not HybridRetrievalEngine._eligible(memory, retrieval_request):
|
|
436
|
+
requested_memory = {
|
|
437
|
+
"memory_id": requested_id,
|
|
438
|
+
"status": "not_retrieved",
|
|
439
|
+
"reason_code": "TEMPORALLY_INELIGIBLE",
|
|
440
|
+
"reason": "temporal_policy_ineligible",
|
|
441
|
+
"lifecycle": memory.status.value,
|
|
442
|
+
"temporal_status": memory.temporal_status.value,
|
|
443
|
+
}
|
|
444
|
+
else:
|
|
445
|
+
lexical_idx = self.services.get("lexical_index") or self.services.get("bm25_index")
|
|
446
|
+
vector_st = self.services.get("vector_store")
|
|
447
|
+
in_lexical = bool(
|
|
448
|
+
lexical_idx and (
|
|
449
|
+
lexical_idx.contains(requested_id)
|
|
450
|
+
if hasattr(lexical_idx, "contains")
|
|
451
|
+
else (requested_id in getattr(lexical_idx, "_documents", {}))
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
in_vector = bool(
|
|
455
|
+
vector_st and (
|
|
456
|
+
vector_st.contains(requested_id)
|
|
457
|
+
if hasattr(vector_st, "contains")
|
|
458
|
+
else (requested_id in getattr(vector_st, "_ids", []))
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
if (
|
|
462
|
+
not in_lexical and not in_vector
|
|
463
|
+
and retrieval_request.mode not in {
|
|
464
|
+
RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH,
|
|
465
|
+
}
|
|
466
|
+
):
|
|
467
|
+
requested_memory = {
|
|
468
|
+
"memory_id": requested_id,
|
|
469
|
+
"status": "not_retrieved",
|
|
470
|
+
"reason_code": "INDEX_NOT_PRESENT",
|
|
471
|
+
"reason": "absent_from_retrieval_index",
|
|
472
|
+
}
|
|
473
|
+
elif retrieval_request.mode in {
|
|
474
|
+
RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH,
|
|
475
|
+
} and not in_lexical and not in_vector:
|
|
476
|
+
requested_memory = {
|
|
477
|
+
"memory_id": requested_id,
|
|
478
|
+
"status": "not_available",
|
|
479
|
+
"reason_code": "NOT_AVAILABLE",
|
|
480
|
+
"reason": "graph_channel_membership_not_proven",
|
|
481
|
+
}
|
|
482
|
+
else:
|
|
483
|
+
requested_memory = {
|
|
484
|
+
"memory_id": requested_id,
|
|
485
|
+
"status": "not_retrieved",
|
|
486
|
+
"reason_code": "CHANNEL_NOT_RETRIEVED",
|
|
487
|
+
"reason": "channel_not_retrieved",
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
provider_dispatch_data: dict[str, Any]
|
|
491
|
+
if dispatch_evidence is not None:
|
|
492
|
+
provider_dispatch_data = dispatch_evidence.model_dump(mode="json")
|
|
493
|
+
else:
|
|
494
|
+
provider_dispatch_data = {
|
|
495
|
+
"state": "NOT_ATTEMPTED",
|
|
496
|
+
"status_message": "prepared by ContextOS; no provider dispatch attempted",
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
measured_pipeline = pipeline_ms if pipeline_ms is not None else (
|
|
500
|
+
retrieved.trace.total_latency_ms + selection.trace.latency_ms + getattr(compiled, "total_tokens", 0)
|
|
501
|
+
)
|
|
502
|
+
execution_ms = (time.perf_counter() - started_at) * 1000 if started_at else 0.0
|
|
503
|
+
|
|
504
|
+
return ExplanationTrace(
|
|
505
|
+
trace_id=trace_id,
|
|
506
|
+
query_id=trace_id,
|
|
507
|
+
strategy=mode.value,
|
|
508
|
+
stages=[
|
|
509
|
+
{"name": safe_text(stage.stage_name, 64), "input_count": stage.input_count,
|
|
510
|
+
"output_count": stage.output_count, "latency_ms": stage.latency_ms}
|
|
511
|
+
for stage in retrieved.trace.stages[:20]
|
|
512
|
+
] + [
|
|
513
|
+
{"name": "optimizer", "input_count": selection.trace.candidate_count,
|
|
514
|
+
"output_count": selection.trace.selected_count,
|
|
515
|
+
"latency_ms": selection.trace.latency_ms,
|
|
516
|
+
"tokens_used": selection.trace.tokens_used,
|
|
517
|
+
"token_budget": selection.trace.available_tokens},
|
|
518
|
+
*[{"name": safe_text(stage.stage_name, 64), "input_count": stage.input_count,
|
|
519
|
+
"output_count": stage.output_count, "latency_ms": stage.latency_ms,
|
|
520
|
+
"input_tokens": stage.input_tokens, "output_tokens": stage.output_tokens}
|
|
521
|
+
for stage in compiled.trace.stages[:15]],
|
|
522
|
+
][:20],
|
|
523
|
+
candidates=candidate_rows,
|
|
524
|
+
selected=selected_ids,
|
|
525
|
+
excluded=excluded[:request.limit],
|
|
526
|
+
final_context={
|
|
527
|
+
"candidate_count": retrieved.trace.total_candidates,
|
|
528
|
+
"candidate_tokens": sum(row["content_tokens"] or 0 for row in candidate_rows),
|
|
529
|
+
"optimized_tokens": selection.total_tokens,
|
|
530
|
+
"compiled_tokens": compiled.total_tokens,
|
|
531
|
+
"token_budget": compiled.budget,
|
|
532
|
+
"token_measurement_source": (
|
|
533
|
+
self.token_counter.measurement_source.value if self.token_counter else "unknown"
|
|
534
|
+
),
|
|
535
|
+
"tokenizer": self.token_counter.encoding_name if self.token_counter else "unknown",
|
|
536
|
+
"facts_emitted": len(compiled.facts),
|
|
537
|
+
"memories_contributing": [str(value) for value in compiled.included_memory_ids[:request.limit]],
|
|
538
|
+
"graph_contribution_count": sum(row["retrieval"]["origin"] == "graph_expanded" for row in candidate_rows),
|
|
539
|
+
"temporal_filter_count": next(
|
|
540
|
+
(stage.input_count - stage.output_count
|
|
541
|
+
for stage in retrieved.trace.stages
|
|
542
|
+
if stage.stage_name == "eligibility_filter"),
|
|
543
|
+
0,
|
|
544
|
+
),
|
|
545
|
+
},
|
|
546
|
+
content=compiled.context_text[:20_000] if request.include_content else None,
|
|
547
|
+
evidence_scope="retrieved candidates only",
|
|
548
|
+
requested_memory=requested_memory,
|
|
549
|
+
provider_dispatch=provider_dispatch_data,
|
|
550
|
+
execution_ms=execution_ms,
|
|
551
|
+
measured_pipeline_ms=measured_pipeline,
|
|
552
|
+
explanation_overhead_ms=max(0.0, execution_ms - measured_pipeline),
|
|
553
|
+
)
|