ltcai 9.5.0 → 9.7.0
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.
- package/README.md +67 -43
- package/auto_setup.py +11 -1
- package/docs/CHANGELOG.md +86 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
- package/docs/DEVELOPMENT.md +8 -8
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/OPERATIONS.md +1 -1
- package/docs/PERFORMANCE.md +106 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/docs/mcp-tools.md +1 -1
- package/kg_schema.py +11 -1
- package/knowledge_graph.py +12 -2
- package/knowledge_graph_api.py +11 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +583 -0
- package/lattice_brain/graph/retrieval.py +211 -7
- package/lattice_brain/graph/retrieval_vector.py +102 -0
- package/lattice_brain/ingestion.py +360 -4
- package/lattice_brain/quality.py +44 -9
- package/lattice_brain/runtime/multi_agent.py +38 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +27 -2
- package/latticeai/api/change_proposals.py +89 -0
- package/latticeai/api/chat_agent_http.py +2 -0
- package/latticeai/api/review_queue.py +66 -2
- package/latticeai/app_factory.py +23 -0
- package/latticeai/cli/entrypoint.py +3 -1
- package/latticeai/core/agent.py +273 -101
- package/latticeai/core/agent_eval.py +411 -0
- package/latticeai/core/agent_trace.py +104 -0
- package/latticeai/core/legacy_compatibility.py +15 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_governor.py +101 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/router_registration.py +2 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +97 -1
- package/latticeai/services/change_proposals.py +322 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/review_queue.py +47 -3
- package/latticeai/tools/__init__.py +6 -1
- package/llm_router.py +10 -1
- package/local_knowledge_api.py +10 -1
- package/ltcai_cli.py +11 -1
- package/mcp_registry.py +10 -1
- package/p_reinforce.py +10 -1
- package/package.json +1 -1
- package/scripts/agent_eval.py +46 -0
- package/scripts/brain_quality_eval.py +1 -1
- package/scripts/check_current_release_docs.mjs +2 -1
- package/scripts/profile_kg.py +360 -0
- package/setup_wizard.py +10 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/{Act-Cdfqx4wN.js → Act-B6c39ays.js} +2 -1
- package/static/app/assets/{Brain-w_tAuyg6.js → Brain-D7Qg4k6M.js} +1 -1
- package/static/app/assets/{Capture-iJwi9LmS.js → Capture-VF_di68r.js} +1 -1
- package/static/app/assets/{Library-Do-jjhzi.js → Library-D_Gis2PA.js} +1 -1
- package/static/app/assets/{System-DFg_q3E6.js → System-C5s5H2ov.js} +1 -1
- package/static/app/assets/{index-BN6HIWVC.css → index-85wQvEie.css} +1 -1
- package/static/app/assets/index-DJC_2oub.js +18 -0
- package/static/app/assets/{primitives-ZTUlU7pR.js → primitives-DL4Nip8C.js} +1 -1
- package/static/app/assets/{textarea-CMfhqPhE.js → textarea-woZfCXHy.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/static/app/assets/index-aJuRi-Xo.js +0 -17
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agent-loop evaluation gate (v9.6.0).
|
|
3
|
+
|
|
4
|
+
Runs the deterministic scenario suite in ``latticeai.core.agent_eval``
|
|
5
|
+
against the real SingleAgentRuntime state machine (scripted model, fake
|
|
6
|
+
ports) and fails the release when any scenario regresses.
|
|
7
|
+
|
|
8
|
+
Usage: .venv/bin/python scripts/agent_eval.py [--verbose]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
18
|
+
|
|
19
|
+
from latticeai.core.agent_eval import run_agent_eval # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
verbose = "--verbose" in sys.argv
|
|
24
|
+
report = run_agent_eval()
|
|
25
|
+
headline = {
|
|
26
|
+
"scenarios": report["scenarios"],
|
|
27
|
+
"passed": report["passed"],
|
|
28
|
+
"success_rate": report["success_rate"],
|
|
29
|
+
"parse_errors": report["parse_errors"],
|
|
30
|
+
"parse_recovered": report["parse_recovered"],
|
|
31
|
+
"recovery_rate": report["recovery_rate"],
|
|
32
|
+
}
|
|
33
|
+
if verbose:
|
|
34
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
35
|
+
failures = [r for r in report["results"] if not r["ok"]]
|
|
36
|
+
if failures:
|
|
37
|
+
print(f"agent-loop-eval: FAIL {headline}")
|
|
38
|
+
for result in failures:
|
|
39
|
+
print(f" ✗ {result['name']}: {'; '.join(result['failures'])}")
|
|
40
|
+
return 1
|
|
41
|
+
print(f"agent-loop-eval: OK {headline}")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
|
@@ -16,7 +16,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
16
16
|
if str(REPO_ROOT) not in sys.path:
|
|
17
17
|
sys.path.insert(0, str(REPO_ROOT))
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from lattice_brain.graph.store import KnowledgeGraphStore # noqa: E402
|
|
20
20
|
from lattice_brain.quality import RetrievalBenchmarkRunner # noqa: E402
|
|
21
21
|
from latticeai.services.search_service import SearchService # noqa: E402
|
|
22
22
|
from latticeai.services.memory_service import MemoryService # noqa: E402
|
|
@@ -6,11 +6,12 @@ const root = process.cwd();
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
7
7
|
const version = pkg.version;
|
|
8
8
|
const releaseDir = `output/release/v${version}`;
|
|
9
|
-
const releaseTheme = "
|
|
9
|
+
const releaseTheme = "Proactive Hybrid Brain";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
13
13
|
const currentReleaseFiles = [
|
|
14
|
+
"AGENTS.md",
|
|
14
15
|
"README.md",
|
|
15
16
|
"ARCHITECTURE.md",
|
|
16
17
|
"FEATURE_STATUS.md",
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Synthetic Knowledge Graph performance / memory profiler.
|
|
3
|
+
|
|
4
|
+
Builds a synthetic knowledge graph against a throwaway SQLite path using the
|
|
5
|
+
real ``KnowledgeGraphStore`` API (the same construction pattern the unit tests
|
|
6
|
+
use: ``KnowledgeGraphStore(db_path, blob_dir)``), then measures wall-time
|
|
7
|
+
percentiles (p50/p95) and tracemalloc peak memory for the hot read/write paths:
|
|
8
|
+
|
|
9
|
+
* ``ingest_source()`` — ingestion throughput (items/sec)
|
|
10
|
+
* ``search()`` — FTS5/LIKE keyword search
|
|
11
|
+
* ``context_for_query()`` — RAG context assembly
|
|
12
|
+
* ``neighbors()`` / ``traverse()`` — 1-hop and depth-2 graph walks
|
|
13
|
+
* ``stats()`` — aggregate counters
|
|
14
|
+
* ``rebuild_vector_index()`` / ``vector_search()`` — offline hash-embedder
|
|
15
|
+
vector path (skipped gracefully if the vector path is unavailable)
|
|
16
|
+
|
|
17
|
+
Everything runs offline: no model downloads, no network, no LLM router — the
|
|
18
|
+
concept/triple extractors fall back to their rule-based paths and the default
|
|
19
|
+
embedder is the deterministic local hash model.
|
|
20
|
+
|
|
21
|
+
Honesty note: this is a *synthetic* baseline. Corpus text is generated from a
|
|
22
|
+
fixed vocabulary, so extraction density and FTS selectivity differ from real
|
|
23
|
+
user data. Timings include tracemalloc instrumentation overhead (allocation
|
|
24
|
+
tracking roughly doubles Python allocation cost), so treat absolute numbers as
|
|
25
|
+
upper bounds and use them for release-over-release comparison, not marketing.
|
|
26
|
+
|
|
27
|
+
Usage::
|
|
28
|
+
|
|
29
|
+
python scripts/profile_kg.py # 5000 sources, 50 queries (~2-2.5 min)
|
|
30
|
+
python scripts/profile_kg.py --nodes 500 # quick run (~10 s)
|
|
31
|
+
python scripts/profile_kg.py --json # machine-readable output
|
|
32
|
+
python scripts/profile_kg.py --db-path /tmp/kg # keep the DB around
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
import random
|
|
40
|
+
import statistics
|
|
41
|
+
import sys
|
|
42
|
+
import tempfile
|
|
43
|
+
import time
|
|
44
|
+
import tracemalloc
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
47
|
+
|
|
48
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
49
|
+
if str(ROOT) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(ROOT))
|
|
51
|
+
|
|
52
|
+
from lattice_brain.graph.store import KnowledgeGraphStore # noqa: E402
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Synthetic corpus ─────────────────────────────────────────────────────────
|
|
56
|
+
# Recurring entities so concept nodes are shared across documents and edges
|
|
57
|
+
# actually form; a Korean slice exercises the FTS5 trigram path.
|
|
58
|
+
ENTITIES = [
|
|
59
|
+
"Lattice", "GraphStore", "Kubernetes", "Postgres", "FastAPI", "SQLite",
|
|
60
|
+
"Telegram", "MLX", "Embedding", "Pipeline", "Workspace", "Provenance",
|
|
61
|
+
"Scheduler", "Retrieval", "Ingestion", "Router", "Registry", "Backup",
|
|
62
|
+
]
|
|
63
|
+
FILLER = (
|
|
64
|
+
"the service processes requests and stores results in the database "
|
|
65
|
+
"while the worker retries failed jobs and reports metrics to the "
|
|
66
|
+
"dashboard for the on-call engineer to review during the incident "
|
|
67
|
+
).split()
|
|
68
|
+
KOREAN = ["프로젝트", "일정", "회의", "결정", "지식그래프", "검색", "성능", "메모리"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _make_text(rng: random.Random, size: str) -> str:
|
|
72
|
+
lengths = {"small": 40, "medium": 160, "large": 520} # words
|
|
73
|
+
words: List[str] = []
|
|
74
|
+
target = lengths[size]
|
|
75
|
+
while len(words) < target:
|
|
76
|
+
words.extend(rng.sample(FILLER, k=min(8, target - len(words))))
|
|
77
|
+
if rng.random() < 0.6:
|
|
78
|
+
words.append(rng.choice(ENTITIES))
|
|
79
|
+
if rng.random() < 0.25:
|
|
80
|
+
words.append(rng.choice(KOREAN))
|
|
81
|
+
if rng.random() < 0.1:
|
|
82
|
+
words.append(f"{rng.choice(ENTITIES)} improves {rng.choice(ENTITIES)}.")
|
|
83
|
+
return " ".join(words)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _pick_size(rng: random.Random) -> str:
|
|
87
|
+
r = rng.random()
|
|
88
|
+
if r < 0.6:
|
|
89
|
+
return "small"
|
|
90
|
+
if r < 0.9:
|
|
91
|
+
return "medium"
|
|
92
|
+
return "large"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── Measurement helpers ──────────────────────────────────────────────────────
|
|
96
|
+
def _percentiles(samples_s: List[float]) -> Dict[str, float]:
|
|
97
|
+
if not samples_s:
|
|
98
|
+
return {"p50_ms": 0.0, "p95_ms": 0.0, "mean_ms": 0.0}
|
|
99
|
+
ordered = sorted(samples_s)
|
|
100
|
+
|
|
101
|
+
def q(p: float) -> float:
|
|
102
|
+
idx = min(len(ordered) - 1, max(0, round(p * (len(ordered) - 1))))
|
|
103
|
+
return ordered[idx]
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"p50_ms": round(q(0.50) * 1000, 3),
|
|
107
|
+
"p95_ms": round(q(0.95) * 1000, 3),
|
|
108
|
+
"mean_ms": round(statistics.fmean(ordered) * 1000, 3),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _timed_phase(fn: Callable[[], List[float]]) -> Dict[str, Any]:
|
|
113
|
+
"""Run one phase; return sample percentiles + phase peak memory."""
|
|
114
|
+
tracemalloc.reset_peak()
|
|
115
|
+
wall_start = time.perf_counter()
|
|
116
|
+
samples = fn()
|
|
117
|
+
wall = time.perf_counter() - wall_start
|
|
118
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
119
|
+
result = _percentiles(samples)
|
|
120
|
+
result.update(
|
|
121
|
+
{
|
|
122
|
+
"calls": len(samples),
|
|
123
|
+
"wall_s": round(wall, 3),
|
|
124
|
+
"peak_mem_mb": round(peak / (1024 * 1024), 2),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── Profiler ─────────────────────────────────────────────────────────────────
|
|
131
|
+
def run_profile(
|
|
132
|
+
nodes: int,
|
|
133
|
+
queries: int,
|
|
134
|
+
db_path: Optional[Path],
|
|
135
|
+
seed: int = 42,
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
rng = random.Random(seed)
|
|
138
|
+
tmp_ctx = None
|
|
139
|
+
if db_path is None:
|
|
140
|
+
tmp_ctx = tempfile.TemporaryDirectory(prefix="ltcai-profile-kg-")
|
|
141
|
+
base = Path(tmp_ctx.name)
|
|
142
|
+
else:
|
|
143
|
+
base = Path(db_path)
|
|
144
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
report: Dict[str, Any] = {
|
|
147
|
+
"schema_version": "kg-profile/v1",
|
|
148
|
+
"params": {"nodes": nodes, "queries": queries, "seed": seed,
|
|
149
|
+
"db_path": str(base), "ephemeral": tmp_ctx is not None},
|
|
150
|
+
"phases": {},
|
|
151
|
+
"notes": [],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
tracemalloc.start()
|
|
155
|
+
try:
|
|
156
|
+
store = KnowledgeGraphStore(base / "graph.sqlite", base / "blobs")
|
|
157
|
+
report["fts_enabled"] = bool(getattr(store, "_fts_enabled", False))
|
|
158
|
+
|
|
159
|
+
# Phase 1: ingestion ------------------------------------------------
|
|
160
|
+
doc_ids: List[str] = []
|
|
161
|
+
char_total = 0
|
|
162
|
+
|
|
163
|
+
def _ingest() -> List[float]:
|
|
164
|
+
nonlocal char_total
|
|
165
|
+
samples: List[float] = []
|
|
166
|
+
for i in range(nodes):
|
|
167
|
+
size = _pick_size(rng)
|
|
168
|
+
text = _make_text(rng, size)
|
|
169
|
+
char_total += len(text)
|
|
170
|
+
t0 = time.perf_counter()
|
|
171
|
+
result = store.ingest_source(
|
|
172
|
+
source_type="note",
|
|
173
|
+
title=f"Synthetic note {i} {rng.choice(ENTITIES)}",
|
|
174
|
+
text=text,
|
|
175
|
+
source_uri=f"synthetic://note/{i}",
|
|
176
|
+
owner="profiler@example.com",
|
|
177
|
+
)
|
|
178
|
+
samples.append(time.perf_counter() - t0)
|
|
179
|
+
doc_ids.append(result["node_id"])
|
|
180
|
+
return samples
|
|
181
|
+
|
|
182
|
+
ingest = _timed_phase(_ingest)
|
|
183
|
+
ingest["items_per_sec"] = round(nodes / ingest["wall_s"], 1) if ingest["wall_s"] else 0.0
|
|
184
|
+
ingest["corpus_chars"] = char_total
|
|
185
|
+
report["phases"]["ingest_source"] = ingest
|
|
186
|
+
|
|
187
|
+
# Query terms: mix entity names, Korean tokens, and rare misses.
|
|
188
|
+
terms = [rng.choice(ENTITIES + KOREAN) for _ in range(max(1, queries))]
|
|
189
|
+
for i in range(0, len(terms), 10):
|
|
190
|
+
terms[i] = f"missing-term-{i}" # cache-unfriendly misses
|
|
191
|
+
|
|
192
|
+
# Phase 2: search ----------------------------------------------------
|
|
193
|
+
def _search() -> List[float]:
|
|
194
|
+
samples = []
|
|
195
|
+
for term in terms:
|
|
196
|
+
t0 = time.perf_counter()
|
|
197
|
+
store.search(term, limit=30)
|
|
198
|
+
samples.append(time.perf_counter() - t0)
|
|
199
|
+
return samples
|
|
200
|
+
|
|
201
|
+
report["phases"]["search"] = _timed_phase(_search)
|
|
202
|
+
|
|
203
|
+
# Phase 3: context_for_query ----------------------------------------
|
|
204
|
+
def _context() -> List[float]:
|
|
205
|
+
samples = []
|
|
206
|
+
for term in terms:
|
|
207
|
+
t0 = time.perf_counter()
|
|
208
|
+
store.context_for_query(term, limit=6)
|
|
209
|
+
samples.append(time.perf_counter() - t0)
|
|
210
|
+
return samples
|
|
211
|
+
|
|
212
|
+
report["phases"]["context_for_query"] = _timed_phase(_context)
|
|
213
|
+
|
|
214
|
+
# Phase 4: neighbors / traverse -------------------------------------
|
|
215
|
+
sample_ids = rng.sample(doc_ids, k=min(len(doc_ids), max(1, queries)))
|
|
216
|
+
|
|
217
|
+
def _neighbors() -> List[float]:
|
|
218
|
+
samples = []
|
|
219
|
+
for node_id in sample_ids:
|
|
220
|
+
t0 = time.perf_counter()
|
|
221
|
+
store.neighbors(node_id)
|
|
222
|
+
samples.append(time.perf_counter() - t0)
|
|
223
|
+
return samples
|
|
224
|
+
|
|
225
|
+
report["phases"]["neighbors"] = _timed_phase(_neighbors)
|
|
226
|
+
|
|
227
|
+
def _traverse() -> List[float]:
|
|
228
|
+
samples = []
|
|
229
|
+
for node_id in sample_ids:
|
|
230
|
+
t0 = time.perf_counter()
|
|
231
|
+
store.traverse(node_id, depth=2, limit=100)
|
|
232
|
+
samples.append(time.perf_counter() - t0)
|
|
233
|
+
return samples
|
|
234
|
+
|
|
235
|
+
report["phases"]["traverse_depth2"] = _timed_phase(_traverse)
|
|
236
|
+
|
|
237
|
+
# Phase 5: stats -----------------------------------------------------
|
|
238
|
+
def _stats() -> List[float]:
|
|
239
|
+
samples = []
|
|
240
|
+
for _ in range(5):
|
|
241
|
+
t0 = time.perf_counter()
|
|
242
|
+
store.stats()
|
|
243
|
+
samples.append(time.perf_counter() - t0)
|
|
244
|
+
return samples
|
|
245
|
+
|
|
246
|
+
report["phases"]["stats"] = _timed_phase(_stats)
|
|
247
|
+
graph_stats = store.stats()
|
|
248
|
+
node_counts = graph_stats.get("nodes") or {}
|
|
249
|
+
edge_counts = graph_stats.get("edges") or {}
|
|
250
|
+
report["graph"] = {
|
|
251
|
+
"nodes_total": sum(node_counts.values()) if isinstance(node_counts, dict) else node_counts,
|
|
252
|
+
"edges_total": sum(edge_counts.values()) if isinstance(edge_counts, dict) else edge_counts,
|
|
253
|
+
"by_node_type": node_counts if isinstance(node_counts, dict) else None,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Phase 6: vector index (offline hash embedder) ----------------------
|
|
257
|
+
try:
|
|
258
|
+
def _vector_build() -> List[float]:
|
|
259
|
+
t0 = time.perf_counter()
|
|
260
|
+
store.rebuild_vector_index(full=True, include_nodes=True, include_chunks=True)
|
|
261
|
+
return [time.perf_counter() - t0]
|
|
262
|
+
|
|
263
|
+
report["phases"]["rebuild_vector_index"] = _timed_phase(_vector_build)
|
|
264
|
+
|
|
265
|
+
# vector_search is a brute-force scan over all stored embeddings
|
|
266
|
+
# (O(index size) per query); cap the query count so the default
|
|
267
|
+
# profile stays inside its time budget while still sampling p50/p95.
|
|
268
|
+
vector_terms = terms[: min(len(terms), 10)]
|
|
269
|
+
|
|
270
|
+
def _vector_search() -> List[float]:
|
|
271
|
+
samples = []
|
|
272
|
+
for term in vector_terms:
|
|
273
|
+
t0 = time.perf_counter()
|
|
274
|
+
store.vector_search(term, limit=30)
|
|
275
|
+
samples.append(time.perf_counter() - t0)
|
|
276
|
+
return samples
|
|
277
|
+
|
|
278
|
+
report["phases"]["vector_search"] = _timed_phase(_vector_search)
|
|
279
|
+
report["notes"].append(
|
|
280
|
+
"Vector phase uses the built-in deterministic hash embedder "
|
|
281
|
+
"(offline); real embedding providers will be slower to index "
|
|
282
|
+
"but produce semantically meaningful rankings."
|
|
283
|
+
)
|
|
284
|
+
except Exception as exc: # pragma: no cover - environment dependent
|
|
285
|
+
report["phases"]["vector"] = {"skipped": True, "reason": str(exc)[:200]}
|
|
286
|
+
report["notes"].append(
|
|
287
|
+
"Vector phase skipped: the vector path was unavailable in this "
|
|
288
|
+
"environment (no embedding provider required for the rest of "
|
|
289
|
+
"the profile)."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
current, overall_peak = tracemalloc.get_traced_memory()
|
|
293
|
+
report["memory"] = {
|
|
294
|
+
"tracemalloc_current_mb": round(current / (1024 * 1024), 2),
|
|
295
|
+
"phase_peaks_are_reset_per_phase": True,
|
|
296
|
+
}
|
|
297
|
+
report["db_size_mb"] = round((base / "graph.sqlite").stat().st_size / (1024 * 1024), 2)
|
|
298
|
+
report["notes"].append(
|
|
299
|
+
"Synthetic baseline: fixed-vocabulary corpus, rule-based extraction, "
|
|
300
|
+
"tracemalloc enabled for the whole run (adds allocation-tracking "
|
|
301
|
+
"overhead to every timing)."
|
|
302
|
+
)
|
|
303
|
+
finally:
|
|
304
|
+
tracemalloc.stop()
|
|
305
|
+
if tmp_ctx is not None:
|
|
306
|
+
tmp_ctx.cleanup()
|
|
307
|
+
|
|
308
|
+
return report
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _print_report(report: Dict[str, Any]) -> None:
|
|
312
|
+
p = report["params"]
|
|
313
|
+
print("Knowledge Graph synthetic profile")
|
|
314
|
+
print(f" sources={p['nodes']} queries={p['queries']} seed={p['seed']}")
|
|
315
|
+
print(f" db={p['db_path']} ephemeral={p['ephemeral']} fts_enabled={report.get('fts_enabled')}")
|
|
316
|
+
graph = report.get("graph") or {}
|
|
317
|
+
print(f" graph: nodes={graph.get('nodes_total')} edges={graph.get('edges_total')} db_size={report.get('db_size_mb')} MB")
|
|
318
|
+
print()
|
|
319
|
+
header = f" {'phase':<22}{'calls':>6}{'p50 ms':>10}{'p95 ms':>10}{'mean ms':>10}{'wall s':>9}{'peak MB':>9}"
|
|
320
|
+
print(header)
|
|
321
|
+
print(" " + "-" * (len(header) - 2))
|
|
322
|
+
for name, phase in report["phases"].items():
|
|
323
|
+
if phase.get("skipped"):
|
|
324
|
+
print(f" {name:<22} skipped: {phase.get('reason', '')}")
|
|
325
|
+
continue
|
|
326
|
+
print(
|
|
327
|
+
f" {name:<22}{phase['calls']:>6}{phase['p50_ms']:>10}{phase['p95_ms']:>10}"
|
|
328
|
+
f"{phase['mean_ms']:>10}{phase['wall_s']:>9}{phase['peak_mem_mb']:>9}"
|
|
329
|
+
)
|
|
330
|
+
if "items_per_sec" in phase:
|
|
331
|
+
print(f" {'':<22} throughput: {phase['items_per_sec']} items/sec corpus: {phase['corpus_chars']} chars")
|
|
332
|
+
print()
|
|
333
|
+
for note in report.get("notes", []):
|
|
334
|
+
print(f" note: {note}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
338
|
+
parser = argparse.ArgumentParser(description="Profile KnowledgeGraphStore on a synthetic corpus (offline).")
|
|
339
|
+
parser.add_argument("--nodes", type=int, default=5000, help="synthetic sources to ingest (default: 5000)")
|
|
340
|
+
parser.add_argument("--queries", type=int, default=50, help="queries per read phase (default: 50)")
|
|
341
|
+
parser.add_argument("--db-path", type=Path, default=None, help="directory for the profile DB (default: temp dir, auto-removed)")
|
|
342
|
+
parser.add_argument("--json", action="store_true", help="emit the full report as JSON")
|
|
343
|
+
parser.add_argument("--seed", type=int, default=42, help="corpus RNG seed (default: 42)")
|
|
344
|
+
args = parser.parse_args(argv)
|
|
345
|
+
|
|
346
|
+
if args.nodes < 1 or args.nodes > 200_000:
|
|
347
|
+
parser.error("--nodes must be between 1 and 200000")
|
|
348
|
+
if args.queries < 1 or args.queries > 10_000:
|
|
349
|
+
parser.error("--queries must be between 1 and 10000")
|
|
350
|
+
|
|
351
|
+
report = run_profile(args.nodes, args.queries, args.db_path, seed=args.seed)
|
|
352
|
+
if args.json:
|
|
353
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
354
|
+
else:
|
|
355
|
+
_print_report(report)
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
raise SystemExit(main())
|
package/setup_wizard.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"""Compatibility shim for :mod:`latticeai.setup.wizard`."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
import warnings
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
warnings.warn(
|
|
7
|
+
"Importing 'setup_wizard' from the repository root is deprecated; "
|
|
8
|
+
"use 'from latticeai.setup import wizard' instead. "
|
|
9
|
+
"The root shim will be removed in a future major release.",
|
|
10
|
+
DeprecationWarning,
|
|
11
|
+
stacklevel=2,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from latticeai.setup import wizard as _impl # noqa: E402
|
|
6
15
|
|
|
7
16
|
sys.modules[__name__] = _impl
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "9.
|
|
2
|
+
"version": "9.7.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
|
-
"app": "/static/app/assets/index-
|
|
5
|
+
"app": "/static/app/assets/index-DJC_2oub.js"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"_primitives-
|
|
10
|
-
"_textarea-
|
|
11
|
-
"index.html": "/static/app/assets/index-
|
|
12
|
-
"assets/index-
|
|
13
|
-
"src/pages/Act.tsx": "/static/app/assets/Act-
|
|
14
|
-
"src/pages/Brain.tsx": "/static/app/assets/Brain-
|
|
15
|
-
"src/pages/Capture.tsx": "/static/app/assets/Capture-
|
|
16
|
-
"src/pages/Library.tsx": "/static/app/assets/Library-
|
|
17
|
-
"src/pages/System.tsx": "/static/app/assets/System-
|
|
9
|
+
"_primitives-DL4Nip8C.js": "/static/app/assets/primitives-DL4Nip8C.js",
|
|
10
|
+
"_textarea-woZfCXHy.js": "/static/app/assets/textarea-woZfCXHy.js",
|
|
11
|
+
"index.html": "/static/app/assets/index-DJC_2oub.js",
|
|
12
|
+
"assets/index-85wQvEie.css": "/static/app/assets/index-85wQvEie.css",
|
|
13
|
+
"src/pages/Act.tsx": "/static/app/assets/Act-B6c39ays.js",
|
|
14
|
+
"src/pages/Brain.tsx": "/static/app/assets/Brain-D7Qg4k6M.js",
|
|
15
|
+
"src/pages/Capture.tsx": "/static/app/assets/Capture-VF_di68r.js",
|
|
16
|
+
"src/pages/Library.tsx": "/static/app/assets/Library-D_Gis2PA.js",
|
|
17
|
+
"src/pages/System.tsx": "/static/app/assets/System-C5s5H2ov.js"
|
|
18
18
|
}
|
|
19
19
|
}
|