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.
Files changed (93) hide show
  1. contextos/__init__.py +3 -0
  2. contextos/__main__.py +6 -0
  3. contextos/api/__init__.py +1 -0
  4. contextos/api/routes/__init__.py +1 -0
  5. contextos/api/routes/desktop.py +322 -0
  6. contextos/api/routes/ingest.py +17 -0
  7. contextos/api/routes/memories.py +84 -0
  8. contextos/api/routes/models.py +81 -0
  9. contextos/api/routes/retrieval.py +89 -0
  10. contextos/api/routes/system.py +216 -0
  11. contextos/api/server.py +195 -0
  12. contextos/benchmarks/__init__.py +1 -0
  13. contextos/benchmarks/compilation.py +245 -0
  14. contextos/benchmarks/connectors.py +423 -0
  15. contextos/benchmarks/explainability.py +103 -0
  16. contextos/benchmarks/final.py +406 -0
  17. contextos/benchmarks/graph.py +310 -0
  18. contextos/benchmarks/graph_adversarial.py +525 -0
  19. contextos/benchmarks/mcp.py +324 -0
  20. contextos/benchmarks/model_routing.py +203 -0
  21. contextos/benchmarks/optimization.py +305 -0
  22. contextos/benchmarks/rescue_integration.py +127 -0
  23. contextos/benchmarks/retrieval.py +266 -0
  24. contextos/benchmarks/temporal.py +377 -0
  25. contextos/benchmarks/temporal_hotpath.py +76 -0
  26. contextos/benchmarks/terminal.py +62 -0
  27. contextos/cli/__init__.py +1 -0
  28. contextos/cli/app.py +932 -0
  29. contextos/cli/dashboard.py +174 -0
  30. contextos/cli/formatters.py +299 -0
  31. contextos/config/__init__.py +1 -0
  32. contextos/config/settings.py +160 -0
  33. contextos/connectors/__init__.py +6 -0
  34. contextos/connectors/fake.py +11 -0
  35. contextos/connectors/json_import.py +125 -0
  36. contextos/connectors/local_files.py +102 -0
  37. contextos/connectors/manager.py +293 -0
  38. contextos/connectors/models.py +62 -0
  39. contextos/connectors/protocols.py +11 -0
  40. contextos/core/__init__.py +103 -0
  41. contextos/core/enums.py +489 -0
  42. contextos/core/exceptions.py +293 -0
  43. contextos/core/models.py +1147 -0
  44. contextos/core/protocols.py +549 -0
  45. contextos/daemon/__init__.py +1 -0
  46. contextos/daemon/manager.py +510 -0
  47. contextos/daemon/state.py +127 -0
  48. contextos/daemon/wiring.py +296 -0
  49. contextos/demo.py +217 -0
  50. contextos/embedding/__init__.py +1 -0
  51. contextos/embedding/deterministic.py +76 -0
  52. contextos/embedding/sentence_transformers.py +80 -0
  53. contextos/mcp/__init__.py +5 -0
  54. contextos/mcp/server.py +269 -0
  55. contextos/providers/__init__.py +13 -0
  56. contextos/providers/fake.py +217 -0
  57. contextos/providers/ollama.py +297 -0
  58. contextos/providers/openai_compatible.py +337 -0
  59. contextos/services/__init__.py +1 -0
  60. contextos/services/compilation.py +535 -0
  61. contextos/services/explainability.py +553 -0
  62. contextos/services/extraction.py +311 -0
  63. contextos/services/graph.py +524 -0
  64. contextos/services/graph_retrieval.py +143 -0
  65. contextos/services/ingestion.py +143 -0
  66. contextos/services/inspection.py +174 -0
  67. contextos/services/memory.py +291 -0
  68. contextos/services/model_service.py +409 -0
  69. contextos/services/optimization.py +426 -0
  70. contextos/services/privacy.py +331 -0
  71. contextos/services/retrieval.py +302 -0
  72. contextos/services/retrieval_index.py +88 -0
  73. contextos/services/router.py +302 -0
  74. contextos/services/secret_scanner.py +207 -0
  75. contextos/services/telemetry_query.py +102 -0
  76. contextos/services/temporal.py +500 -0
  77. contextos/services/token_counter.py +222 -0
  78. contextos/storage/__init__.py +1 -0
  79. contextos/storage/connector_repo.py +67 -0
  80. contextos/storage/database.py +497 -0
  81. contextos/storage/event_repo.py +137 -0
  82. contextos/storage/graph_repo.py +228 -0
  83. contextos/storage/lexical/__init__.py +1 -0
  84. contextos/storage/lexical/bm25.py +134 -0
  85. contextos/storage/memory_repo.py +589 -0
  86. contextos/storage/relation_repo.py +80 -0
  87. contextos/storage/telemetry_repo.py +481 -0
  88. contextos/storage/vector/__init__.py +1 -0
  89. contextos/storage/vector/in_memory.py +162 -0
  90. contextos_memory_runtime-1.0.0rc2.dist-info/METADATA +143 -0
  91. contextos_memory_runtime-1.0.0rc2.dist-info/RECORD +93 -0
  92. contextos_memory_runtime-1.0.0rc2.dist-info/WHEEL +4 -0
  93. 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
+ )