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,296 @@
1
+ """Dependency wiring for ContextOS daemon.
2
+
3
+ This is where all protocol implementations are bound to their interfaces.
4
+ One function, called at startup, that assembles the entire dependency graph.
5
+ No magic. No DI framework.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from contextos.config.settings import Settings
15
+ from contextos.core.enums import SecretDetectionMode
16
+ from contextos.storage.database import Database
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ if TYPE_CHECKING:
21
+ from contextos.services.token_counter import TokenCounter
22
+
23
+
24
+ async def wire_services(settings: Settings) -> dict[str, Any]:
25
+ """Create and wire all service instances.
26
+
27
+ Returns a dict of {service_name: instance} ready for injection into
28
+ the API server and CLI.
29
+ """
30
+ from contextos.connectors.local_files import LocalFileConnector
31
+ from contextos.connectors.json_import import JsonImportConnector
32
+
33
+ configured_connectors = []
34
+ for connector_id, roots in settings.connectors.local_files.items():
35
+ if not roots or any(not root.is_dir() for root in roots):
36
+ raise ValueError(f"Connector {connector_id} has an invalid local root")
37
+ configured_connectors.append(LocalFileConnector(connector_id, roots))
38
+ for connector_id, path in settings.connectors.json_imports.items():
39
+ if (str(path).startswith(("\\\\", "//")) or
40
+ str(path.resolve()).startswith(("\\\\", "//")) or not path.is_file()):
41
+ raise ValueError(f"Connector {connector_id} has an invalid JSON import path")
42
+ configured_connectors.append(JsonImportConnector(connector_id, path))
43
+ if len({item.connector_id for item in configured_connectors}) != len(configured_connectors):
44
+ raise ValueError("Connector IDs must be unique")
45
+
46
+ # --- Database ---
47
+ data_dir = settings.daemon.data_dir
48
+ data_dir.mkdir(parents=True, exist_ok=True)
49
+
50
+ db = Database(data_dir / "contextos.db")
51
+ try:
52
+ await db.initialize()
53
+ return _wire_initialized_services(settings, db, configured_connectors)
54
+ except BaseException:
55
+ # Startup can fail before the caller receives services and owns cleanup.
56
+ await db.close()
57
+ raise
58
+
59
+
60
+ def _wire_initialized_services(
61
+ settings: Settings, db: Database, configured_connectors: list[Any],
62
+ ) -> dict[str, Any]:
63
+ """Construct services only after the database has a cleanup owner."""
64
+ services: dict[str, Any] = {"settings": settings}
65
+ services["database"] = db
66
+
67
+ conn = db.connection()
68
+
69
+ # --- Repositories ---
70
+ from contextos.storage.memory_repo import SqliteMemoryRepository
71
+ from contextos.storage.event_repo import SqliteEventRepository
72
+
73
+ memory_repo = SqliteMemoryRepository(conn)
74
+ event_repo = SqliteEventRepository(conn)
75
+ from contextos.storage.relation_repo import SqliteRelationRepository
76
+ from contextos.storage.graph_repo import SqliteGraphRepository
77
+ relation_repo = SqliteRelationRepository(conn)
78
+ graph_repo = SqliteGraphRepository(conn)
79
+ services["memory_repo"] = memory_repo
80
+ services["event_repo"] = event_repo
81
+ services["relation_repo"] = relation_repo
82
+ services["graph_repo"] = graph_repo
83
+
84
+ # --- Temporal resolution ---
85
+ from contextos.services.temporal import TemporalMemoryService
86
+
87
+ temporal = TemporalMemoryService(memory_repo)
88
+ services["temporal"] = temporal
89
+
90
+ from contextos.services.graph import MemoryGraphService
91
+
92
+ graph = MemoryGraphService(
93
+ memory_repo=memory_repo,
94
+ relation_repo=relation_repo,
95
+ graph_repo=graph_repo,
96
+ )
97
+ services["graph"] = graph
98
+
99
+ # --- Token Counter ---
100
+ from contextos.services.token_counter import DeterministicWordTokenCounter, TiktokenCounter
101
+
102
+ token_counter: TokenCounter
103
+ if settings.token_counter.encoding == "deterministic":
104
+ token_counter = DeterministicWordTokenCounter()
105
+ else:
106
+ token_counter = TiktokenCounter(settings.token_counter.encoding)
107
+ services["token_counter"] = token_counter
108
+
109
+ # --- Token-aware optimizer ---
110
+ from contextos.services.optimization import MemoryContextOptimizer
111
+
112
+ optimizer = MemoryContextOptimizer(token_counter=token_counter)
113
+ services["optimizer"] = optimizer
114
+
115
+ # --- Embedding Service ---
116
+ if settings.embedding.model == "deterministic":
117
+ # Explicit local/test configuration. This avoids a model download while
118
+ # retaining the normal retrieval, indexing, graph, and SQLite services.
119
+ from contextos.embedding.deterministic import DeterministicEmbedding
120
+ embedding_service = DeterministicEmbedding(16)
121
+ vector_dimension = 16
122
+ else:
123
+ from contextos.embedding.sentence_transformers import SentenceTransformerEmbedding
124
+ embedding_service = SentenceTransformerEmbedding(
125
+ model_name=settings.embedding.model,
126
+ device=settings.embedding.device,
127
+ )
128
+ vector_dimension = 384
129
+ services["embedding"] = embedding_service
130
+
131
+ # --- Vector Store ---
132
+ from contextos.storage.vector.in_memory import InMemoryVectorStore
133
+
134
+ vector_store = InMemoryVectorStore(dimension=vector_dimension)
135
+ services["vector_store"] = vector_store
136
+
137
+ # --- BM25 Index ---
138
+ from contextos.storage.lexical.bm25 import BM25Index
139
+
140
+ bm25_index = BM25Index()
141
+ services["bm25_index"] = bm25_index
142
+ services["lexical_index"] = bm25_index
143
+
144
+ # --- Explicit retrieval index synchronization ---
145
+ from contextos.services.retrieval_index import RetrievalIndexSynchronizer
146
+
147
+ retrieval_index = RetrievalIndexSynchronizer(
148
+ memory_repo=memory_repo,
149
+ embedding_service=embedding_service,
150
+ vector_store=vector_store,
151
+ lexical_index=bm25_index,
152
+ )
153
+ services["retrieval_index"] = retrieval_index
154
+
155
+ # --- Secret Scanner ---
156
+ from contextos.services.secret_scanner import PatternSecretScanner
157
+
158
+ scanner = PatternSecretScanner(
159
+ entropy_threshold=settings.privacy.entropy_threshold,
160
+ )
161
+ services["secret_scanner"] = scanner
162
+
163
+ # --- Memory Extractor ---
164
+ from contextos.services.extraction import RuleBasedMemoryExtractor
165
+
166
+ extractor = RuleBasedMemoryExtractor()
167
+ services["memory_extractor"] = extractor
168
+
169
+ # --- Ingestion Pipeline ---
170
+ from contextos.services.ingestion import IngestionPipeline
171
+
172
+ secret_mode = SecretDetectionMode(settings.privacy.secret_detection)
173
+ ingestion = IngestionPipeline(
174
+ secret_scanner=scanner,
175
+ memory_extractor=extractor,
176
+ memory_repo=memory_repo,
177
+ event_repo=event_repo,
178
+ embedding_service=embedding_service,
179
+ vector_store=vector_store,
180
+ lexical_index=bm25_index,
181
+ token_counter=token_counter,
182
+ secret_detection_mode=secret_mode,
183
+ )
184
+ services["ingestion"] = ingestion
185
+
186
+ # --- Phase 11: connector state and bounded sync manager ---
187
+ from contextos.storage.connector_repo import SqliteConnectorRepository
188
+ from contextos.connectors.manager import ConnectorManager
189
+ connector_repo = SqliteConnectorRepository(conn)
190
+ services["connector_repo"] = connector_repo
191
+ services["connectors"] = ConnectorManager(
192
+ state_repo=connector_repo, ingestion=ingestion, temporal=temporal,
193
+ )
194
+ for connector in configured_connectors:
195
+ services["connectors"].register(connector)
196
+
197
+ # --- Memory Manager ---
198
+ from contextos.services.memory import MemoryManager
199
+
200
+ memory_manager = MemoryManager(
201
+ memory_repo=memory_repo,
202
+ event_repo=event_repo,
203
+ vector_store=vector_store,
204
+ lexical_index=bm25_index,
205
+ embedding_service=embedding_service,
206
+ )
207
+ services["memory"] = memory_manager
208
+
209
+ # --- Retrieval Engine ---
210
+ from contextos.services.retrieval import HybridRetrievalEngine
211
+
212
+ base_retrieval = HybridRetrievalEngine(
213
+ memory_repo=memory_repo,
214
+ vector_store=vector_store,
215
+ lexical_index=bm25_index,
216
+ embedding_service=embedding_service,
217
+ index_synchronizer=retrieval_index,
218
+ )
219
+ from contextos.services.graph_retrieval import GraphAugmentedRetrievalEngine
220
+
221
+ retrieval = GraphAugmentedRetrievalEngine(
222
+ base_engine=base_retrieval,
223
+ graph_service=graph,
224
+ memory_repo=memory_repo,
225
+ )
226
+ services["base_retrieval"] = base_retrieval
227
+ services["retrieval"] = retrieval
228
+
229
+ # --- Context Compiler ---
230
+ from contextos.services.compilation import QueryAwareContextCompiler
231
+
232
+ compiler = QueryAwareContextCompiler(token_counter=token_counter)
233
+ services["compilation"] = compiler
234
+
235
+ # --- Phase 9: Telemetry Repository & Query Service ---
236
+ from contextos.storage.telemetry_repo import SqliteTelemetryRepository
237
+ from contextos.services.telemetry_query import TelemetryQueryService
238
+
239
+ telemetry_repo = SqliteTelemetryRepository(conn)
240
+ telemetry_query = TelemetryQueryService(telemetry_repo)
241
+ services["telemetry_repo"] = telemetry_repo
242
+ services["telemetry_query"] = telemetry_query
243
+
244
+ # --- Phase 9: Provider Adapters ---
245
+ from contextos.providers.fake import DeterministicFakeProvider
246
+ from contextos.providers.ollama import OllamaProvider
247
+ from contextos.providers.openai_compatible import OpenAICompatibleProvider
248
+
249
+ fake_provider = DeterministicFakeProvider()
250
+ ollama_provider = OllamaProvider()
251
+ openai_compatible_provider = OpenAICompatibleProvider()
252
+
253
+ providers = {
254
+ fake_provider.provider_id: fake_provider,
255
+ ollama_provider.provider_id: ollama_provider,
256
+ openai_compatible_provider.provider_id: openai_compatible_provider,
257
+ }
258
+ services["fake_provider"] = fake_provider
259
+ services["ollama_provider"] = ollama_provider
260
+ services["openai_compatible_provider"] = openai_compatible_provider
261
+ services["providers"] = providers
262
+
263
+ # --- Phase 9: Model Router ---
264
+ from contextos.core.enums import RoutingPolicy
265
+ from contextos.services.router import DeterministicModelRouter
266
+
267
+ router = DeterministicModelRouter(
268
+ default_provider_id="fake",
269
+ default_model_id="fake-default",
270
+ default_policy=RoutingPolicy.LOCAL_FIRST,
271
+ )
272
+ services["router"] = router
273
+
274
+ # --- Phase 9: Unified ContextOS Model Service ---
275
+ from contextos.services.model_service import ContextOSModelService
276
+
277
+ model_service = ContextOSModelService(
278
+ retrieval_service=retrieval,
279
+ optimizer=optimizer,
280
+ compilation_service=compiler,
281
+ router=router,
282
+ providers=providers,
283
+ telemetry_repo=telemetry_repo,
284
+ token_counter=token_counter,
285
+ )
286
+ services["model_service"] = model_service
287
+
288
+ from contextos.services.explainability import ExplainabilityService
289
+ explainability = ExplainabilityService(services)
290
+ services["explainability"] = explainability
291
+ model_service.set_explainability_service(explainability)
292
+ from contextos.services.inspection import RAGInspector
293
+ services["inspector"] = RAGInspector(services)
294
+
295
+ logger.info("All services wired successfully")
296
+ return services
contextos/demo.py ADDED
@@ -0,0 +1,217 @@
1
+ """Reproducible offline ContextOS walkthrough on a temporary database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from uuid import UUID
11
+
12
+ from httpx import ASGITransport, AsyncClient
13
+
14
+ import contextos.api.server as api_server
15
+ from contextos.config.settings import DaemonConfig, EmbeddingConfig, Settings, TokenCounterConfig
16
+ from contextos.connectors.fake import FakeConnector
17
+ from contextos.connectors.models import ConnectorItem
18
+ from contextos.core.enums import SourceRole
19
+ from contextos.core.exceptions import SecretDetectedError
20
+ from contextos.core.models import IngestRequest
21
+ from contextos.daemon.wiring import wire_services
22
+ from contextos.services.explainability import ExplanationRequest
23
+ from contextos.services.inspection import InspectionRequest
24
+
25
+
26
+ async def run_demo() -> dict[str, Any]:
27
+ """Exercise real services; synthetic fixture values are discarded on exit."""
28
+ with tempfile.TemporaryDirectory(prefix="contextos-demo-") as folder:
29
+ services = await wire_services(
30
+ Settings(
31
+ daemon=DaemonConfig(data_dir=Path(folder)),
32
+ embedding=EmbeddingConfig(model="deterministic"),
33
+ token_counter=TokenCounterConfig(encoding="deterministic"),
34
+ )
35
+ )
36
+ try:
37
+
38
+ async def remember(text: str) -> list[str]:
39
+ ingested = await services["ingestion"].ingest(
40
+ IngestRequest(
41
+ content=text,
42
+ source_type="manual",
43
+ source_role=SourceRole.USER,
44
+ )
45
+ )
46
+ accepted = [
47
+ await services["temporal"].accept(
48
+ candidate, provenance_event_id=ingested.event_id
49
+ )
50
+ for candidate in ingested.candidates
51
+ ]
52
+ return [str(item.memory.id) for item in accepted]
53
+
54
+ initial = await remember("I prefer concise answers for technical questions.")
55
+ changed = await remember("I now prefer detailed answers for technical questions.")
56
+ graph_ids = await remember("My project Atlas uses Ollama for local inference.")
57
+ connector = FakeConnector(
58
+ "demo_notes",
59
+ [
60
+ ConnectorItem(
61
+ external_id="note-1",
62
+ source_type="fake",
63
+ source_uri="fake://demo/note-1",
64
+ revision="1",
65
+ content="I prefer concise technical documentation.",
66
+ )
67
+ ],
68
+ )
69
+ services["connectors"].register(connector)
70
+ first_sync = await services["connectors"].sync("demo_notes")
71
+ second_sync = await services["connectors"].sync("demo_notes")
72
+ await services["retrieval_index"].ensure_current()
73
+ graph_nodes, graph_edges, _ = await services["graph"].rebuild()
74
+ query = "detailed answers for technical questions"
75
+ explained = await services["explainability"].explain(
76
+ ExplanationRequest(
77
+ query=query,
78
+ graph=False,
79
+ )
80
+ )
81
+ inspected = await services["inspector"].inspect(
82
+ InspectionRequest(
83
+ query=query,
84
+ graph=False,
85
+ target_memory_id=UUID(changed[0]) if changed else None,
86
+ )
87
+ )
88
+ graph_expansion = await services["graph"].expand(
89
+ query_text="Atlas",
90
+ seed_memory_ids=[UUID(graph_ids[0])] if graph_ids else [],
91
+ )
92
+ graph_memory = (
93
+ await services["memory_repo"].get(UUID(graph_ids[0])) if graph_ids else None
94
+ )
95
+ asked = await services["model_service"].ask(
96
+ query, target_provider="fake", target_model="fake-default", explain=True
97
+ )
98
+ rejected = False
99
+ try:
100
+ await services["ingestion"].ingest(
101
+ IngestRequest(
102
+ content="My test API key is sk-demoPrivateFixture1234567890",
103
+ source_type="manual",
104
+ source_role=SourceRole.USER,
105
+ )
106
+ )
107
+ except SecretDetectedError:
108
+ rejected = True
109
+ telemetry = await services["telemetry_query"].summary_range(
110
+ provider_id="fake", model_id="fake-default"
111
+ )
112
+ old_memory = await services["memory_repo"].get(UUID(initial[0])) if initial else None
113
+ new_memory = await services["memory_repo"].get(UUID(changed[0])) if changed else None
114
+ dashboard_services = api_server._services.copy()
115
+ try:
116
+ api_server.set_services(services)
117
+ async with AsyncClient(
118
+ transport=ASGITransport(app=api_server.create_app()),
119
+ base_url="http://localhost",
120
+ ) as client:
121
+ dashboard_response = await client.get("/api/v1/dashboard")
122
+ dashboard_response.raise_for_status()
123
+ dashboard_data = dashboard_response.json()
124
+ finally:
125
+ api_server._services.clear()
126
+ api_server._services.update(dashboard_services)
127
+ if not (
128
+ initial and changed and graph_ids and first_sync.accepted > 0
129
+ and second_sync.unchanged > 0 and graph_edges > 0
130
+ and graph_expansion.candidate_scores and explained.trace_id
131
+ and inspected.candidates and inspected.context_diff["facts_emitted"] > 0
132
+ and asked.compiled_context.included_memory_ids
133
+ and asked.compiled_context.total_tokens > 0
134
+ and asked.telemetry.context_tokens_avoided > 0
135
+ and asked.telemetry.selected_memory_count > 0
136
+ and asked.telemetry.compiled_fact_count > 0
137
+ and asked.compiled_context.provenance_coverage > 0
138
+ ):
139
+ raise RuntimeError("Offline demo did not produce all required pipeline evidence")
140
+ if asked.telemetry.provider_id != "fake" or telemetry.total_invocations != 1:
141
+ raise RuntimeError(
142
+ "Offline demo did not invoke and record FakeProvider exactly once"
143
+ )
144
+ demo_connector = next(
145
+ (item for item in dashboard_data["connectors"] if item["id"] == "demo_notes"),
146
+ None,
147
+ )
148
+ if (
149
+ dashboard_data["memories"]["active"] < 1
150
+ or demo_connector is None
151
+ or demo_connector["tracked_items"] < 1
152
+ or not dashboard_data["recent"]
153
+ ):
154
+ raise RuntimeError(
155
+ "Offline demo dashboard is missing memory, connector, or telemetry data"
156
+ )
157
+ return {
158
+ "label": "LOCAL SYNTHETIC OFFLINE DEMO",
159
+ "token_measurement_source": services["token_counter"].measurement_source.value,
160
+ "tokenizer": services["token_counter"].encoding_name,
161
+ "initial_memory_ids": initial[:10],
162
+ "changed_memory_ids": changed[:10],
163
+ "temporal": {
164
+ "previous_status": old_memory.status.value if old_memory else None,
165
+ "current_status": new_memory.status.value if new_memory else None,
166
+ },
167
+ "connector_first": {"accepted": first_sync.accepted, "status": first_sync.status},
168
+ "connector_second": {
169
+ "unchanged": second_sync.unchanged,
170
+ "status": second_sync.status,
171
+ },
172
+ "graph": {
173
+ "nodes": graph_nodes,
174
+ "edges": graph_edges,
175
+ "expansion_candidates": len(graph_expansion.candidate_scores),
176
+ "memory_status": graph_memory.status.value if graph_memory else None,
177
+ "seed_count": len(graph_expansion.seed_node_ids),
178
+ "visited": len(graph_expansion.visited_node_ids),
179
+ },
180
+ "explanation": {
181
+ "trace_id": explained.trace_id,
182
+ "provider_state": explained.provider_dispatch["state"],
183
+ },
184
+ "inspection": {
185
+ "inspection_id": inspected.inspection_id,
186
+ "candidates": len(inspected.candidates),
187
+ "context_diff": inspected.context_diff,
188
+ },
189
+ "model": {
190
+ "provider": asked.route_decision.selected_provider,
191
+ "model": asked.route_decision.selected_model,
192
+ "dispatch_state": asked.dispatch_evidence.state.value,
193
+ "compiled_memory_ids": len(asked.compiled_context.included_memory_ids),
194
+ "compiled_tokens": asked.compiled_context.total_tokens,
195
+ "tokens_avoided": asked.telemetry.context_tokens_avoided,
196
+ "provenance_coverage": asked.compiled_context.provenance_coverage,
197
+ },
198
+ "telemetry_invocations": telemetry.total_invocations,
199
+ "dashboard": {
200
+ "active_memories": dashboard_data["memories"]["active"],
201
+ "connector_tracked_items": demo_connector["tracked_items"],
202
+ "recent_invocations": len(dashboard_data["recent"]),
203
+ "graph_projection_dirty": dashboard_data["graph"]["dirty"],
204
+ },
205
+ "privacy_secret_rejected": rejected,
206
+ "ephemeral": True,
207
+ }
208
+ finally:
209
+ await services["database"].close()
210
+
211
+
212
+ def main() -> None:
213
+ print(json.dumps(asyncio.run(run_demo()), indent=2))
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
@@ -0,0 +1 @@
1
+ """Embedding package for ContextOS."""
@@ -0,0 +1,76 @@
1
+ """Small deterministic embedding adapter for offline tests and benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+ import re
8
+
9
+
10
+ _SYNONYMS = {
11
+ "ai": "model",
12
+ "artificial": "model",
13
+ "inference": "runtime",
14
+ "engine": "runtime",
15
+ "programming": "language",
16
+ "coding": "language",
17
+ "formerly": "previous",
18
+ "prior": "previous",
19
+ "before": "previous",
20
+ "style": "response",
21
+ "brief": "concise",
22
+ "short": "concise",
23
+ "large": "larger",
24
+ "ram": "memory",
25
+ "resources": "memory",
26
+ "crashed": "failed",
27
+ }
28
+ _STOP_WORDS = {
29
+ "a", "an", "and", "did", "do", "does", "for", "has", "have", "i", "in",
30
+ "is", "it", "of", "on", "the", "to", "user", "was", "what", "when", "which",
31
+ "with",
32
+ }
33
+
34
+
35
+ class DeterministicEmbedding:
36
+ """Hashed bag-of-concepts embedding with no model or network dependency."""
37
+
38
+ def __init__(self, dimension: int = 128) -> None:
39
+ if dimension < 8:
40
+ raise ValueError("dimension must be at least 8")
41
+ self._dimension = dimension
42
+
43
+ def _vectorize(self, text: str) -> list[float]:
44
+ vector = [0.0] * self._dimension
45
+ for token in re.findall(r"[a-z0-9]+(?:[+#._-][a-z0-9]+)*", text.casefold()):
46
+ if token in _STOP_WORDS:
47
+ continue
48
+ if token in {"focused", "focuses", "focusing"}:
49
+ token = "focus"
50
+ elif (
51
+ token != "focus"
52
+ and token.endswith("s")
53
+ and len(token) > 4
54
+ and not token.endswith("ss")
55
+ ):
56
+ token = token[:-1]
57
+ token = _SYNONYMS.get(token, token)
58
+ digest = hashlib.sha256(token.encode("utf-8")).digest()
59
+ index = int.from_bytes(digest[:4], "big") % self._dimension
60
+ vector[index] += 1.0
61
+ norm = math.sqrt(sum(value * value for value in vector))
62
+ return [value / norm for value in vector] if norm else vector
63
+
64
+ async def embed(self, texts: list[str]) -> list[list[float]]:
65
+ return [self._vectorize(text) for text in texts]
66
+
67
+ async def embed_query(self, query: str) -> list[float]:
68
+ return self._vectorize(query)
69
+
70
+ @property
71
+ def dimension(self) -> int:
72
+ return self._dimension
73
+
74
+ @property
75
+ def model_name(self) -> str:
76
+ return "deterministic-hashed-concepts"
@@ -0,0 +1,80 @@
1
+ """Sentence-transformers embedding service for ContextOS.
2
+
3
+ Wraps sentence-transformers for local embedding generation.
4
+ Default model: all-MiniLM-L6-v2 (384 dimensions, ~22M parameters, fast).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SentenceTransformerEmbedding:
15
+ """Local embedding using sentence-transformers.
16
+
17
+ Implements the EmbeddingService protocol.
18
+ Lazy-loads the model on first use to avoid blocking startup.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ model_name: str = "all-MiniLM-L6-v2",
24
+ device: str = "cpu",
25
+ ) -> None:
26
+ self._model_name = model_name
27
+ self._device = device
28
+ self._model = None
29
+ self._dimension: int | None = None
30
+
31
+ def _load_model(self) -> None:
32
+ """Lazily load the model."""
33
+ if self._model is not None:
34
+ return
35
+
36
+ try:
37
+ from sentence_transformers import SentenceTransformer
38
+
39
+ logger.info("Loading embedding model: %s (device: %s)", self._model_name, self._device)
40
+ self._model = SentenceTransformer(self._model_name, device=self._device)
41
+ self._dimension = self._model.get_sentence_embedding_dimension()
42
+ logger.info(
43
+ "Embedding model loaded: %s (dim=%d)", self._model_name, self._dimension
44
+ )
45
+ except ImportError:
46
+ raise RuntimeError(
47
+ "sentence-transformers is required for SentenceTransformerEmbedding. "
48
+ "Install ContextOS with the embeddings extra: pip install 'contextos-memory-runtime[embeddings]'"
49
+ )
50
+ except Exception as e:
51
+ raise RuntimeError(f"Failed to load embedding model '{self._model_name}': {e}") from e
52
+
53
+ async def embed(self, texts: list[str]) -> list[list[float]]:
54
+ """Embed a batch of texts."""
55
+ self._load_model()
56
+ assert self._model is not None
57
+
58
+ embeddings = self._model.encode(
59
+ texts,
60
+ normalize_embeddings=True,
61
+ show_progress_bar=False,
62
+ )
63
+
64
+ return embeddings.tolist()
65
+
66
+ async def embed_query(self, query: str) -> list[float]:
67
+ """Embed a single query."""
68
+ results = await self.embed([query])
69
+ return results[0]
70
+
71
+ @property
72
+ def dimension(self) -> int:
73
+ """Embedding vector dimension."""
74
+ self._load_model()
75
+ assert self._dimension is not None
76
+ return self._dimension
77
+
78
+ @property
79
+ def model_name(self) -> str:
80
+ return self._model_name
@@ -0,0 +1,5 @@
1
+ """MCP transport adapter for ContextOS."""
2
+
3
+ from contextos.mcp.server import create_mcp_server
4
+
5
+ __all__ = ["create_mcp_server"]