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,423 @@
|
|
|
1
|
+
"""Deterministic Phase 11 connector performance and unchanged skip benchmark.
|
|
2
|
+
|
|
3
|
+
Label: LOCAL DEVELOPMENT SYNTHETIC CONNECTOR BENCHMARK
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from contextos.connectors.fake import FakeConnector
|
|
17
|
+
from contextos.connectors.json_import import JsonImportConnector
|
|
18
|
+
from contextos.connectors.local_files import LocalFileConnector
|
|
19
|
+
from contextos.connectors.manager import ConnectorManager
|
|
20
|
+
from contextos.connectors.models import ConnectorItem, RetentionPolicy
|
|
21
|
+
from contextos.core.enums import SecretDetectionMode
|
|
22
|
+
from contextos.embedding.deterministic import DeterministicEmbedding
|
|
23
|
+
from contextos.services.extraction import RuleBasedMemoryExtractor
|
|
24
|
+
from contextos.services.ingestion import IngestionPipeline
|
|
25
|
+
from contextos.services.secret_scanner import PatternSecretScanner
|
|
26
|
+
from contextos.services.temporal import TemporalMemoryService
|
|
27
|
+
from contextos.services.token_counter import DeterministicWordTokenCounter
|
|
28
|
+
from contextos.storage.connector_repo import SqliteConnectorRepository
|
|
29
|
+
from contextos.storage.database import Database
|
|
30
|
+
from contextos.storage.event_repo import SqliteEventRepository
|
|
31
|
+
from contextos.storage.lexical.bm25 import BM25Index
|
|
32
|
+
from contextos.storage.memory_repo import SqliteMemoryRepository
|
|
33
|
+
from contextos.storage.vector.in_memory import InMemoryVectorStore
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CountingSecretScanner(PatternSecretScanner):
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
super().__init__()
|
|
39
|
+
self.calls = 0
|
|
40
|
+
|
|
41
|
+
def scan(self, text: str, *args, **kwargs):
|
|
42
|
+
self.calls += 1
|
|
43
|
+
return super().scan(text, *args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CountingExtractor(RuleBasedMemoryExtractor):
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.calls = 0
|
|
50
|
+
|
|
51
|
+
def extract(self, text: str, *args, **kwargs):
|
|
52
|
+
self.calls += 1
|
|
53
|
+
return super().extract(text, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CountingTemporalService(TemporalMemoryService):
|
|
57
|
+
def __init__(self, memory_repo) -> None:
|
|
58
|
+
super().__init__(memory_repo)
|
|
59
|
+
self.accept_calls = 0
|
|
60
|
+
|
|
61
|
+
async def accept(self, candidate, *, provenance_event_id=None):
|
|
62
|
+
self.accept_calls += 1
|
|
63
|
+
return await super().accept(candidate, provenance_event_id=provenance_event_id)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class BenchmarkScenarioResult:
|
|
68
|
+
name: str
|
|
69
|
+
items_count: int
|
|
70
|
+
duration_ms: float
|
|
71
|
+
items_per_sec: float
|
|
72
|
+
scanned: int
|
|
73
|
+
accepted: int
|
|
74
|
+
unchanged: int
|
|
75
|
+
updated: int
|
|
76
|
+
rejected: int
|
|
77
|
+
failed: int
|
|
78
|
+
privacy_calls: int
|
|
79
|
+
extractor_calls: int
|
|
80
|
+
temporal_calls: int
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def run_connector_benchmark() -> list[BenchmarkScenarioResult]:
|
|
84
|
+
tmp_dir = Path(tempfile.mkdtemp())
|
|
85
|
+
db_path = tmp_dir / "bench_connectors.db"
|
|
86
|
+
db = Database(db_path)
|
|
87
|
+
await db.initialize()
|
|
88
|
+
conn = db.connection()
|
|
89
|
+
|
|
90
|
+
memory_repo = SqliteMemoryRepository(conn)
|
|
91
|
+
event_repo = SqliteEventRepository(conn)
|
|
92
|
+
connector_repo = SqliteConnectorRepository(conn)
|
|
93
|
+
|
|
94
|
+
scanner = CountingSecretScanner()
|
|
95
|
+
extractor = CountingExtractor()
|
|
96
|
+
token_counter = DeterministicWordTokenCounter()
|
|
97
|
+
embedding = DeterministicEmbedding(16)
|
|
98
|
+
lexical = BM25Index()
|
|
99
|
+
vector = InMemoryVectorStore(16)
|
|
100
|
+
|
|
101
|
+
ingestion = IngestionPipeline(
|
|
102
|
+
secret_scanner=scanner,
|
|
103
|
+
memory_extractor=extractor,
|
|
104
|
+
memory_repo=memory_repo,
|
|
105
|
+
event_repo=event_repo,
|
|
106
|
+
embedding_service=embedding,
|
|
107
|
+
vector_store=vector,
|
|
108
|
+
lexical_index=lexical,
|
|
109
|
+
token_counter=token_counter,
|
|
110
|
+
secret_detection_mode=SecretDetectionMode.STRICT,
|
|
111
|
+
)
|
|
112
|
+
temporal = CountingTemporalService(memory_repo)
|
|
113
|
+
manager = ConnectorManager(
|
|
114
|
+
state_repo=connector_repo,
|
|
115
|
+
ingestion=ingestion,
|
|
116
|
+
temporal=temporal,
|
|
117
|
+
retention_policy=RetentionPolicy.KEEP_DERIVED_MEMORY,
|
|
118
|
+
memory_repo=memory_repo,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
results: list[BenchmarkScenarioResult] = []
|
|
122
|
+
|
|
123
|
+
def reset_counters():
|
|
124
|
+
scanner.calls = 0
|
|
125
|
+
extractor.calls = 0
|
|
126
|
+
temporal.accept_calls = 0
|
|
127
|
+
|
|
128
|
+
# --- Scenario 1: Initial Sync (100 items) ---
|
|
129
|
+
items_100 = [
|
|
130
|
+
ConnectorItem(
|
|
131
|
+
external_id=f"doc_{i}",
|
|
132
|
+
source_type="fake",
|
|
133
|
+
source_uri=f"fake://doc_{i}",
|
|
134
|
+
content=f"I am working on Project Atlas module {i}. Project Atlas uses Python.",
|
|
135
|
+
revision=f"rev_1_{i}",
|
|
136
|
+
)
|
|
137
|
+
for i in range(100)
|
|
138
|
+
]
|
|
139
|
+
connector_100 = FakeConnector("fake-100", items_100)
|
|
140
|
+
manager.register(connector_100)
|
|
141
|
+
|
|
142
|
+
reset_counters()
|
|
143
|
+
started = time.perf_counter()
|
|
144
|
+
sync_res = await manager.sync("fake-100")
|
|
145
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
146
|
+
results.append(
|
|
147
|
+
BenchmarkScenarioResult(
|
|
148
|
+
name="Initial Sync (100 items)",
|
|
149
|
+
items_count=100,
|
|
150
|
+
duration_ms=dur_ms,
|
|
151
|
+
items_per_sec=100 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
152
|
+
scanned=sync_res.scanned,
|
|
153
|
+
accepted=sync_res.accepted,
|
|
154
|
+
unchanged=sync_res.unchanged,
|
|
155
|
+
updated=sync_res.updated,
|
|
156
|
+
rejected=sync_res.rejected,
|
|
157
|
+
failed=sync_res.failed,
|
|
158
|
+
privacy_calls=scanner.calls,
|
|
159
|
+
extractor_calls=extractor.calls,
|
|
160
|
+
temporal_calls=temporal.accept_calls,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# --- Scenario 2: Unchanged Second Sync (100 items) ---
|
|
165
|
+
reset_counters()
|
|
166
|
+
started = time.perf_counter()
|
|
167
|
+
sync_res = await manager.sync("fake-100")
|
|
168
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
169
|
+
results.append(
|
|
170
|
+
BenchmarkScenarioResult(
|
|
171
|
+
name="Unchanged Second Sync (100 items)",
|
|
172
|
+
items_count=100,
|
|
173
|
+
duration_ms=dur_ms,
|
|
174
|
+
items_per_sec=100 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
175
|
+
scanned=sync_res.scanned,
|
|
176
|
+
accepted=sync_res.accepted,
|
|
177
|
+
unchanged=sync_res.unchanged,
|
|
178
|
+
updated=sync_res.updated,
|
|
179
|
+
rejected=sync_res.rejected,
|
|
180
|
+
failed=sync_res.failed,
|
|
181
|
+
privacy_calls=scanner.calls,
|
|
182
|
+
extractor_calls=extractor.calls,
|
|
183
|
+
temporal_calls=temporal.accept_calls,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# --- Scenario 3: 10% Changed Sync (100 items, 10 modified) ---
|
|
188
|
+
changed_items_100 = list(items_100)
|
|
189
|
+
for i in range(10):
|
|
190
|
+
changed_items_100[i] = ConnectorItem(
|
|
191
|
+
external_id=f"doc_{i}",
|
|
192
|
+
source_type="fake",
|
|
193
|
+
source_uri=f"fake://doc_{i}",
|
|
194
|
+
content=f"I am working on Project Atlas module {i}. Now using Rust.",
|
|
195
|
+
revision=f"rev_2_{i}",
|
|
196
|
+
)
|
|
197
|
+
connector_100.items = changed_items_100
|
|
198
|
+
|
|
199
|
+
reset_counters()
|
|
200
|
+
started = time.perf_counter()
|
|
201
|
+
sync_res = await manager.sync("fake-100")
|
|
202
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
203
|
+
results.append(
|
|
204
|
+
BenchmarkScenarioResult(
|
|
205
|
+
name="10% Changed Sync (100 items)",
|
|
206
|
+
items_count=100,
|
|
207
|
+
duration_ms=dur_ms,
|
|
208
|
+
items_per_sec=100 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
209
|
+
scanned=sync_res.scanned,
|
|
210
|
+
accepted=sync_res.accepted,
|
|
211
|
+
unchanged=sync_res.unchanged,
|
|
212
|
+
updated=sync_res.updated,
|
|
213
|
+
rejected=sync_res.rejected,
|
|
214
|
+
failed=sync_res.failed,
|
|
215
|
+
privacy_calls=scanner.calls,
|
|
216
|
+
extractor_calls=extractor.calls,
|
|
217
|
+
temporal_calls=temporal.accept_calls,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# --- Scenario 4: Privacy Rejection Batch (20 items with secrets) ---
|
|
222
|
+
secret_items = [
|
|
223
|
+
ConnectorItem(
|
|
224
|
+
external_id=f"sec_{i}",
|
|
225
|
+
source_type="fake",
|
|
226
|
+
source_uri=f"fake://sec_{i}",
|
|
227
|
+
content=f"I prefer secret key: sk-proj-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
228
|
+
revision="rev_sec",
|
|
229
|
+
)
|
|
230
|
+
for i in range(20)
|
|
231
|
+
]
|
|
232
|
+
connector_sec = FakeConnector("fake-sec", secret_items)
|
|
233
|
+
manager.register(connector_sec)
|
|
234
|
+
|
|
235
|
+
reset_counters()
|
|
236
|
+
started = time.perf_counter()
|
|
237
|
+
sync_res = await manager.sync("fake-sec")
|
|
238
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
239
|
+
results.append(
|
|
240
|
+
BenchmarkScenarioResult(
|
|
241
|
+
name="Privacy Rejection Batch (20 items)",
|
|
242
|
+
items_count=20,
|
|
243
|
+
duration_ms=dur_ms,
|
|
244
|
+
items_per_sec=20 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
245
|
+
scanned=sync_res.scanned,
|
|
246
|
+
accepted=sync_res.accepted,
|
|
247
|
+
unchanged=sync_res.unchanged,
|
|
248
|
+
updated=sync_res.updated,
|
|
249
|
+
rejected=sync_res.rejected,
|
|
250
|
+
failed=sync_res.failed,
|
|
251
|
+
privacy_calls=scanner.calls,
|
|
252
|
+
extractor_calls=extractor.calls,
|
|
253
|
+
temporal_calls=temporal.accept_calls,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# --- Scenario 5: JSONL Import Scan (100 items) ---
|
|
258
|
+
jsonl_file = tmp_dir / "import.jsonl"
|
|
259
|
+
jsonl_lines = [
|
|
260
|
+
json.dumps({"id": f"j_{i}", "content": f"I am working on Project Gamma {i}. Project Gamma uses PostgreSQL."})
|
|
261
|
+
for i in range(100)
|
|
262
|
+
]
|
|
263
|
+
jsonl_file.write_text("\n".join(jsonl_lines), encoding="utf-8")
|
|
264
|
+
json_conn = JsonImportConnector("json-100", jsonl_file)
|
|
265
|
+
manager.register(json_conn)
|
|
266
|
+
|
|
267
|
+
reset_counters()
|
|
268
|
+
started = time.perf_counter()
|
|
269
|
+
sync_res = await manager.sync("json-100")
|
|
270
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
271
|
+
results.append(
|
|
272
|
+
BenchmarkScenarioResult(
|
|
273
|
+
name="JSONL Import Scan (100 items)",
|
|
274
|
+
items_count=100,
|
|
275
|
+
duration_ms=dur_ms,
|
|
276
|
+
items_per_sec=100 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
277
|
+
scanned=sync_res.scanned,
|
|
278
|
+
accepted=sync_res.accepted,
|
|
279
|
+
unchanged=sync_res.unchanged,
|
|
280
|
+
updated=sync_res.updated,
|
|
281
|
+
rejected=sync_res.rejected,
|
|
282
|
+
failed=sync_res.failed,
|
|
283
|
+
privacy_calls=scanner.calls,
|
|
284
|
+
extractor_calls=extractor.calls,
|
|
285
|
+
temporal_calls=temporal.accept_calls,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# --- Scenario 6: Local File Scan (50 files) ---
|
|
290
|
+
files_dir = tmp_dir / "files"
|
|
291
|
+
files_dir.mkdir()
|
|
292
|
+
for i in range(50):
|
|
293
|
+
(files_dir / f"note_{i}.txt").write_text(
|
|
294
|
+
f"I am working on Project Beta module {i}. Project Beta uses Rust.", encoding="utf-8"
|
|
295
|
+
)
|
|
296
|
+
file_conn = LocalFileConnector("files-50", [files_dir])
|
|
297
|
+
manager.register(file_conn)
|
|
298
|
+
|
|
299
|
+
reset_counters()
|
|
300
|
+
started = time.perf_counter()
|
|
301
|
+
sync_res = await manager.sync("files-50")
|
|
302
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
303
|
+
results.append(
|
|
304
|
+
BenchmarkScenarioResult(
|
|
305
|
+
name="Local File Scan (50 files)",
|
|
306
|
+
items_count=50,
|
|
307
|
+
duration_ms=dur_ms,
|
|
308
|
+
items_per_sec=50 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
309
|
+
scanned=sync_res.scanned,
|
|
310
|
+
accepted=sync_res.accepted,
|
|
311
|
+
unchanged=sync_res.unchanged,
|
|
312
|
+
updated=sync_res.updated,
|
|
313
|
+
rejected=sync_res.rejected,
|
|
314
|
+
failed=sync_res.failed,
|
|
315
|
+
privacy_calls=scanner.calls,
|
|
316
|
+
extractor_calls=extractor.calls,
|
|
317
|
+
temporal_calls=temporal.accept_calls,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# --- Scenario 7: Scale Initial Sync (1000 items) ---
|
|
322
|
+
items_1000 = [
|
|
323
|
+
ConnectorItem(
|
|
324
|
+
external_id=f"big_{i}",
|
|
325
|
+
source_type="fake",
|
|
326
|
+
source_uri=f"fake://big_{i}",
|
|
327
|
+
content=f"I am working on Project Scale component {i}. Uses TypeScript.",
|
|
328
|
+
revision=f"rev_scale_{i}",
|
|
329
|
+
)
|
|
330
|
+
for i in range(1000)
|
|
331
|
+
]
|
|
332
|
+
connector_1000 = FakeConnector("fake-1000", items_1000)
|
|
333
|
+
manager.register(connector_1000)
|
|
334
|
+
|
|
335
|
+
reset_counters()
|
|
336
|
+
started = time.perf_counter()
|
|
337
|
+
sync_res = await manager.sync("fake-1000")
|
|
338
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
339
|
+
results.append(
|
|
340
|
+
BenchmarkScenarioResult(
|
|
341
|
+
name="Initial Sync (1000 items)",
|
|
342
|
+
items_count=1000,
|
|
343
|
+
duration_ms=dur_ms,
|
|
344
|
+
items_per_sec=1000 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
345
|
+
scanned=sync_res.scanned,
|
|
346
|
+
accepted=sync_res.accepted,
|
|
347
|
+
unchanged=sync_res.unchanged,
|
|
348
|
+
updated=sync_res.updated,
|
|
349
|
+
rejected=sync_res.rejected,
|
|
350
|
+
failed=sync_res.failed,
|
|
351
|
+
privacy_calls=scanner.calls,
|
|
352
|
+
extractor_calls=extractor.calls,
|
|
353
|
+
temporal_calls=temporal.accept_calls,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# --- Scenario 8: Scale Unchanged Second Sync (1000 items) ---
|
|
358
|
+
reset_counters()
|
|
359
|
+
started = time.perf_counter()
|
|
360
|
+
sync_res = await manager.sync("fake-1000")
|
|
361
|
+
dur_ms = (time.perf_counter() - started) * 1000
|
|
362
|
+
results.append(
|
|
363
|
+
BenchmarkScenarioResult(
|
|
364
|
+
name="Unchanged Second Sync (1000 items)",
|
|
365
|
+
items_count=1000,
|
|
366
|
+
duration_ms=dur_ms,
|
|
367
|
+
items_per_sec=1000 / (dur_ms / 1000) if dur_ms > 0 else 0,
|
|
368
|
+
scanned=sync_res.scanned,
|
|
369
|
+
accepted=sync_res.accepted,
|
|
370
|
+
unchanged=sync_res.unchanged,
|
|
371
|
+
updated=sync_res.updated,
|
|
372
|
+
rejected=sync_res.rejected,
|
|
373
|
+
failed=sync_res.failed,
|
|
374
|
+
privacy_calls=scanner.calls,
|
|
375
|
+
extractor_calls=extractor.calls,
|
|
376
|
+
temporal_calls=temporal.accept_calls,
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
await db.close()
|
|
381
|
+
return results
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def main() -> None:
|
|
385
|
+
results = asyncio.run(run_connector_benchmark())
|
|
386
|
+
|
|
387
|
+
print("=" * 80)
|
|
388
|
+
print("LOCAL DEVELOPMENT SYNTHETIC CONNECTOR BENCHMARK")
|
|
389
|
+
print("=" * 80)
|
|
390
|
+
print()
|
|
391
|
+
print(
|
|
392
|
+
f"{'Scenario':<38} | {'Items':<6} | {'Time (ms)':<9} | {'Items/sec':<10} | "
|
|
393
|
+
f"{'Accepted':<8} | {'Unchanged':<9} | {'Pipeline Calls (Scanner/Extract/Temporal)'}"
|
|
394
|
+
)
|
|
395
|
+
print("-" * 115)
|
|
396
|
+
|
|
397
|
+
for r in results:
|
|
398
|
+
pipe_str = f"{r.privacy_calls}/{r.extractor_calls}/{r.temporal_calls}"
|
|
399
|
+
print(
|
|
400
|
+
f"{r.name:<38} | {r.items_count:<6} | {r.duration_ms:>9.2f} | "
|
|
401
|
+
f"{r.items_per_sec:>10.1f} | {r.accepted:<8} | {r.unchanged:<9} | {pipe_str}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
print()
|
|
405
|
+
print("UNCHANGED SKIP INVARIANT SUMMARY:")
|
|
406
|
+
print("-" * 50)
|
|
407
|
+
unchanged_100 = next(r for r in results if r.name == "Unchanged Second Sync (100 items)")
|
|
408
|
+
unchanged_1000 = next(r for r in results if r.name == "Unchanged Second Sync (1000 items)")
|
|
409
|
+
print(
|
|
410
|
+
f"100 Unchanged Sync: Unchanged={unchanged_100.unchanged}/100, "
|
|
411
|
+
f"Pipeline Calls (Scanner/Extract/Temporal) = {unchanged_100.privacy_calls}/{unchanged_100.extractor_calls}/{unchanged_100.temporal_calls}"
|
|
412
|
+
)
|
|
413
|
+
print(
|
|
414
|
+
f"1000 Unchanged Sync: Unchanged={unchanged_1000.unchanged}/1000, "
|
|
415
|
+
f"Pipeline Calls (Scanner/Extract/Temporal) = {unchanged_1000.privacy_calls}/{unchanged_1000.extractor_calls}/{unchanged_1000.temporal_calls}"
|
|
416
|
+
)
|
|
417
|
+
print("* Instrumentation Note: Scanner calls count internal PatternSecretScanner checks across content and candidates (14 per item).")
|
|
418
|
+
print()
|
|
419
|
+
print("Benchmark complete.")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if __name__ == "__main__":
|
|
423
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""LOCAL DEVELOPMENT SYNTHETIC EXPLAINABILITY BENCHMARK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import statistics
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from contextos.config.settings import Settings
|
|
13
|
+
from contextos.core.models import CompilationConfig, ContextBudget, RetrievalQuery
|
|
14
|
+
from contextos.daemon.wiring import wire_services
|
|
15
|
+
from contextos.services.explainability import ExplainabilityService, ExplanationRequest
|
|
16
|
+
from contextos.core.models import Memory
|
|
17
|
+
from contextos.core.enums import MemoryStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _p95(values: list[float]) -> float:
|
|
21
|
+
ordered = sorted(values)
|
|
22
|
+
return ordered[min(len(ordered) - 1, int(0.95 * len(ordered)))]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def measure() -> dict:
|
|
26
|
+
report: dict[str, object] = {
|
|
27
|
+
"label": "LOCAL DEVELOPMENT SYNTHETIC EXPLAINABILITY BENCHMARK",
|
|
28
|
+
"iterations": 5,
|
|
29
|
+
"datasets": {},
|
|
30
|
+
}
|
|
31
|
+
for size in (100, 1000):
|
|
32
|
+
with tempfile.TemporaryDirectory(prefix=f"contextos-explain-{size}-") as folder:
|
|
33
|
+
services = await wire_services(Settings(
|
|
34
|
+
daemon={"data_dir": Path(folder)}, embedding={"model": "deterministic"}
|
|
35
|
+
))
|
|
36
|
+
try:
|
|
37
|
+
for index in range(size):
|
|
38
|
+
await services["memory_repo"].create(Memory(
|
|
39
|
+
content=f"I use Python automation tool number {index} for local project task {index % 31}.",
|
|
40
|
+
status=MemoryStatus.ACTIVE,
|
|
41
|
+
source_type="benchmark",
|
|
42
|
+
))
|
|
43
|
+
await services["retrieval_index"].ensure_current()
|
|
44
|
+
explanation_service = ExplainabilityService(services)
|
|
45
|
+
retrieval_ms: list[float] = []
|
|
46
|
+
full_explain_ms: list[float] = []
|
|
47
|
+
compile_ms: list[float] = []
|
|
48
|
+
trace_bytes: list[int] = []
|
|
49
|
+
candidate_counts: list[int] = []
|
|
50
|
+
trace_overhead_ms: list[float] = []
|
|
51
|
+
measured_pipeline_ms: list[float] = []
|
|
52
|
+
for _ in range(5):
|
|
53
|
+
query = "Python automation local project"
|
|
54
|
+
started = time.perf_counter()
|
|
55
|
+
retrieved = await services["retrieval"].retrieve(
|
|
56
|
+
RetrievalQuery(text=query, k=25)
|
|
57
|
+
)
|
|
58
|
+
retrieval_ms.append((time.perf_counter() - started) * 1000)
|
|
59
|
+
started = time.perf_counter()
|
|
60
|
+
explained = await explanation_service.explain(ExplanationRequest(
|
|
61
|
+
query=query, budget=1000, limit=25, graph=False,
|
|
62
|
+
))
|
|
63
|
+
full_explain_ms.append((time.perf_counter() - started) * 1000)
|
|
64
|
+
candidate_counts.append(len(explained.candidates))
|
|
65
|
+
trace_bytes.append(len(explained.model_dump_json().encode("utf-8")))
|
|
66
|
+
trace_overhead_ms.append(explained.explanation_overhead_ms)
|
|
67
|
+
measured_pipeline_ms.append(explained.measured_pipeline_ms)
|
|
68
|
+
|
|
69
|
+
started = time.perf_counter()
|
|
70
|
+
compile_retrieved = await services["retrieval"].retrieve(RetrievalQuery(text=query, k=25))
|
|
71
|
+
selection = services["optimizer"].optimize(query, compile_retrieved.memories,
|
|
72
|
+
ContextBudget(max_tokens=1000))
|
|
73
|
+
await services["compilation"].compile(
|
|
74
|
+
query, selection, CompilationConfig(budget=1000)
|
|
75
|
+
)
|
|
76
|
+
compile_ms.append((time.perf_counter() - started) * 1000)
|
|
77
|
+
baseline = statistics.mean(compile_ms)
|
|
78
|
+
explained_mean = statistics.mean(full_explain_ms)
|
|
79
|
+
report["datasets"][str(size)] = {
|
|
80
|
+
"mean_ms": round(explained_mean, 3),
|
|
81
|
+
"median_ms": round(statistics.median(full_explain_ms), 3),
|
|
82
|
+
"p95_ms": round(_p95(full_explain_ms), 3),
|
|
83
|
+
"retrieval_without_explanation_mean_ms": round(statistics.mean(retrieval_ms), 3),
|
|
84
|
+
"compile_without_explanation_mean_ms": round(baseline, 3),
|
|
85
|
+
"compile_with_explanation_mean_ms": round(explained_mean, 3),
|
|
86
|
+
"measured_pipeline_mean_ms": round(statistics.mean(measured_pipeline_ms), 3),
|
|
87
|
+
"explanation_overhead_ms": round(statistics.mean(trace_overhead_ms), 3),
|
|
88
|
+
"explanation_overhead_percent": round(statistics.mean(trace_overhead_ms) / statistics.mean(measured_pipeline_ms) * 100, 2) if statistics.mean(measured_pipeline_ms) else 0.0,
|
|
89
|
+
"trace_size_bytes_mean": round(statistics.mean(trace_bytes)),
|
|
90
|
+
"candidate_count_mean": round(statistics.mean(candidate_counts), 2),
|
|
91
|
+
"samples": 5,
|
|
92
|
+
}
|
|
93
|
+
finally:
|
|
94
|
+
await services["database"].close()
|
|
95
|
+
return report
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main() -> None:
|
|
99
|
+
print(json.dumps(asyncio.run(measure()), indent=2))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|