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,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
|