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,409 @@
|
|
|
1
|
+
"""ContextOS Model Service — high-level unified model execution orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from contextos.core.enums import (
|
|
12
|
+
ModelFinishReason,
|
|
13
|
+
ProviderDispatchState,
|
|
14
|
+
RetrievalMode,
|
|
15
|
+
RoutingPolicy,
|
|
16
|
+
TemporalScope,
|
|
17
|
+
TokenMeasurementSource,
|
|
18
|
+
)
|
|
19
|
+
from contextos.core.models import (
|
|
20
|
+
AskResult,
|
|
21
|
+
CompilationConfig,
|
|
22
|
+
CompiledContext,
|
|
23
|
+
ContextBudget,
|
|
24
|
+
ModelInvocationTelemetry,
|
|
25
|
+
ModelRequest,
|
|
26
|
+
ModelResponse,
|
|
27
|
+
ProviderDispatchEvidence,
|
|
28
|
+
RetrievalConfig,
|
|
29
|
+
RetrievalQuery,
|
|
30
|
+
RouteDecision,
|
|
31
|
+
)
|
|
32
|
+
from contextos.core.protocols import (
|
|
33
|
+
CompilationService,
|
|
34
|
+
ModelProvider,
|
|
35
|
+
ModelRouter,
|
|
36
|
+
RetrievalService,
|
|
37
|
+
TelemetryRepository,
|
|
38
|
+
TokenAwareOptimizer,
|
|
39
|
+
TokenCounter,
|
|
40
|
+
)
|
|
41
|
+
from contextos.services.token_counter import get_token_counter_for_model
|
|
42
|
+
from contextos.storage.telemetry_repo import sanitize_telemetry_metadata
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ContextOSModelService:
|
|
48
|
+
"""Orchestrates ContextOS context preparation, model routing, and invocation telemetry.
|
|
49
|
+
|
|
50
|
+
ContextOS prepares, compresses, routes, measures, and explains context.
|
|
51
|
+
The model adapter is downstream.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
retrieval_service: RetrievalService,
|
|
57
|
+
optimizer: TokenAwareOptimizer,
|
|
58
|
+
compilation_service: CompilationService,
|
|
59
|
+
router: ModelRouter,
|
|
60
|
+
providers: dict[str, ModelProvider],
|
|
61
|
+
telemetry_repo: TelemetryRepository,
|
|
62
|
+
token_counter: TokenCounter,
|
|
63
|
+
explainability_service: Any = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._retrieval = retrieval_service
|
|
66
|
+
self._optimizer = optimizer
|
|
67
|
+
self._compilation = compilation_service
|
|
68
|
+
self._router = router
|
|
69
|
+
self._providers = dict(providers)
|
|
70
|
+
self._telemetry = telemetry_repo
|
|
71
|
+
self._token_counter = token_counter
|
|
72
|
+
self._explainability = explainability_service
|
|
73
|
+
|
|
74
|
+
def set_explainability_service(self, explainability_service: Any) -> None:
|
|
75
|
+
self._explainability = explainability_service
|
|
76
|
+
|
|
77
|
+
def register_provider(self, provider: ModelProvider) -> None:
|
|
78
|
+
"""Register or replace a provider adapter."""
|
|
79
|
+
self._providers[provider.provider_id] = provider
|
|
80
|
+
|
|
81
|
+
def get_provider(self, provider_id: str) -> ModelProvider | None:
|
|
82
|
+
return self._providers.get(provider_id)
|
|
83
|
+
|
|
84
|
+
def list_providers(self) -> list[ModelProvider]:
|
|
85
|
+
return list(self._providers.values())
|
|
86
|
+
|
|
87
|
+
async def ask(
|
|
88
|
+
self,
|
|
89
|
+
query: str,
|
|
90
|
+
system_prompt: str | None = None,
|
|
91
|
+
retrieval_config: RetrievalConfig | None = None,
|
|
92
|
+
compilation_config: CompilationConfig | None = None,
|
|
93
|
+
routing_policy: RoutingPolicy | None = None,
|
|
94
|
+
target_provider: str | None = None,
|
|
95
|
+
target_model: str | None = None,
|
|
96
|
+
allow_fallback: bool = False,
|
|
97
|
+
temperature: float = 0.7,
|
|
98
|
+
max_output_tokens: int = 1024,
|
|
99
|
+
timeout_seconds: float = 30.0,
|
|
100
|
+
required_capabilities: list[str] | None = None,
|
|
101
|
+
session_id: str | None = None,
|
|
102
|
+
metadata: dict[str, Any] | None = None,
|
|
103
|
+
explain: bool = False,
|
|
104
|
+
) -> AskResult:
|
|
105
|
+
"""Execute end-to-end query -> retrieval -> optimization -> compilation -> routing -> generation -> telemetry."""
|
|
106
|
+
total_started = time.perf_counter()
|
|
107
|
+
invocation_id = uuid4()
|
|
108
|
+
req_metadata = metadata or {}
|
|
109
|
+
|
|
110
|
+
# -------------------------------------------------------------------
|
|
111
|
+
# 1. Retrieval (Hybrid + Graph + Temporal)
|
|
112
|
+
# -------------------------------------------------------------------
|
|
113
|
+
retrieval_result = await self._retrieval.retrieve(query, retrieval_config)
|
|
114
|
+
retrieval_ms = retrieval_result.trace.total_latency_ms
|
|
115
|
+
|
|
116
|
+
raw_candidates = retrieval_result.memories
|
|
117
|
+
candidate_context_tokens = sum(
|
|
118
|
+
self._token_counter.count(item.memory.content) for item in raw_candidates
|
|
119
|
+
)
|
|
120
|
+
lexical_candidate_count = len(retrieval_result.strategy_results.get("lexical", []))
|
|
121
|
+
dense_candidate_count = len(retrieval_result.strategy_results.get("dense", []))
|
|
122
|
+
graph_expanded_count = len(retrieval_result.strategy_results.get("graph", []))
|
|
123
|
+
hybrid_candidate_count = len(raw_candidates)
|
|
124
|
+
|
|
125
|
+
# Count temporal filter exclusions from retrieval stage trace if present
|
|
126
|
+
temporal_filtered_count = 0
|
|
127
|
+
for st in retrieval_result.trace.stages:
|
|
128
|
+
if st.stage_name == "eligibility_filter":
|
|
129
|
+
temporal_filtered_count = max(0, st.input_count - st.output_count)
|
|
130
|
+
|
|
131
|
+
# -------------------------------------------------------------------
|
|
132
|
+
# 2. Token-Aware Whole-Memory Optimization
|
|
133
|
+
# -------------------------------------------------------------------
|
|
134
|
+
comp_cfg = compilation_config or CompilationConfig()
|
|
135
|
+
opt_started = time.perf_counter()
|
|
136
|
+
selection = self._optimizer.optimize(
|
|
137
|
+
query=query,
|
|
138
|
+
candidates=raw_candidates,
|
|
139
|
+
budget=ContextBudget(max_tokens=comp_cfg.budget),
|
|
140
|
+
)
|
|
141
|
+
optimization_ms = (time.perf_counter() - opt_started) * 1000.0
|
|
142
|
+
optimized_context_tokens = selection.total_tokens
|
|
143
|
+
selected_memory_count = len(selection.selected_memories)
|
|
144
|
+
|
|
145
|
+
# -------------------------------------------------------------------
|
|
146
|
+
# 3. Query-Aware Fact Context Compilation
|
|
147
|
+
# -------------------------------------------------------------------
|
|
148
|
+
comp_started = time.perf_counter()
|
|
149
|
+
compiled_context = await self._compilation.compile(
|
|
150
|
+
query=query,
|
|
151
|
+
memories=selection,
|
|
152
|
+
config=comp_cfg,
|
|
153
|
+
)
|
|
154
|
+
compilation_ms = (time.perf_counter() - comp_started) * 1000.0
|
|
155
|
+
compiled_context_tokens = compiled_context.total_tokens
|
|
156
|
+
compiled_fact_count = len(compiled_context.facts)
|
|
157
|
+
|
|
158
|
+
# -------------------------------------------------------------------
|
|
159
|
+
# 4. Routing Decision
|
|
160
|
+
# -------------------------------------------------------------------
|
|
161
|
+
model_req = ModelRequest(
|
|
162
|
+
user_prompt=query,
|
|
163
|
+
model=target_model,
|
|
164
|
+
provider=target_provider,
|
|
165
|
+
system_prompt=system_prompt,
|
|
166
|
+
compiled_context=compiled_context,
|
|
167
|
+
temperature=temperature,
|
|
168
|
+
max_output_tokens=max_output_tokens,
|
|
169
|
+
timeout_seconds=timeout_seconds,
|
|
170
|
+
routing_policy=routing_policy,
|
|
171
|
+
allow_fallback=allow_fallback,
|
|
172
|
+
required_capabilities=required_capabilities or [],
|
|
173
|
+
metadata=req_metadata,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
route_decision = await self._router.route(
|
|
177
|
+
request=model_req,
|
|
178
|
+
providers=self._providers,
|
|
179
|
+
policy=routing_policy,
|
|
180
|
+
)
|
|
181
|
+
routing_ms = route_decision.routing_latency_ms
|
|
182
|
+
chosen_provider = self._providers[route_decision.selected_provider]
|
|
183
|
+
|
|
184
|
+
# -------------------------------------------------------------------
|
|
185
|
+
# 5. Target-Model Token Counting
|
|
186
|
+
# -------------------------------------------------------------------
|
|
187
|
+
tc_started = time.perf_counter()
|
|
188
|
+
target_counter = get_token_counter_for_model(route_decision.selected_model)
|
|
189
|
+
prompt_tokens_before = target_counter.count(query)
|
|
190
|
+
if system_prompt:
|
|
191
|
+
prompt_tokens_before += target_counter.count(system_prompt)
|
|
192
|
+
|
|
193
|
+
# Target-model specific recount: ensures candidate, optimized, compiled,
|
|
194
|
+
# avoided, and reduction ratio all share the exact same tokenization basis.
|
|
195
|
+
target_candidate_tokens = sum(
|
|
196
|
+
target_counter.count(item.memory.content) for item in raw_candidates
|
|
197
|
+
)
|
|
198
|
+
target_optimized_tokens = sum(
|
|
199
|
+
target_counter.count(mem.memory.content) for mem in selection.selected_memories
|
|
200
|
+
)
|
|
201
|
+
target_compiled_tokens = (
|
|
202
|
+
target_counter.count(compiled_context.context_text)
|
|
203
|
+
if compiled_context.context_text
|
|
204
|
+
else 0
|
|
205
|
+
)
|
|
206
|
+
final_input_tokens = prompt_tokens_before + target_compiled_tokens
|
|
207
|
+
preflight_input_tokens = final_input_tokens
|
|
208
|
+
token_counting_ms = (time.perf_counter() - tc_started) * 1000.0
|
|
209
|
+
|
|
210
|
+
# Update model request with chosen model and invoke
|
|
211
|
+
model_req.model = route_decision.selected_model
|
|
212
|
+
model_req.provider = route_decision.selected_provider
|
|
213
|
+
|
|
214
|
+
# -------------------------------------------------------------------
|
|
215
|
+
# 5b. Request Construction & Fingerprinting (Provider Dispatch Receipt)
|
|
216
|
+
# -------------------------------------------------------------------
|
|
217
|
+
context_str = compiled_context.context_text or ""
|
|
218
|
+
compiled_context_sha256 = hashlib.sha256(context_str.encode("utf-8")).hexdigest()
|
|
219
|
+
# This is the actual ModelRequest handed to the provider adapter, not its
|
|
220
|
+
# provider-specific HTTP payload (which each adapter constructs later).
|
|
221
|
+
actual_req_context = (
|
|
222
|
+
model_req.compiled_context.context_text
|
|
223
|
+
if model_req.compiled_context and model_req.compiled_context.context_text
|
|
224
|
+
else ""
|
|
225
|
+
)
|
|
226
|
+
context_match = bool(
|
|
227
|
+
(context_str == actual_req_context)
|
|
228
|
+
and (context_str == "" or context_str in actual_req_context)
|
|
229
|
+
)
|
|
230
|
+
compiled_context_in_request = context_match
|
|
231
|
+
request_payload_representation = (
|
|
232
|
+
f"system:{model_req.system_prompt or ''}\n"
|
|
233
|
+
f"context:{actual_req_context}\n"
|
|
234
|
+
f"prompt:{model_req.user_prompt}"
|
|
235
|
+
)
|
|
236
|
+
logical_request_sha256 = hashlib.sha256(
|
|
237
|
+
request_payload_representation.encode("utf-8")
|
|
238
|
+
).hexdigest()
|
|
239
|
+
|
|
240
|
+
dispatch_evidence = ProviderDispatchEvidence(
|
|
241
|
+
provider_id=chosen_provider.provider_id,
|
|
242
|
+
model_id=route_decision.selected_model,
|
|
243
|
+
state=ProviderDispatchState.REQUEST_CONSTRUCTED,
|
|
244
|
+
compiled_context_sha256=compiled_context_sha256,
|
|
245
|
+
logical_request_sha256=logical_request_sha256,
|
|
246
|
+
compiled_context_in_request=compiled_context_in_request,
|
|
247
|
+
preflight_input_tokens=preflight_input_tokens,
|
|
248
|
+
provider_input_tokens=None,
|
|
249
|
+
provider_response_received=False,
|
|
250
|
+
measurement_source=None,
|
|
251
|
+
context_match=context_match,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# -------------------------------------------------------------------
|
|
255
|
+
# 6. Downstream Provider Generation
|
|
256
|
+
# -------------------------------------------------------------------
|
|
257
|
+
dispatch_evidence.state = ProviderDispatchState.DISPATCH_ATTEMPTED
|
|
258
|
+
gen_exc: Exception | None = None
|
|
259
|
+
try:
|
|
260
|
+
response = await chosen_provider.generate(model_req)
|
|
261
|
+
if response.finish_reason == ModelFinishReason.ERROR or response.error:
|
|
262
|
+
dispatch_evidence.state = ProviderDispatchState.DISPATCH_FAILED
|
|
263
|
+
dispatch_evidence.provider_response_received = False
|
|
264
|
+
else:
|
|
265
|
+
dispatch_evidence.state = ProviderDispatchState.RESPONSE_RECEIVED
|
|
266
|
+
dispatch_evidence.provider_response_received = True
|
|
267
|
+
dispatch_evidence.provider_input_tokens = response.input_tokens
|
|
268
|
+
dispatch_evidence.measurement_source = (
|
|
269
|
+
response.token_measurement_source.value if response.token_measurement_source else None
|
|
270
|
+
)
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
gen_exc = exc
|
|
273
|
+
dispatch_evidence.state = ProviderDispatchState.DISPATCH_FAILED
|
|
274
|
+
dispatch_evidence.provider_response_received = False
|
|
275
|
+
# Synthesize safe failure response for failure telemetry persistence
|
|
276
|
+
response = ModelResponse(
|
|
277
|
+
text="",
|
|
278
|
+
model_id=route_decision.selected_model,
|
|
279
|
+
provider_id=chosen_provider.provider_id,
|
|
280
|
+
input_tokens=0,
|
|
281
|
+
output_tokens=0,
|
|
282
|
+
total_tokens=0,
|
|
283
|
+
latency_ms=(time.perf_counter() - total_started) * 1000.0,
|
|
284
|
+
finish_reason=ModelFinishReason.ERROR,
|
|
285
|
+
error=exc.__class__.__name__,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
provider_latency_ms = response.latency_ms
|
|
289
|
+
end_to_end_ms = (time.perf_counter() - total_started) * 1000.0
|
|
290
|
+
|
|
291
|
+
# -------------------------------------------------------------------
|
|
292
|
+
# 7. Savings and Reduction Calculation (Model-Specific Basis)
|
|
293
|
+
# -------------------------------------------------------------------
|
|
294
|
+
tokens_avoided = max(0, target_candidate_tokens - target_compiled_tokens)
|
|
295
|
+
reduction_ratio = (
|
|
296
|
+
max(0.0, 1.0 - (target_compiled_tokens / target_candidate_tokens))
|
|
297
|
+
if target_candidate_tokens > 0
|
|
298
|
+
else 0.0
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# -------------------------------------------------------------------
|
|
302
|
+
# 8. Telemetry Recording
|
|
303
|
+
# -------------------------------------------------------------------
|
|
304
|
+
safe_meta = sanitize_telemetry_metadata({
|
|
305
|
+
"request_id": response.request_id,
|
|
306
|
+
"strategy": compiled_context.strategy.value,
|
|
307
|
+
**(req_metadata or {}),
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
telemetry = ModelInvocationTelemetry(
|
|
311
|
+
invocation_id=invocation_id,
|
|
312
|
+
session_id=session_id,
|
|
313
|
+
provider_id=chosen_provider.provider_id,
|
|
314
|
+
model_id=route_decision.selected_model,
|
|
315
|
+
is_local=chosen_provider.is_local,
|
|
316
|
+
candidate_context_tokens=target_candidate_tokens,
|
|
317
|
+
retrieved_context_tokens=target_candidate_tokens,
|
|
318
|
+
optimized_context_tokens=target_optimized_tokens,
|
|
319
|
+
compiled_context_tokens=target_compiled_tokens,
|
|
320
|
+
prompt_tokens_before_context=prompt_tokens_before,
|
|
321
|
+
preflight_input_tokens=preflight_input_tokens,
|
|
322
|
+
final_input_tokens=final_input_tokens,
|
|
323
|
+
provider_input_tokens=response.input_tokens,
|
|
324
|
+
provider_output_tokens=response.output_tokens,
|
|
325
|
+
provider_total_tokens=response.total_tokens or (response.input_tokens + response.output_tokens),
|
|
326
|
+
token_measurement_source=response.token_measurement_source,
|
|
327
|
+
context_token_measurement_source=target_counter.measurement_source,
|
|
328
|
+
context_tokenizer=target_counter.encoding_name,
|
|
329
|
+
context_tokens_avoided=tokens_avoided,
|
|
330
|
+
reduction_ratio=reduction_ratio,
|
|
331
|
+
lexical_candidate_count=lexical_candidate_count,
|
|
332
|
+
dense_candidate_count=dense_candidate_count,
|
|
333
|
+
hybrid_candidate_count=hybrid_candidate_count,
|
|
334
|
+
graph_expanded_count=graph_expanded_count,
|
|
335
|
+
temporal_filtered_count=temporal_filtered_count,
|
|
336
|
+
selected_memory_count=selected_memory_count,
|
|
337
|
+
compiled_fact_count=compiled_fact_count,
|
|
338
|
+
retrieval_ms=retrieval_ms,
|
|
339
|
+
optimization_ms=optimization_ms,
|
|
340
|
+
compilation_ms=compilation_ms,
|
|
341
|
+
routing_ms=routing_ms,
|
|
342
|
+
token_counting_ms=token_counting_ms,
|
|
343
|
+
provider_latency_ms=provider_latency_ms,
|
|
344
|
+
end_to_end_ms=end_to_end_ms,
|
|
345
|
+
routing_policy=route_decision.policy,
|
|
346
|
+
routing_reason=route_decision.reason,
|
|
347
|
+
selected_provider=route_decision.selected_provider,
|
|
348
|
+
selected_model=route_decision.selected_model,
|
|
349
|
+
fallback_used=route_decision.fallback_used,
|
|
350
|
+
fallback_reason=route_decision.fallback_reason,
|
|
351
|
+
finish_reason=response.finish_reason,
|
|
352
|
+
status="success" if gen_exc is None and not response.error else "error",
|
|
353
|
+
error_code=response.error or (gen_exc.__class__.__name__ if gen_exc else None),
|
|
354
|
+
metadata=safe_meta,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
await self._telemetry.record(telemetry)
|
|
359
|
+
except Exception as tel_exc:
|
|
360
|
+
logger.error("Failed to persist model invocation telemetry: %s", tel_exc)
|
|
361
|
+
|
|
362
|
+
explanation_data: dict[str, Any] | None = None
|
|
363
|
+
if explain and self._explainability is not None:
|
|
364
|
+
try:
|
|
365
|
+
from contextos.services.explainability import ExplanationRequest
|
|
366
|
+
exp_req = ExplanationRequest(
|
|
367
|
+
query=query,
|
|
368
|
+
budget=comp_cfg.budget,
|
|
369
|
+
limit=retrieval_config.max_results if retrieval_config else 25,
|
|
370
|
+
temporal_scope=(
|
|
371
|
+
TemporalScope(retrieval_config.temporal_scope.value)
|
|
372
|
+
if retrieval_config and hasattr(retrieval_config, "temporal_scope")
|
|
373
|
+
else TemporalScope.CURRENT
|
|
374
|
+
),
|
|
375
|
+
graph=True,
|
|
376
|
+
)
|
|
377
|
+
trace = await self._explainability.build_trace(
|
|
378
|
+
request=exp_req,
|
|
379
|
+
retrieval_request=RetrievalQuery(
|
|
380
|
+
text=query,
|
|
381
|
+
mode=RetrievalMode.HYBRID_GRAPH,
|
|
382
|
+
k=retrieval_config.max_results if retrieval_config else 25,
|
|
383
|
+
),
|
|
384
|
+
retrieved=retrieval_result,
|
|
385
|
+
selection=selection,
|
|
386
|
+
compiled=compiled_context,
|
|
387
|
+
dispatch_evidence=dispatch_evidence,
|
|
388
|
+
started_at=total_started,
|
|
389
|
+
pipeline_ms=retrieval_ms + optimization_ms + compilation_ms,
|
|
390
|
+
)
|
|
391
|
+
explanation_data = trace.model_dump(mode="json")
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.warning("Failed to build explanation in ask(): %s", e)
|
|
394
|
+
|
|
395
|
+
if gen_exc is not None:
|
|
396
|
+
# Attach observable dispatch evidence, explanation, and telemetry before re-raising
|
|
397
|
+
setattr(gen_exc, "dispatch_evidence", dispatch_evidence)
|
|
398
|
+
setattr(gen_exc, "explanation", explanation_data)
|
|
399
|
+
setattr(gen_exc, "telemetry", telemetry)
|
|
400
|
+
raise gen_exc
|
|
401
|
+
|
|
402
|
+
return AskResult(
|
|
403
|
+
response=response,
|
|
404
|
+
compiled_context=compiled_context,
|
|
405
|
+
route_decision=route_decision,
|
|
406
|
+
telemetry=telemetry,
|
|
407
|
+
dispatch_evidence=dispatch_evidence,
|
|
408
|
+
explanation=explanation_data,
|
|
409
|
+
)
|