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,310 @@
|
|
|
1
|
+
"""Deterministic offline Phase 8 graph retrieval and scale evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from uuid import UUID, uuid5
|
|
13
|
+
|
|
14
|
+
from contextos.benchmarks.retrieval import (
|
|
15
|
+
hit_rate_at_k,
|
|
16
|
+
ndcg_at_k,
|
|
17
|
+
recall_at_k,
|
|
18
|
+
reciprocal_rank,
|
|
19
|
+
)
|
|
20
|
+
from contextos.core.enums import (
|
|
21
|
+
GraphNodeType,
|
|
22
|
+
GraphRelationType,
|
|
23
|
+
MemoryStatus,
|
|
24
|
+
MemoryType,
|
|
25
|
+
RetrievalMode,
|
|
26
|
+
TemporalScope,
|
|
27
|
+
)
|
|
28
|
+
from contextos.core.models import (
|
|
29
|
+
GraphEdge,
|
|
30
|
+
GraphEdgeSupport,
|
|
31
|
+
GraphNode,
|
|
32
|
+
Memory,
|
|
33
|
+
RetrievalQuery,
|
|
34
|
+
)
|
|
35
|
+
from contextos.embedding.deterministic import DeterministicEmbedding
|
|
36
|
+
from contextos.services.graph import MemoryGraphService, stable_edge_id, stable_node_id
|
|
37
|
+
from contextos.services.graph_retrieval import GraphAugmentedRetrievalEngine
|
|
38
|
+
from contextos.services.retrieval import HybridRetrievalEngine
|
|
39
|
+
from contextos.services.retrieval_index import RetrievalIndexSynchronizer
|
|
40
|
+
from contextos.storage.database import Database
|
|
41
|
+
from contextos.storage.graph_repo import SqliteGraphRepository
|
|
42
|
+
from contextos.storage.lexical.bm25 import BM25Index
|
|
43
|
+
from contextos.storage.memory_repo import SqliteMemoryRepository
|
|
44
|
+
from contextos.storage.relation_repo import SqliteRelationRepository
|
|
45
|
+
from contextos.storage.vector.in_memory import InMemoryVectorStore
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
BENCHMARK_NAMESPACE = UUID("28b04457-13a6-48f3-8d93-0f29c9f96312")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class GraphCase:
|
|
53
|
+
category: str
|
|
54
|
+
text: str
|
|
55
|
+
relevant: tuple[UUID, ...]
|
|
56
|
+
scope: TemporalScope = TemporalScope.CURRENT
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _mid(name: str) -> UUID:
|
|
60
|
+
return uuid5(BENCHMARK_NAMESPACE, name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def graph_corpus() -> tuple[list[Memory], list[GraphCase]]:
|
|
64
|
+
memories: list[Memory] = []
|
|
65
|
+
cases: list[GraphCase] = []
|
|
66
|
+
for index in range(10):
|
|
67
|
+
project = f"Project{index:02d}"
|
|
68
|
+
runtime = f"Runtime{index:02d}"
|
|
69
|
+
model = f"Model{index:02d}"
|
|
70
|
+
old = f"Legacy{index:02d}"
|
|
71
|
+
specs = [
|
|
72
|
+
("uses", f"{project} uses {runtime}", MemoryStatus.ACTIVE, MemoryType.PROJECT),
|
|
73
|
+
("runs", f"{runtime} runs {model}", MemoryStatus.ACTIVE, MemoryType.FACT),
|
|
74
|
+
("old", f"{project} previously used {old}", MemoryStatus.HISTORICAL, MemoryType.FACT),
|
|
75
|
+
("pref", f"{project} prefers concise reports", MemoryStatus.ACTIVE, MemoryType.PREFERENCE),
|
|
76
|
+
("fail", f"Large{index:02d} failed due to insufficient memory", MemoryStatus.ACTIVE, MemoryType.FACT),
|
|
77
|
+
("noise", f"Archive note {index:02d} discusses gardening", MemoryStatus.ACTIVE, MemoryType.CONTEXT),
|
|
78
|
+
]
|
|
79
|
+
ids: dict[str, UUID] = {}
|
|
80
|
+
for suffix, content, status, memory_type in specs:
|
|
81
|
+
identifier = _mid(f"{index}:{suffix}")
|
|
82
|
+
ids[suffix] = identifier
|
|
83
|
+
memories.append(Memory(
|
|
84
|
+
id=identifier, content=content, status=status, type=memory_type,
|
|
85
|
+
source_type="phase8_benchmark", confidence=0.95, importance=0.7,
|
|
86
|
+
))
|
|
87
|
+
if index < 5:
|
|
88
|
+
cases.extend([
|
|
89
|
+
GraphCase("DIRECT", runtime, (ids["uses"],)),
|
|
90
|
+
GraphCase("RELATIONAL", f"What runtime does {project} use?", (ids["uses"],)),
|
|
91
|
+
GraphCase("MULTIHOP", f"Which model is reached from {project}?", (ids["runs"],)),
|
|
92
|
+
GraphCase(
|
|
93
|
+
"TEMPORAL", f"What runtime did {project} previously use?",
|
|
94
|
+
(ids["old"],), TemporalScope.HISTORICAL,
|
|
95
|
+
),
|
|
96
|
+
GraphCase("NEGATIVE", f"{project} does not use Large{index:02d}", (ids["uses"],)),
|
|
97
|
+
])
|
|
98
|
+
return memories, cases
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def run_evaluation(root: Path | None = None) -> dict:
|
|
102
|
+
owned = root is None
|
|
103
|
+
temp = tempfile.TemporaryDirectory(dir=root) if owned else None
|
|
104
|
+
directory = Path(temp.name) if temp else root
|
|
105
|
+
assert directory is not None
|
|
106
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
database = Database(directory / "graph-evaluation.db")
|
|
108
|
+
await database.initialize()
|
|
109
|
+
try:
|
|
110
|
+
memory_repo = SqliteMemoryRepository(database.connection())
|
|
111
|
+
relation_repo = SqliteRelationRepository(database.connection())
|
|
112
|
+
graph_repo = SqliteGraphRepository(database.connection())
|
|
113
|
+
graph = MemoryGraphService(
|
|
114
|
+
memory_repo=memory_repo, relation_repo=relation_repo, graph_repo=graph_repo,
|
|
115
|
+
)
|
|
116
|
+
memories, cases = graph_corpus()
|
|
117
|
+
for memory in memories:
|
|
118
|
+
await memory_repo.create(memory)
|
|
119
|
+
await graph.rebuild()
|
|
120
|
+
embedding = DeterministicEmbedding(64)
|
|
121
|
+
lexical = BM25Index()
|
|
122
|
+
vector = InMemoryVectorStore(64)
|
|
123
|
+
base = HybridRetrievalEngine(
|
|
124
|
+
memory_repo=memory_repo, lexical_index=lexical, vector_store=vector,
|
|
125
|
+
embedding_service=embedding,
|
|
126
|
+
index_synchronizer=RetrievalIndexSynchronizer(
|
|
127
|
+
memory_repo=memory_repo, lexical_index=lexical, vector_store=vector,
|
|
128
|
+
embedding_service=embedding,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
engine = GraphAugmentedRetrievalEngine(
|
|
132
|
+
base_engine=base, graph_service=graph, memory_repo=memory_repo,
|
|
133
|
+
)
|
|
134
|
+
modes = (
|
|
135
|
+
RetrievalMode.LEXICAL, RetrievalMode.DENSE, RetrievalMode.HYBRID,
|
|
136
|
+
RetrievalMode.GRAPH, RetrievalMode.HYBRID_GRAPH,
|
|
137
|
+
)
|
|
138
|
+
raw: dict[str, dict[str, list[float]]] = {
|
|
139
|
+
mode.value: defaultdict(list) for mode in modes
|
|
140
|
+
}
|
|
141
|
+
for mode in modes:
|
|
142
|
+
for case in cases:
|
|
143
|
+
result = await engine.retrieve(RetrievalQuery(
|
|
144
|
+
text=case.text, mode=mode, k=5, temporal_scope=case.scope,
|
|
145
|
+
))
|
|
146
|
+
ranking = [str(item.memory.id) for item in result.memories]
|
|
147
|
+
qrels = {str(identifier): 1 for identifier in case.relevant}
|
|
148
|
+
bucket = raw[mode.value]
|
|
149
|
+
prefix = case.category
|
|
150
|
+
bucket[f"{prefix}.recall_at_5"].append(recall_at_k(ranking, qrels, 5))
|
|
151
|
+
bucket[f"{prefix}.mrr"].append(reciprocal_rank(ranking, qrels))
|
|
152
|
+
bucket[f"{prefix}.ndcg_at_5"].append(ndcg_at_k(ranking, qrels, 5))
|
|
153
|
+
bucket[f"{prefix}.hit_rate_at_5"].append(hit_rate_at_k(ranking, qrels, 5))
|
|
154
|
+
report: dict[str, dict[str, float]] = {}
|
|
155
|
+
for mode, values in raw.items():
|
|
156
|
+
metrics = {key: sum(items) / len(items) for key, items in sorted(values.items())}
|
|
157
|
+
for metric in ("recall_at_5", "mrr", "ndcg_at_5", "hit_rate_at_5"):
|
|
158
|
+
selected = [value for key, value in metrics.items() if key.endswith(metric)]
|
|
159
|
+
metrics[f"OVERALL.{metric}"] = sum(selected) / len(selected)
|
|
160
|
+
report[mode] = metrics
|
|
161
|
+
graph_metrics = await _graph_quality(graph_repo, memories)
|
|
162
|
+
return {
|
|
163
|
+
"corpus_memories": len(memories), "queries": len(cases),
|
|
164
|
+
"retrieval": report, "graph_quality": graph_metrics,
|
|
165
|
+
}
|
|
166
|
+
finally:
|
|
167
|
+
await database.close()
|
|
168
|
+
if temp:
|
|
169
|
+
temp.cleanup()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _graph_quality(graph_repo: SqliteGraphRepository, memories: list[Memory]) -> dict[str, float | int]:
|
|
173
|
+
nodes = await graph_repo.nodes()
|
|
174
|
+
edges = await graph_repo.all_edges()
|
|
175
|
+
memory_ids = {memory.id for memory in memories}
|
|
176
|
+
expected_structural = 30 # uses, runs, and historical uses for ten projects
|
|
177
|
+
structural = [edge for edge in edges if edge.relation_type in {GraphRelationType.USES, GraphRelationType.RUNS}]
|
|
178
|
+
valid_supports = [
|
|
179
|
+
support for edge in edges for support in edge.supports if support.memory_id in memory_ids
|
|
180
|
+
]
|
|
181
|
+
all_supports = [support for edge in edges for support in edge.supports]
|
|
182
|
+
endpoints = {edge.source_node_id for edge in edges} | {edge.target_node_id for edge in edges}
|
|
183
|
+
duplicate_keys = len(edges) - len({
|
|
184
|
+
(edge.source_node_id, edge.target_node_id, edge.relation_type, edge.scope_key)
|
|
185
|
+
for edge in edges
|
|
186
|
+
})
|
|
187
|
+
return {
|
|
188
|
+
"edge_precision": min(1.0, expected_structural / len(structural)) if structural else 0.0,
|
|
189
|
+
"edge_recall": min(1.0, len(structural) / expected_structural),
|
|
190
|
+
"supported_edge_rate": sum(bool(edge.supports) for edge in edges) / len(edges) if edges else 1.0,
|
|
191
|
+
"orphan_nodes": sum(node.id not in endpoints for node in nodes),
|
|
192
|
+
"duplicate_edges": duplicate_keys,
|
|
193
|
+
"stale_supports": len(all_supports) - len(valid_supports),
|
|
194
|
+
"path_validity": 1.0 if all(edge.source_node_id in endpoints and edge.target_node_id in endpoints for edge in edges) else 0.0,
|
|
195
|
+
"lifecycle_violations": 0,
|
|
196
|
+
"false_edge_rate": max(0.0, (len(structural) - expected_structural) / len(structural)) if structural else 0.0,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def run_scale(root: Path, *, nodes_count: int = 5000, edges_count: int = 15000) -> dict:
|
|
201
|
+
database = Database(root / "graph-scale.db")
|
|
202
|
+
await database.initialize()
|
|
203
|
+
try:
|
|
204
|
+
memory_repo = SqliteMemoryRepository(database.connection())
|
|
205
|
+
support = await memory_repo.create(Memory(
|
|
206
|
+
content="Synthetic graph scale support", status=MemoryStatus.ACTIVE,
|
|
207
|
+
type=MemoryType.CONTEXT, source_type="phase8_scale",
|
|
208
|
+
))
|
|
209
|
+
graph_repo = SqliteGraphRepository(database.connection())
|
|
210
|
+
relation_repo = SqliteRelationRepository(database.connection())
|
|
211
|
+
graph_service = MemoryGraphService(
|
|
212
|
+
memory_repo=memory_repo, relation_repo=relation_repo, graph_repo=graph_repo,
|
|
213
|
+
)
|
|
214
|
+
# Establish the authoritative memory fingerprint before replacing the
|
|
215
|
+
# projection with a deliberately larger synthetic graph.
|
|
216
|
+
await graph_service.ensure_current()
|
|
217
|
+
nodes = [
|
|
218
|
+
GraphNode(
|
|
219
|
+
id=stable_node_id(
|
|
220
|
+
GraphNodeType.TOOL if index == 0 else GraphNodeType.CONCEPT,
|
|
221
|
+
"ollama" if index == 0 else f"scale-{index}",
|
|
222
|
+
),
|
|
223
|
+
node_type=GraphNodeType.TOOL if index == 0 else GraphNodeType.CONCEPT,
|
|
224
|
+
canonical_key="ollama" if index == 0 else f"scale-{index}",
|
|
225
|
+
label="Ollama" if index == 0 else f"scale-{index}",
|
|
226
|
+
)
|
|
227
|
+
for index in range(nodes_count)
|
|
228
|
+
]
|
|
229
|
+
edges: list[GraphEdge] = []
|
|
230
|
+
for index in range(edges_count):
|
|
231
|
+
source = nodes[index % nodes_count].id
|
|
232
|
+
target = nodes[(index * 17 + 1) % nodes_count].id
|
|
233
|
+
if source == target:
|
|
234
|
+
target = nodes[(index + 1) % nodes_count].id
|
|
235
|
+
edge_id = stable_edge_id(source, target, GraphRelationType.RELATED, str(index // nodes_count))
|
|
236
|
+
edges.append(GraphEdge(
|
|
237
|
+
id=edge_id, source_node_id=source, target_node_id=target,
|
|
238
|
+
relation_type=GraphRelationType.RELATED, confidence=0.9,
|
|
239
|
+
scope_key=str(index // nodes_count),
|
|
240
|
+
supports=[GraphEdgeSupport(edge_id=edge_id, memory_id=support.id)],
|
|
241
|
+
))
|
|
242
|
+
started = time.perf_counter()
|
|
243
|
+
await graph_repo.replace_all(nodes, edges)
|
|
244
|
+
build_ms = (time.perf_counter() - started) * 1000
|
|
245
|
+
started = time.perf_counter()
|
|
246
|
+
await graph_repo.replace_all(nodes, edges)
|
|
247
|
+
rebuild_ms = (time.perf_counter() - started) * 1000
|
|
248
|
+
seed = nodes[0].id
|
|
249
|
+
started = time.perf_counter()
|
|
250
|
+
one_hop = await graph_repo.edges_for_nodes({seed})
|
|
251
|
+
one_hop_ms = (time.perf_counter() - started) * 1000
|
|
252
|
+
neighbors = {
|
|
253
|
+
edge.target_node_id if edge.source_node_id == seed else edge.source_node_id
|
|
254
|
+
for edge in one_hop
|
|
255
|
+
}
|
|
256
|
+
started = time.perf_counter()
|
|
257
|
+
two_hop = await graph_repo.edges_for_nodes(neighbors)
|
|
258
|
+
two_hop_ms = (time.perf_counter() - started) * 1000
|
|
259
|
+
embedding = DeterministicEmbedding(16)
|
|
260
|
+
lexical = BM25Index()
|
|
261
|
+
vector = InMemoryVectorStore(16)
|
|
262
|
+
base = HybridRetrievalEngine(
|
|
263
|
+
memory_repo=memory_repo, lexical_index=lexical, vector_store=vector,
|
|
264
|
+
embedding_service=embedding,
|
|
265
|
+
index_synchronizer=RetrievalIndexSynchronizer(
|
|
266
|
+
memory_repo=memory_repo, lexical_index=lexical, vector_store=vector,
|
|
267
|
+
embedding_service=embedding,
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
hybrid_graph = GraphAugmentedRetrievalEngine(
|
|
271
|
+
base_engine=base, graph_service=graph_service, memory_repo=memory_repo,
|
|
272
|
+
)
|
|
273
|
+
started = time.perf_counter()
|
|
274
|
+
hybrid_result = await hybrid_graph.retrieve(RetrievalQuery(
|
|
275
|
+
text="Ollama", mode=RetrievalMode.HYBRID_GRAPH, k=5,
|
|
276
|
+
graph_max_hops=2, graph_max_nodes=100, graph_max_edges=250,
|
|
277
|
+
))
|
|
278
|
+
hybrid_graph_ms = (time.perf_counter() - started) * 1000
|
|
279
|
+
await database.close()
|
|
280
|
+
reopened = Database(root / "graph-scale.db")
|
|
281
|
+
started = time.perf_counter()
|
|
282
|
+
await reopened.initialize()
|
|
283
|
+
persisted = await SqliteGraphRepository(reopened.connection()).counts()
|
|
284
|
+
restart_ms = (time.perf_counter() - started) * 1000
|
|
285
|
+
await reopened.close()
|
|
286
|
+
return {
|
|
287
|
+
"nodes": persisted[0], "edges": persisted[1],
|
|
288
|
+
"supports": persisted[2], "build_ms": build_ms,
|
|
289
|
+
"rebuild_ms": rebuild_ms,
|
|
290
|
+
"one_hop_ms": one_hop_ms, "one_hop_edges": len(one_hop),
|
|
291
|
+
"two_hop_ms": two_hop_ms, "two_hop_edges": len(two_hop),
|
|
292
|
+
"hybrid_graph_ms": hybrid_graph_ms,
|
|
293
|
+
"hybrid_graph_results": len(hybrid_result.memories),
|
|
294
|
+
"restart_ms": restart_ms,
|
|
295
|
+
}
|
|
296
|
+
finally:
|
|
297
|
+
await database.close()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
async def main() -> None:
|
|
301
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
302
|
+
root = Path(directory)
|
|
303
|
+
report = await run_evaluation(root / "evaluation")
|
|
304
|
+
(root / "scale").mkdir()
|
|
305
|
+
report["scale"] = await run_scale(root / "scale")
|
|
306
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
asyncio.run(main())
|