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,549 @@
1
+ """Component protocols (interfaces) for ContextOS.
2
+
3
+ Every major component communicates through a Protocol defined here.
4
+ Concrete implementations are injected at startup via daemon/wiring.py.
5
+
6
+ Protocol rules:
7
+ - All methods that touch I/O are async.
8
+ - Protocols are minimal — they define the contract, not convenience methods.
9
+ - Return types are domain models from core.models, never raw dicts or tuples.
10
+ - Protocols are testable: contract test suites verify any implementation.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from datetime import datetime
16
+ from typing import Any, Protocol, runtime_checkable
17
+ from uuid import UUID
18
+
19
+ from contextos.core.enums import (
20
+ GraphRelationType,
21
+ MemoryStatus,
22
+ MemoryType,
23
+ OptimizationStrategy,
24
+ RoutingPolicy,
25
+ SourceRole,
26
+ )
27
+ from contextos.core.models import (
28
+ CandidateMemory,
29
+ CompiledContext,
30
+ CompilationConfig,
31
+ ContextBudget,
32
+ EventFilters,
33
+ GraphEdge,
34
+ GraphExpansion,
35
+ GraphNode,
36
+ IngestRequest,
37
+ IngestResult,
38
+ LexicalResult,
39
+ Memory,
40
+ MemoryFilters,
41
+ MemoryRelation,
42
+ MemoryUpdate,
43
+ ModelCapabilities,
44
+ ModelInvocationTelemetry,
45
+ ModelRequest,
46
+ ModelResponse,
47
+ RawEvent,
48
+ RetrievalConfig,
49
+ RetrievalQuery,
50
+ RetrievalResult,
51
+ RouteDecision,
52
+ SelectionResult,
53
+ ScanResult,
54
+ ScoredMemory,
55
+ StageTrace,
56
+ TelemetrySummary,
57
+ TemporalDecision,
58
+ TemporalResolutionResult,
59
+ MemorySlot,
60
+ VectorResult,
61
+ )
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Storage Protocols
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ @runtime_checkable
70
+ class MemoryRepository(Protocol):
71
+ """Persistence layer for Memory objects (SQLite)."""
72
+
73
+ async def get(self, memory_id: UUID) -> Memory | None: ...
74
+
75
+ async def list(self, filters: MemoryFilters) -> list[Memory]: ...
76
+
77
+ async def create(self, memory: Memory) -> Memory: ...
78
+
79
+ async def update(self, memory_id: UUID, update: MemoryUpdate, expected_version: int) -> Memory:
80
+ """Update a memory. Raises ConcurrencyError if version doesn't match."""
81
+ ...
82
+
83
+ async def update_status(
84
+ self, memory_id: UUID, new_status: MemoryStatus, expected_version: int
85
+ ) -> Memory: ...
86
+
87
+ async def supersede(
88
+ self, old_id: UUID, successor: Memory, expected_version: int
89
+ ) -> Memory: ...
90
+
91
+ async def update_access(self, memory_id: UUID) -> None:
92
+ """Increment access_count and update last_accessed_at."""
93
+ ...
94
+
95
+ async def delete(self, memory_id: UUID) -> None:
96
+ """Remove from database entirely (hard delete at storage level)."""
97
+ ...
98
+
99
+ async def count(self, filters: MemoryFilters | None = None) -> int: ...
100
+
101
+ async def get_by_hash(self, content_hash: str) -> Memory | None:
102
+ """Find a memory by its content hash. Used for exact dedup."""
103
+ ...
104
+
105
+ async def list_by_slot(self, slot_key: str) -> list[Memory]: ...
106
+
107
+ async def list_temporal(self, *, limit: int = 500) -> list[Memory]: ...
108
+
109
+ async def apply_temporal_decision(
110
+ self, candidate: Memory, decision: TemporalDecision
111
+ ) -> TemporalResolutionResult: ...
112
+
113
+
114
+ @runtime_checkable
115
+ class EventRepository(Protocol):
116
+ """Append-only persistence for RawEvent objects."""
117
+
118
+ async def append(self, event: RawEvent) -> None: ...
119
+
120
+ async def get(self, event_id: UUID) -> RawEvent | None: ...
121
+
122
+ async def list(self, filters: EventFilters) -> list[RawEvent]: ...
123
+
124
+ async def count(self, filters: EventFilters | None = None) -> int: ...
125
+
126
+
127
+ @runtime_checkable
128
+ class RelationRepository(Protocol):
129
+ """Persistence for MemoryRelation objects."""
130
+
131
+ async def create(self, relation: MemoryRelation) -> MemoryRelation: ...
132
+
133
+ async def get_relations(
134
+ self, memory_id: UUID, direction: str = "both"
135
+ ) -> list[MemoryRelation]:
136
+ """Get relations for a memory.
137
+
138
+ direction: 'outgoing', 'incoming', or 'both'.
139
+ """
140
+ ...
141
+
142
+ async def delete_for_memory(self, memory_id: UUID) -> int:
143
+ """Delete all relations involving this memory. Returns count deleted."""
144
+ ...
145
+
146
+
147
+ @runtime_checkable
148
+ class GraphRepository(Protocol):
149
+ """Persistence contract for the rebuildable graph projection."""
150
+
151
+ async def replace_all(self, nodes: list[GraphNode], edges: list[GraphEdge]) -> None: ...
152
+
153
+ async def nodes(self) -> list[GraphNode]: ...
154
+
155
+ async def find_nodes(self, canonical_keys: set[str]) -> list[GraphNode]: ...
156
+
157
+ async def edges_for_nodes(
158
+ self, node_ids: set[UUID], *, limit: int | None = None
159
+ ) -> list[GraphEdge]: ...
160
+
161
+ async def all_edges(self) -> list[GraphEdge]: ...
162
+
163
+ async def counts(self) -> tuple[int, int, int]: ...
164
+
165
+ async def source_is_dirty(self) -> bool: ...
166
+
167
+ @runtime_checkable
168
+ class VectorStore(Protocol):
169
+ """Vector storage and similarity search."""
170
+
171
+ async def add(
172
+ self, ids: list[str], vectors: list[list[float]], metadata: list[dict[str, Any]]
173
+ ) -> None: ...
174
+
175
+ async def search(
176
+ self,
177
+ vector: list[float],
178
+ top_k: int = 20,
179
+ filters: dict[str, Any] | None = None,
180
+ ) -> list[VectorResult]: ...
181
+
182
+ async def delete(self, ids: list[str]) -> None: ...
183
+
184
+ async def count(self) -> int: ...
185
+
186
+ async def rebuild(
187
+ self, ids: list[str], vectors: list[list[float]], metadata: list[dict[str, Any]]
188
+ ) -> None: ...
189
+
190
+
191
+ @runtime_checkable
192
+ class LexicalIndex(Protocol):
193
+ """Full-text / BM25 lexical search index."""
194
+
195
+ async def index(self, doc_id: str, text: str, metadata: dict[str, Any] | None = None) -> None:
196
+ ...
197
+
198
+ async def search(
199
+ self,
200
+ query: str,
201
+ top_k: int = 20,
202
+ filters: dict[str, Any] | None = None,
203
+ ) -> list[LexicalResult]: ...
204
+
205
+ async def delete(self, doc_id: str) -> None: ...
206
+
207
+ async def count(self) -> int: ...
208
+
209
+ async def rebuild(self, documents: dict[str, str]) -> None:
210
+ """Rebuild the entire index from a dict of {id: text}."""
211
+ ...
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Service Protocols
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ @runtime_checkable
220
+ class IngestionService(Protocol):
221
+ """End-to-end ingestion pipeline: validate → scan → store → extract → index."""
222
+
223
+ async def ingest(self, request: IngestRequest) -> IngestResult: ...
224
+
225
+
226
+ @runtime_checkable
227
+ class MemoryService(Protocol):
228
+ """Business logic for memory management, including lifecycle transitions."""
229
+
230
+ async def get(self, memory_id: UUID) -> Memory | None: ...
231
+
232
+ async def list(self, filters: MemoryFilters) -> list[Memory]: ...
233
+
234
+ async def search(self, query: str, limit: int = 50) -> list[ScoredMemory]: ...
235
+
236
+ async def update(self, memory_id: UUID, update: MemoryUpdate) -> Memory: ...
237
+
238
+ async def transition(
239
+ self, memory_id: UUID, new_status: MemoryStatus, reason: str = ""
240
+ ) -> Memory:
241
+ """Transition a memory to a new lifecycle state.
242
+
243
+ Raises InvalidTransitionError if the transition is not allowed.
244
+ """
245
+ ...
246
+
247
+ async def delete(self, memory_id: UUID) -> None:
248
+ """Soft delete: transition to DELETED, remove from indices."""
249
+ ...
250
+
251
+ async def purge(self, memory_id: UUID) -> None:
252
+ """Hard delete: destroy from all stores."""
253
+ ...
254
+
255
+ async def history(self, topic: str) -> list[Memory]:
256
+ """Temporal evolution of memories related to a topic."""
257
+ ...
258
+
259
+
260
+ @runtime_checkable
261
+ class RetrievalService(Protocol):
262
+ """Multi-strategy retrieval with fusion, reranking, and dedup."""
263
+
264
+ async def retrieve(
265
+ self, query: str | RetrievalQuery, config: RetrievalConfig | None = None
266
+ ) -> RetrievalResult: ...
267
+
268
+
269
+ @runtime_checkable
270
+ class CompilationService(Protocol):
271
+ """Budget-constrained context compilation from retrieved memories."""
272
+
273
+ async def compile(
274
+ self,
275
+ query: str,
276
+ memories: list[ScoredMemory] | SelectionResult,
277
+ config: CompilationConfig | None = None,
278
+ ) -> CompiledContext: ...
279
+
280
+
281
+ @runtime_checkable
282
+ class TokenAwareOptimizer(Protocol):
283
+ """Select whole retrieved memories within a memory-context budget."""
284
+
285
+ def optimize(
286
+ self,
287
+ query: str,
288
+ candidates: list[ScoredMemory],
289
+ budget: ContextBudget,
290
+ strategy: OptimizationStrategy = OptimizationStrategy.CONTEXTOS,
291
+ ) -> SelectionResult: ...
292
+
293
+
294
+ @runtime_checkable
295
+ class TemporalResolver(Protocol):
296
+ """Resolve accepted candidates into deterministic temporal timelines."""
297
+
298
+ async def resolve(self, candidate: Memory) -> TemporalResolutionResult: ...
299
+
300
+ async def get_current_state(self, slot: MemorySlot | str) -> list[Memory]: ...
301
+
302
+ async def get_history(self, slot: MemorySlot | str) -> list[Memory]: ...
303
+
304
+ async def get_previous(self, memory_id: UUID) -> Memory | None: ...
305
+
306
+
307
+ @runtime_checkable
308
+ class GraphService(Protocol):
309
+ """Bounded graph projection and traversal contract."""
310
+
311
+ async def rebuild(self) -> tuple[int, int, int]: ...
312
+
313
+ async def upsert_memory(self, memory: Memory) -> tuple[int, int, int]: ...
314
+
315
+ async def remove_memory(self, memory_id: UUID) -> tuple[int, int, int]: ...
316
+
317
+ async def find_entities(self, text: str) -> list[GraphNode]: ...
318
+
319
+ async def neighbors(
320
+ self,
321
+ node_id: UUID,
322
+ *,
323
+ relation_types: set[GraphRelationType] | None = None,
324
+ min_confidence: float = 0.0,
325
+ ) -> list[GraphEdge]: ...
326
+
327
+ async def expand(
328
+ self,
329
+ *,
330
+ query_text: str,
331
+ seed_memory_ids: list[UUID] | None = None,
332
+ max_hops: int = 2,
333
+ min_confidence: float = 0.6,
334
+ max_nodes: int = 100,
335
+ max_edges: int = 250,
336
+ relation_types: set[GraphRelationType] | None = None,
337
+ ) -> GraphExpansion: ...
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # Infrastructure Protocols
342
+ # ---------------------------------------------------------------------------
343
+
344
+
345
+ @runtime_checkable
346
+ class EmbeddingService(Protocol):
347
+ """Generate embeddings from text. Abstracts over model choice."""
348
+
349
+ async def embed(self, texts: list[str]) -> list[list[float]]:
350
+ """Embed a batch of texts."""
351
+ ...
352
+
353
+ async def embed_query(self, query: str) -> list[float]:
354
+ """Embed a single query. May use a different prefix/strategy than documents."""
355
+ ...
356
+
357
+ @property
358
+ def dimension(self) -> int:
359
+ """Embedding vector dimension."""
360
+ ...
361
+
362
+ @property
363
+ def model_name(self) -> str:
364
+ """Name of the loaded model."""
365
+ ...
366
+
367
+
368
+ @runtime_checkable
369
+ class SecretScanner(Protocol):
370
+ """Detect secrets and sensitive content in text."""
371
+
372
+ def scan(self, text: str) -> ScanResult: ...
373
+
374
+ def redact(self, text: str) -> tuple[str, ScanResult]:
375
+ """Scan and return (redacted_text, scan_result)."""
376
+ ...
377
+
378
+
379
+ @runtime_checkable
380
+ class MemoryExtractor(Protocol):
381
+ """Extract discrete memories from raw text."""
382
+
383
+ async def extract(
384
+ self,
385
+ text: str,
386
+ source_type: str = "cli_input",
387
+ source_uri: str | None = None,
388
+ suggested_type: MemoryType | None = None,
389
+ tags: list[str] | None = None,
390
+ source_role: SourceRole = SourceRole.USER,
391
+ confirmed_user_information: bool = False,
392
+ ) -> list[CandidateMemory]: ...
393
+
394
+
395
+ @runtime_checkable
396
+ class DuplicateDetector(Protocol):
397
+ """Detect duplicates and near-duplicates against existing memories."""
398
+
399
+ async def check(
400
+ self, candidate: CandidateMemory, existing_memories: list[Memory] | None = None
401
+ ) -> DuplicateCheckResult: ...
402
+
403
+
404
+ class DuplicateCheckResult:
405
+ """Result of a duplicate check."""
406
+
407
+ __slots__ = ("is_duplicate", "is_near_duplicate", "matched_memory_id", "similarity_score")
408
+
409
+ def __init__(
410
+ self,
411
+ is_duplicate: bool = False,
412
+ is_near_duplicate: bool = False,
413
+ matched_memory_id: UUID | None = None,
414
+ similarity_score: float = 0.0,
415
+ ) -> None:
416
+ self.is_duplicate = is_duplicate
417
+ self.is_near_duplicate = is_near_duplicate
418
+ self.matched_memory_id = matched_memory_id
419
+ self.similarity_score = similarity_score
420
+
421
+
422
+ @runtime_checkable
423
+ class TokenCounter(Protocol):
424
+ """Count tokens in text. Abstracts over tokenizer choice."""
425
+
426
+ def count(self, text: str) -> int: ...
427
+
428
+ def count_batch(self, texts: list[str]) -> list[int]: ...
429
+
430
+ @property
431
+ def encoding_name(self) -> str: ...
432
+
433
+
434
+ # ---------------------------------------------------------------------------
435
+ # Observability Protocol
436
+ # ---------------------------------------------------------------------------
437
+
438
+
439
+ @runtime_checkable
440
+ class TraceCollector(Protocol):
441
+ """Collect and store pipeline traces."""
442
+
443
+ async def record(self, trace_id: str, stage: StageTrace) -> None: ...
444
+
445
+ async def get_traces(self, limit: int = 100) -> list[dict[str, Any]]: ...
446
+
447
+ async def get_stats(self) -> dict[str, Any]: ...
448
+
449
+
450
+ # ---------------------------------------------------------------------------
451
+ # Phase 9: Model Runtime, Router, and Telemetry Protocols
452
+ # ---------------------------------------------------------------------------
453
+
454
+
455
+ @runtime_checkable
456
+ class ModelProvider(Protocol):
457
+ """Downstream LLM execution provider interface."""
458
+
459
+ @property
460
+ def provider_id(self) -> str:
461
+ """Unique provider identifier (e.g. 'fake', 'ollama', 'openai_compatible')."""
462
+ ...
463
+
464
+ @property
465
+ def is_local(self) -> bool:
466
+ """Whether this provider runs locally without external network exfiltration."""
467
+ ...
468
+
469
+ async def list_models(self) -> list[ModelCapabilities]:
470
+ """List models offered by this provider."""
471
+ ...
472
+
473
+ async def health(self) -> bool:
474
+ """Check provider connectivity and readiness."""
475
+ ...
476
+
477
+ async def generate(self, request: ModelRequest) -> ModelResponse:
478
+ """Generate a completion for the given request."""
479
+ ...
480
+
481
+ def count_tokens(self, text: str, model: str) -> int:
482
+ """Count tokens using this provider's tokenizer or tokenizer family."""
483
+ ...
484
+
485
+
486
+ @runtime_checkable
487
+ class ModelRouter(Protocol):
488
+ """Deterministic routing protocol to select providers and models."""
489
+
490
+ async def route(
491
+ self,
492
+ request: ModelRequest,
493
+ providers: dict[str, ModelProvider],
494
+ policy: RoutingPolicy | None = None,
495
+ ) -> RouteDecision:
496
+ """Select the target provider and model based on policy and constraints."""
497
+ ...
498
+
499
+
500
+ @runtime_checkable
501
+ class TelemetryRepository(Protocol):
502
+ """Persistence repository for model invocation telemetry."""
503
+
504
+ async def record(self, telemetry: ModelInvocationTelemetry) -> None:
505
+ """Persist a single model invocation telemetry record."""
506
+ ...
507
+
508
+ async def get(self, invocation_id: UUID) -> ModelInvocationTelemetry | None:
509
+ """Retrieve a telemetry record by invocation UUID."""
510
+ ...
511
+
512
+ async def list_recent(
513
+ self,
514
+ limit: int = 50,
515
+ model_id: str | None = None,
516
+ provider_id: str | None = None,
517
+ start: datetime | None = None,
518
+ ) -> list[ModelInvocationTelemetry]:
519
+ """List recent invocations in descending chronological order."""
520
+ ...
521
+
522
+ async def provider_model_breakdown(
523
+ self,
524
+ start: datetime | None = None,
525
+ provider_id: str | None = None,
526
+ model_id: str | None = None,
527
+ ) -> list[dict[str, Any]]: ...
528
+
529
+ async def context_measurement_bases(
530
+ self,
531
+ model_id: str | None = None,
532
+ provider_id: str | None = None,
533
+ start: datetime | None = None,
534
+ ) -> list[dict[str, str]]: ...
535
+
536
+ async def count(self) -> int:
537
+ """Count total recorded invocations."""
538
+ ...
539
+
540
+ async def summary(
541
+ self,
542
+ start: datetime | None = None,
543
+ end: datetime | None = None,
544
+ provider_id: str | None = None,
545
+ model_id: str | None = None,
546
+ success_only: bool = False,
547
+ ) -> TelemetrySummary:
548
+ """Compute aggregated token usage, avoidance, and latency statistics."""
549
+ ...
@@ -0,0 +1 @@
1
+ """Daemon package for ContextOS."""