ltcai 10.6.3 → 10.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 +43 -59
- package/docs/CHANGELOG.md +37 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/LAYOUT_REBUILD_SPEC.md +810 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +29 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +72 -0
- package/latticeai/api/automation_intelligence.py +112 -0
- package/latticeai/api/knowledge_graph.py +133 -0
- package/latticeai/api/static_routes.py +35 -1
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +3 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_runs.py +111 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/package.json +5 -3
- package/scripts/capture_release_evidence.mjs +335 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_release_evidence_bound.mjs +116 -0
- package/scripts/check_screenshot_pixel_delta.py +435 -0
- package/scripts/measure_brain_home_fill.mjs +141 -0
- 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 +37 -39
- package/static/app/assets/{Act-05uZSgyw.js → Act-DIbkoJqs.js} +1 -1
- package/static/app/assets/AdminConsole-Cbi6Jre6.js +1 -0
- package/static/app/assets/{Brain-jjMYTdnU.js → Brain-BPfuXZ71.js} +2 -2
- package/static/app/assets/BrainHome-87XaAS4V.js +2 -0
- package/static/app/assets/{BrainSignals-BYxDqBH3.js → BrainSignals-ExpcdiUi.js} +1 -1
- package/static/app/assets/Capture-Bxe4WiVX.js +1 -0
- package/static/app/assets/{CommandPalette-Cj7z-qUE.js → CommandPalette-B1PV5Yd4.js} +1 -1
- package/static/app/assets/Library-BRlJJmwZ.js +1 -0
- package/static/app/assets/{LivingBrain-CTokkZn1.js → LivingBrain-D61qG3WY.js} +1 -1
- package/static/app/assets/ProductFlow-BxjxI8Aa.js +1 -0
- package/static/app/assets/{ReviewCard-uAII_l9v.js → ReviewCard-BoiWv0Y1.js} +2 -2
- package/static/app/assets/System-Ci0HEtTJ.js +1 -0
- package/static/app/assets/arrow-left-DArdBUxd.js +1 -0
- package/static/app/assets/{bot-CkrHFdUl.js → bot-BaPt4PPn.js} +1 -1
- package/static/app/assets/brain-BSkWaj3i.js +1 -0
- package/static/app/assets/{button-Ic6omIbs.js → button-CKwtO1O0.js} +1 -1
- package/static/app/assets/{circle-pause-DBaaUR08.js → circle-pause-CiUc5RRk.js} +1 -1
- package/static/app/assets/{circle-play-Dz0OLYN9.js → circle-play-BaTDxzR9.js} +1 -1
- package/static/app/assets/{cpu-N8uE9ono.js → cpu-Dm1gHuP1.js} +1 -1
- package/static/app/assets/{download-CCjhOjms.js → download-C32oGU-C.js} +1 -1
- package/static/app/assets/{folder-open-Bld9TtHp.js → folder-open-Ch2jLVdD.js} +1 -1
- package/static/app/assets/{hard-drive-BMIqDSyY.js → hard-drive-BBjA5st8.js} +1 -1
- package/static/app/assets/index-BV7Yf6Eq.css +2 -0
- package/static/app/assets/{index-yNtiRyw3.js → index-J6h01X78.js} +4 -4
- package/static/app/assets/{input-DW1tXtc0.js → input-y6ABdKWY.js} +1 -1
- package/static/app/assets/{permissionCopy-Djpwn54H.js → permissionCopy-BEtc0Ihd.js} +1 -1
- package/static/app/assets/{primitives-B6WtkLgB.js → primitives-GSFXF2Pd.js} +1 -1
- package/static/app/assets/search-CaIZKyo_.js +1 -0
- package/static/app/assets/{share-2-DR5L1qRc.js → share-2-BgGq2A63.js} +1 -1
- package/static/app/assets/{shield-alert-CVE5eXsl.js → shield-alert-BYA491cj.js} +1 -1
- package/static/app/assets/{textarea-DdaVFgvk.js → textarea-BAcim3fs.js} +1 -1
- package/static/app/assets/{useFocusTrap-B5TyGaiB.js → useFocusTrap-qLg24jiv.js} +1 -1
- package/static/app/assets/{useQuery-CiRdTQlD.js → useQuery-xkAu9eDX.js} +1 -1
- package/static/app/assets/utils-CogdofAA.js +4 -0
- package/static/app/assets/workspace-BMvfx9_R.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/AdminConsole-C8yz1Cs_.js +0 -1
- package/static/app/assets/BrainHome-D3t45_D4.js +0 -2
- package/static/app/assets/Capture-DKgu7fB0.js +0 -1
- package/static/app/assets/Library-CO76FWeO.js +0 -1
- package/static/app/assets/ProductFlow-Bmted2ys.js +0 -1
- package/static/app/assets/System-PUMzDmof.js +0 -1
- package/static/app/assets/activity-C_7182GG.js +0 -1
- package/static/app/assets/arrow-left-BQCpWR37.js +0 -1
- package/static/app/assets/brain-CdPX7svt.js +0 -1
- package/static/app/assets/index-DYUs0cWy.css +0 -2
- package/static/app/assets/search-CkHYuKpY.js +0 -1
- package/static/app/assets/users-pZqCRxdD.js +0 -1
- package/static/app/assets/utils-C3rXoa-U.js +0 -4
- package/static/app/assets/workspace-DfxKEttN.js +0 -1
package/docs/ONBOARDING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Onboarding
|
|
2
2
|
|
|
3
|
-
Current release: **10.
|
|
3
|
+
Current release: **10.7.0 — Plain Surface**.
|
|
4
4
|
|
|
5
5
|
The first-run goal is a five-minute path from "I opened the app" to "my Brain
|
|
6
6
|
has a source, a question, and proof." This page is the product contract behind
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Lattice AI — Operations Guide (v10.
|
|
1
|
+
# Lattice AI — Operations Guide (v10.7.0)
|
|
2
2
|
|
|
3
3
|
> **Status: canonical** — kept in sync with the current release. Storage layout
|
|
4
4
|
> below reflects the SQLite live Brain store and workspace scoping, not the
|
|
@@ -162,3 +162,31 @@ Ctrl+C 또는 kill <pid>
|
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
서버는 종료 시 진행 중인 HTTP 요청을 완료한 후 종료됩니다 (graceful shutdown — uvicorn 기본 동작).
|
|
165
|
+
|
|
166
|
+
## 10. 릴리스 증거 캡처 순서 (레이아웃·UI 변경 후)
|
|
167
|
+
|
|
168
|
+
`scripts/capture_release_evidence.mjs` 의 `assertNotOverwritingTaggedRelease()` 는
|
|
169
|
+
`package.json` 버전과 동일한 git 태그(`vX.Y.Z`)가 이미 있으면 **즉시 exit 1** 한다.
|
|
170
|
+
레이아웃/UI를 바꾼 뒤 스크린샷을 다시 찍을 때는 반드시 아래 순서를 지킨다.
|
|
171
|
+
|
|
172
|
+
1. **버전 범프 먼저** — `node scripts/run_python.mjs scripts/bump_version.py`
|
|
173
|
+
(또는 동등한 package/pyproject/extension 버전 일괄 상향). 캡처 **전에** 한다.
|
|
174
|
+
2. `npm run build:assets` — 캡처는 빌드된 `static/app` 을 본다.
|
|
175
|
+
3. `npm run release:evidence` — `output/release/v<새버전>/` 에만 기록한다.
|
|
176
|
+
캡처 → 오래된 버전 정리 → **픽셀 델타 검사**까지 한 번에 돈다.
|
|
177
|
+
4. 결속 검사 — `SCREENSHOT_INDEX.md` 의 `asset-manifest.sha256` 과
|
|
178
|
+
`mock-server.sha256` 이 각각 `static/app/asset-manifest.json`,
|
|
179
|
+
`tests/visual/mock_server.cjs` 와 일치해야 `npm run lint` 가 통과한다.
|
|
180
|
+
|
|
181
|
+
레이아웃을 바꿨다고 보고했는데 배치는 그대로인 경우를 잡기 위해 v10.7.0 부터
|
|
182
|
+
`scripts/check_screenshot_pixel_delta.py` 가 12개 캡처를 직전 버전과 대조한다
|
|
183
|
+
(`npm run check:screenshot-delta` 로 단독 실행). 정적 화면은 1.5%, 애니메이션이
|
|
184
|
+
있는 화면은 8% 이상 픽셀이 달라져야 통과한다. 문구만 고친 화면은 여기서 실패로
|
|
185
|
+
잡히므로, 실패했다면 캡처가 아니라 레이아웃을 다시 본다. 직전 버전 디렉터리가
|
|
186
|
+
정리되어 없으면 기준선을 꺼낼 태그를 체크아웃한 뒤 다시 돌린다.
|
|
187
|
+
|
|
188
|
+
이미 태그가 찍힌 버전의 `output/release/vX.Y.Z/` 를 WIP UI로 덮으면 그 릴리스
|
|
189
|
+
증거가 영구히 거짓이 된다. 미리보기만 필요하면
|
|
190
|
+
`LTCAI_RELEASE_EVIDENCE_DIR=/tmp/lattice-preview npm run release:evidence` 로
|
|
191
|
+
게시 경로 밖을 쓴다. 태그된 버전의 원본 증거가 오염됐다면 태그 시점 트리에서
|
|
192
|
+
해당 디렉터리를 복원한 뒤, 새 버전 디렉터리에 다시 캡처한다.
|
package/docs/TRUST_MODEL.md
CHANGED
package/docs/WHY_LATTICE.md
CHANGED
package/docs/kg-schema.md
CHANGED
|
@@ -47,7 +47,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
47
47
|
from ..utils import now_iso as _now
|
|
48
48
|
from .contracts import multi_agent_contract
|
|
49
49
|
|
|
50
|
-
MULTI_AGENT_VERSION = "10.
|
|
50
|
+
MULTI_AGENT_VERSION = "10.7.0"
|
|
51
51
|
|
|
52
52
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
53
53
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/admin.py
CHANGED
|
@@ -159,6 +159,78 @@ def create_admin_router(
|
|
|
159
159
|
"last_message_at": history[-1].get("timestamp") if history else None,
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
@router.get("/admin/health-summary")
|
|
163
|
+
async def admin_health_summary(request: Request):
|
|
164
|
+
"""One-line admin health for the calm console header (layout rebuild).
|
|
165
|
+
|
|
166
|
+
Aggregates existing admin surfaces — no new persistence. ``status`` is
|
|
167
|
+
``attention`` when any issue is present, else ``ok``.
|
|
168
|
+
"""
|
|
169
|
+
_, users = require_admin(request)
|
|
170
|
+
issues: List[Dict[str, Any]] = []
|
|
171
|
+
|
|
172
|
+
disabled = sum(1 for user in users.values() if user.get("disabled"))
|
|
173
|
+
if disabled:
|
|
174
|
+
issues.append({
|
|
175
|
+
"area": "users",
|
|
176
|
+
"severity": "warning",
|
|
177
|
+
"message": f"{disabled} disabled user(s)",
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
report = build_sensitivity_report(_scoped_history(request)) or {}
|
|
182
|
+
severity = ((report.get("summary") or {}).get("severity_counts") or {})
|
|
183
|
+
high = int(severity.get("high") or 0)
|
|
184
|
+
if high:
|
|
185
|
+
issues.append({
|
|
186
|
+
"area": "security",
|
|
187
|
+
"severity": "high",
|
|
188
|
+
"message": f"{high} high-risk event(s)",
|
|
189
|
+
})
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
logging.warning("admin health-summary sensitivity failed: %s", exc)
|
|
192
|
+
|
|
193
|
+
if enable_graph:
|
|
194
|
+
try:
|
|
195
|
+
stats = get_graph_stats() or {}
|
|
196
|
+
if isinstance(stats, dict) and stats.get("error"):
|
|
197
|
+
issues.append({
|
|
198
|
+
"area": "brain_ops",
|
|
199
|
+
"severity": "warning",
|
|
200
|
+
"message": "Knowledge graph unavailable",
|
|
201
|
+
})
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
issues.append({
|
|
204
|
+
"area": "brain_ops",
|
|
205
|
+
"severity": "warning",
|
|
206
|
+
"message": str(exc)[:160],
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if product_hardening_status is not None:
|
|
210
|
+
try:
|
|
211
|
+
res = product_hardening_status()
|
|
212
|
+
hardening = res if isinstance(res, dict) else {}
|
|
213
|
+
startup = hardening.get("startup") if isinstance(hardening, dict) else {} # type: ignore[union-attr]
|
|
214
|
+
if isinstance(startup, dict) and startup.get("network_exposed"):
|
|
215
|
+
issues.append({
|
|
216
|
+
"area": "runtime_trust",
|
|
217
|
+
"severity": "warning",
|
|
218
|
+
"message": "Server is network-exposed",
|
|
219
|
+
})
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logging.warning("admin health-summary hardening failed: %s", exc)
|
|
222
|
+
|
|
223
|
+
# issue_count is a first-class field (not client-derived from issues[]).
|
|
224
|
+
# AdminConsole reads healthData.issue_count for the header sentence;
|
|
225
|
+
# falling back to || 1 when the field is missing misreports multi-issue
|
|
226
|
+
# states as a single problem.
|
|
227
|
+
issue_count = len(issues)
|
|
228
|
+
return {
|
|
229
|
+
"status": "attention" if issue_count else "ok",
|
|
230
|
+
"issue_count": issue_count,
|
|
231
|
+
"issues": issues,
|
|
232
|
+
}
|
|
233
|
+
|
|
162
234
|
@router.get("/admin/stats")
|
|
163
235
|
async def admin_stats(request: Request):
|
|
164
236
|
require_admin(request)
|
|
@@ -44,6 +44,65 @@ class AutomationRunNowRequest(BaseModel):
|
|
|
44
44
|
dry_run: bool = True
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _ensure_activity_run_source(run: Any) -> dict[str, Any]:
|
|
48
|
+
"""Guarantee the Act feed ``source`` field for 레시피/목표 badge split.
|
|
49
|
+
|
|
50
|
+
Capture 09 (``frontend/src/pages/Act.tsx``) distinguishes workflow vs agent
|
|
51
|
+
with ``run.source === "workflow" || Boolean(run.workflow_id)``. A store that
|
|
52
|
+
omits both collapses every row to "목표" and silently undoes the merged
|
|
53
|
+
timeline redesign. Prefer an explicit ``source``; otherwise infer from
|
|
54
|
+
``workflow_id`` / ``agent_id``.
|
|
55
|
+
"""
|
|
56
|
+
if not isinstance(run, dict):
|
|
57
|
+
return {"source": "agent", "id": None, "title": "", "status": ""}
|
|
58
|
+
row = dict(run)
|
|
59
|
+
source = str(row.get("source") or "").strip().lower()
|
|
60
|
+
if source in {"workflow", "agent"}:
|
|
61
|
+
row["source"] = source
|
|
62
|
+
return row
|
|
63
|
+
if row.get("workflow_id"):
|
|
64
|
+
row["source"] = "workflow"
|
|
65
|
+
return row
|
|
66
|
+
if row.get("agent_id"):
|
|
67
|
+
row["source"] = "agent"
|
|
68
|
+
return row
|
|
69
|
+
# Last resort: keep any non-empty legacy value, else agent.
|
|
70
|
+
row["source"] = source if source else "agent"
|
|
71
|
+
return row
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _with_run_truncation_meta(payload: Any, *, capped: int) -> dict[str, Any]:
|
|
75
|
+
"""Guarantee ``total`` / ``truncated`` on combined-run payloads.
|
|
76
|
+
|
|
77
|
+
Stores that already implement :meth:`list_combined_runs` may return only
|
|
78
|
+
``runs``. The Act feed needs to say how many rows are hidden when the
|
|
79
|
+
server slices agent+workflow history (default cap 20).
|
|
80
|
+
|
|
81
|
+
Every run is also passed through :func:`_ensure_activity_run_source` so the
|
|
82
|
+
레시피/목표 badge never loses its discriminator.
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(payload, dict):
|
|
85
|
+
return {"runs": [], "total": 0, "truncated": False}
|
|
86
|
+
runs = [_ensure_activity_run_source(run) for run in (payload.get("runs") or [])]
|
|
87
|
+
if "total" in payload:
|
|
88
|
+
try:
|
|
89
|
+
total = max(0, int(payload.get("total") or 0))
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
total = len(runs)
|
|
92
|
+
else:
|
|
93
|
+
total = len(runs)
|
|
94
|
+
if "truncated" in payload:
|
|
95
|
+
truncated = bool(payload.get("truncated"))
|
|
96
|
+
else:
|
|
97
|
+
truncated = total > len(runs) or total > capped
|
|
98
|
+
# Prefer the larger of declared total and visible rows so a buggy store
|
|
99
|
+
# never claims fewer total than the list it just returned.
|
|
100
|
+
total = max(total, len(runs))
|
|
101
|
+
if total > len(runs):
|
|
102
|
+
truncated = True
|
|
103
|
+
return {**payload, "runs": runs, "total": total, "truncated": truncated}
|
|
104
|
+
|
|
105
|
+
|
|
47
106
|
def _run_now_wait_seconds() -> float:
|
|
48
107
|
raw = os.environ.get("LATTICEAI_AUTOMATION_RUN_NOW_WAIT", "30")
|
|
49
108
|
try:
|
|
@@ -283,6 +342,59 @@ def create_automation_intelligence_router(
|
|
|
283
342
|
payload["review_item_id"] = review_item.get("id")
|
|
284
343
|
return payload
|
|
285
344
|
|
|
345
|
+
def _combined_runs(request: Request, limit: int = 20) -> dict[str, Any]:
|
|
346
|
+
require_user(request)
|
|
347
|
+
scope = gate_read(request)
|
|
348
|
+
capped = max(1, min(int(limit or 20), 100))
|
|
349
|
+
if hasattr(store, "list_combined_runs"):
|
|
350
|
+
payload = store.list_combined_runs(limit=capped, workspace_id=scope)
|
|
351
|
+
return _with_run_truncation_meta(payload, capped=capped)
|
|
352
|
+
# Store without the helper: merge the two existing listings in place.
|
|
353
|
+
agent_listing = (
|
|
354
|
+
store.list_agents(workspace_id=scope)
|
|
355
|
+
if hasattr(store, "list_agents")
|
|
356
|
+
else {"runs": []}
|
|
357
|
+
)
|
|
358
|
+
# Pull a wide window so agent+workflow merge can still fill ``capped``
|
|
359
|
+
# after sort; true total is the unscoped merge size before slice.
|
|
360
|
+
workflow_listing = (
|
|
361
|
+
store.list_workflow_runs(limit=max(capped, 100), workspace_id=scope)
|
|
362
|
+
if hasattr(store, "list_workflow_runs")
|
|
363
|
+
else {"runs": []}
|
|
364
|
+
)
|
|
365
|
+
from latticeai.core.workspace_runs import WorkspaceRuns
|
|
366
|
+
|
|
367
|
+
rows = []
|
|
368
|
+
for run in agent_listing.get("runs") or []:
|
|
369
|
+
rows.append(
|
|
370
|
+
_ensure_activity_run_source(
|
|
371
|
+
WorkspaceRuns.activity_run_row(run, source="agent")
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
for run in workflow_listing.get("runs") or []:
|
|
375
|
+
rows.append(
|
|
376
|
+
_ensure_activity_run_source(
|
|
377
|
+
WorkspaceRuns.activity_run_row(run, source="workflow")
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
rows.sort(key=lambda row: str(row.get("started_at") or ""), reverse=True)
|
|
381
|
+
total = len(rows)
|
|
382
|
+
return {
|
|
383
|
+
"runs": rows[:capped],
|
|
384
|
+
"total": total,
|
|
385
|
+
"truncated": total > capped,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@router.get("/api/activity/runs")
|
|
389
|
+
async def activity_runs(request: Request, limit: int = 20):
|
|
390
|
+
"""Unified agent + workflow run timeline (layout rebuild screen 09)."""
|
|
391
|
+
return _combined_runs(request, limit=limit)
|
|
392
|
+
|
|
393
|
+
@router.get("/automations/runs/combined")
|
|
394
|
+
async def automations_runs_combined(request: Request, limit: int = 20):
|
|
395
|
+
"""Alias used by the frontend handoff for the same combined timeline."""
|
|
396
|
+
return _combined_runs(request, limit=limit)
|
|
397
|
+
|
|
286
398
|
return router
|
|
287
399
|
|
|
288
400
|
|
|
@@ -6,6 +6,7 @@ remains as a deprecation shim. Route paths and response shapes are frozen by
|
|
|
6
6
|
data endpoints back the v3 SPA (Files, Hybrid Search, graph explorer).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Callable, Dict, List, Optional
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ from latticeai.api.workspace_scope import (
|
|
|
18
19
|
resolve_workspace_scope,
|
|
19
20
|
workspace_scope_from_request,
|
|
20
21
|
)
|
|
22
|
+
from latticeai.core.quiet import quiet
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class KnowledgeGraphIngestRequest(BaseModel):
|
|
@@ -71,6 +73,25 @@ def _format_context(matches: list, limit: int) -> str:
|
|
|
71
73
|
return "\n".join(lines)
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
def _pipeline_stage_view(*, count: int, pending: int) -> Dict[str, Any]:
|
|
77
|
+
"""Derive a coherent journey-ribbon stage from count + pending.
|
|
78
|
+
|
|
79
|
+
Invariants (enforced for layout-rebuild Capture screen 11):
|
|
80
|
+
- ``pending > 0`` → ``working``
|
|
81
|
+
- ``pending == 0`` and ``count > 0`` → ``done`` (never ``waiting``)
|
|
82
|
+
- ``pending == 0`` and ``count == 0`` → ``waiting`` (nothing arrived yet)
|
|
83
|
+
"""
|
|
84
|
+
safe_count = max(0, int(count))
|
|
85
|
+
safe_pending = max(0, int(pending))
|
|
86
|
+
if safe_pending > 0:
|
|
87
|
+
status = "working"
|
|
88
|
+
elif safe_count > 0:
|
|
89
|
+
status = "done"
|
|
90
|
+
else:
|
|
91
|
+
status = "waiting"
|
|
92
|
+
return {"count": safe_count, "pending": safe_pending, "status": status}
|
|
93
|
+
|
|
94
|
+
|
|
74
95
|
def create_knowledge_graph_router(
|
|
75
96
|
*,
|
|
76
97
|
get_graph: Callable[[], Any],
|
|
@@ -248,6 +269,118 @@ def create_knowledge_graph_router(
|
|
|
248
269
|
async def knowledge_graph_stats(request: Request):
|
|
249
270
|
return _scoped_stats(request)
|
|
250
271
|
|
|
272
|
+
@router.get("/knowledge-graph/pipeline/status")
|
|
273
|
+
async def knowledge_graph_pipeline_status(request: Request):
|
|
274
|
+
"""Per-stage counts + status for the Capture 3-step journey ribbon.
|
|
275
|
+
|
|
276
|
+
Aggregates existing store data only — no schema change. Top-level
|
|
277
|
+
``received`` / ``extracted`` / ``connected`` stay for simple clients;
|
|
278
|
+
``stages`` is the single source of truth for count, pending, and
|
|
279
|
+
status (``done`` / ``working`` / ``waiting``). Keys are omitted when
|
|
280
|
+
a value cannot be computed (never faked as 0 when the underlying
|
|
281
|
+
source is unavailable).
|
|
282
|
+
"""
|
|
283
|
+
kg, allowed = _scoped(request)
|
|
284
|
+
received: Optional[int] = None
|
|
285
|
+
extracted: Optional[int] = None
|
|
286
|
+
connected: Optional[int] = None
|
|
287
|
+
index_pending: Optional[int] = None
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
docs_payload = kg.list_documents(10_000)
|
|
291
|
+
documents = list(docs_payload.get("documents") or [])
|
|
292
|
+
if allowed is not None:
|
|
293
|
+
documents = _filter_scoped(kg, documents, allowed)
|
|
294
|
+
received = len(documents)
|
|
295
|
+
extracted = sum(
|
|
296
|
+
1
|
|
297
|
+
for doc in documents
|
|
298
|
+
if doc.get("indexed") or int(doc.get("chunks") or 0) > 0
|
|
299
|
+
)
|
|
300
|
+
except Exception:
|
|
301
|
+
quiet()
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
stats = _scoped_stats(request)
|
|
305
|
+
edges = stats.get("edges")
|
|
306
|
+
if isinstance(edges, dict):
|
|
307
|
+
connected = sum(int(value or 0) for value in edges.values())
|
|
308
|
+
elif isinstance(edges, (int, float)) and not isinstance(edges, bool):
|
|
309
|
+
connected = int(edges)
|
|
310
|
+
elif isinstance(stats.get("v2"), dict):
|
|
311
|
+
v2_edges = stats["v2"].get("edges")
|
|
312
|
+
if isinstance(v2_edges, (int, float)) and not isinstance(v2_edges, bool):
|
|
313
|
+
connected = int(v2_edges)
|
|
314
|
+
if received is None:
|
|
315
|
+
nodes = stats.get("nodes")
|
|
316
|
+
if isinstance(nodes, dict):
|
|
317
|
+
received = sum(
|
|
318
|
+
int(value or 0)
|
|
319
|
+
for key, value in nodes.items()
|
|
320
|
+
if str(key).lower() != "chunk"
|
|
321
|
+
)
|
|
322
|
+
elif isinstance(nodes, (int, float)) and not isinstance(nodes, bool):
|
|
323
|
+
received = int(nodes)
|
|
324
|
+
except Exception:
|
|
325
|
+
quiet()
|
|
326
|
+
|
|
327
|
+
index_fn = getattr(kg, "index_status", None)
|
|
328
|
+
if callable(index_fn):
|
|
329
|
+
try:
|
|
330
|
+
index = index_fn() or {}
|
|
331
|
+
if received is None and index.get("source_items") is not None:
|
|
332
|
+
received = int(index.get("source_items") or 0)
|
|
333
|
+
if extracted is None and index.get("ready_items") is not None:
|
|
334
|
+
extracted = int(index.get("ready_items") or 0)
|
|
335
|
+
if index.get("pending_items") is not None:
|
|
336
|
+
index_pending = max(0, int(index.get("pending_items") or 0))
|
|
337
|
+
elif index.get("pending") is not None:
|
|
338
|
+
index_pending = max(0, int(index.get("pending") or 0))
|
|
339
|
+
except Exception:
|
|
340
|
+
quiet()
|
|
341
|
+
|
|
342
|
+
payload: Dict[str, Any] = {}
|
|
343
|
+
if received is not None:
|
|
344
|
+
payload["received"] = max(0, int(received))
|
|
345
|
+
if extracted is not None:
|
|
346
|
+
payload["extracted"] = max(0, int(extracted))
|
|
347
|
+
if connected is not None:
|
|
348
|
+
payload["connected"] = max(0, int(connected))
|
|
349
|
+
|
|
350
|
+
if payload:
|
|
351
|
+
stages: Dict[str, Dict[str, Any]] = {}
|
|
352
|
+
if "received" in payload:
|
|
353
|
+
# Once listed as a document it has been received — no backlog.
|
|
354
|
+
stages["received"] = _pipeline_stage_view(
|
|
355
|
+
count=payload["received"], pending=0
|
|
356
|
+
)
|
|
357
|
+
if "extracted" in payload:
|
|
358
|
+
# Prefer vector-index backlog; fall back to docs not yet chunked.
|
|
359
|
+
if index_pending is not None:
|
|
360
|
+
extract_pending = index_pending
|
|
361
|
+
elif "received" in payload:
|
|
362
|
+
extract_pending = max(
|
|
363
|
+
0, int(payload["received"]) - int(payload["extracted"])
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
extract_pending = 0
|
|
367
|
+
stages["extracted"] = _pipeline_stage_view(
|
|
368
|
+
count=payload["extracted"], pending=extract_pending
|
|
369
|
+
)
|
|
370
|
+
if "connected" in payload:
|
|
371
|
+
# Connections are graph edges; backlog only when index reports
|
|
372
|
+
# pending items that still need linking (reuse extract backlog
|
|
373
|
+
# only when we have no better signal — 0 when graph is ready).
|
|
374
|
+
connect_pending = 0
|
|
375
|
+
if index_pending is not None and payload["connected"] == 0:
|
|
376
|
+
connect_pending = index_pending
|
|
377
|
+
stages["connected"] = _pipeline_stage_view(
|
|
378
|
+
count=payload["connected"], pending=connect_pending
|
|
379
|
+
)
|
|
380
|
+
payload["stages"] = stages
|
|
381
|
+
payload["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
|
382
|
+
return payload
|
|
383
|
+
|
|
251
384
|
@router.get("/knowledge-graph/schema")
|
|
252
385
|
async def knowledge_graph_schema(request: Request):
|
|
253
386
|
stats = _scoped_stats(request)
|
|
@@ -34,6 +34,30 @@ PRODUCTION_CSP = (
|
|
|
34
34
|
INVITE_COOKIE_NAME = "lattice_invite"
|
|
35
35
|
INVITE_COOKIE_TTL_SECONDS = 60 * 60 * 24 * 7
|
|
36
36
|
|
|
37
|
+
# Host-capacity plain-language judgment for System basic mode (layout rebuild).
|
|
38
|
+
# Thresholds live here so the frontend never re-derives copy from raw percents.
|
|
39
|
+
SYSINFO_READINESS_ROOMY_MAX = 55.0
|
|
40
|
+
SYSINFO_READINESS_TIGHT_MAX = 80.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def host_capacity_readiness(
|
|
44
|
+
*,
|
|
45
|
+
cpu_pct: float = 0.0,
|
|
46
|
+
ram_pct: float = 0.0,
|
|
47
|
+
gpu_mem_pct: float = 0.0,
|
|
48
|
+
) -> str:
|
|
49
|
+
"""Map host telemetry to a single plain-language readiness bucket.
|
|
50
|
+
|
|
51
|
+
Returns one of ``roomy`` / ``tight`` / ``low``. The product uses this so
|
|
52
|
+
basic-mode System copy and advanced-mode numbers share one judgment.
|
|
53
|
+
"""
|
|
54
|
+
load = max(float(cpu_pct or 0.0), float(ram_pct or 0.0), float(gpu_mem_pct or 0.0))
|
|
55
|
+
if load <= SYSINFO_READINESS_ROOMY_MAX:
|
|
56
|
+
return "roomy"
|
|
57
|
+
if load <= SYSINFO_READINESS_TIGHT_MAX:
|
|
58
|
+
return "tight"
|
|
59
|
+
return "low"
|
|
60
|
+
|
|
37
61
|
|
|
38
62
|
def _sign_invite_cookie(secret: str, *, now: Optional[int] = None) -> str:
|
|
39
63
|
"""Create an expiring, nonce-bearing invite-gate capability cookie."""
|
|
@@ -250,7 +274,11 @@ def create_static_routes_router(
|
|
|
250
274
|
|
|
251
275
|
@api_router.get("/local/sysinfo")
|
|
252
276
|
async def local_sysinfo(request: Request):
|
|
253
|
-
"""CPU / RAM / GPU(MLX)
|
|
277
|
+
"""CPU / RAM / GPU(MLX) usage plus a plain-language readiness bucket.
|
|
278
|
+
|
|
279
|
+
``readiness`` is ``roomy`` | ``tight`` | ``low`` so basic System copy
|
|
280
|
+
does not re-interpret raw percents on the client.
|
|
281
|
+
"""
|
|
254
282
|
require_user(request)
|
|
255
283
|
import re as _re
|
|
256
284
|
result: Dict[str, Any] = {
|
|
@@ -258,6 +286,7 @@ def create_static_routes_router(
|
|
|
258
286
|
"ram_pct": 0.0,
|
|
259
287
|
"gpu_mem_pct": 0.0,
|
|
260
288
|
"gpu_mem_gb": 0.0,
|
|
289
|
+
"readiness": "roomy",
|
|
261
290
|
}
|
|
262
291
|
try:
|
|
263
292
|
# CPU
|
|
@@ -291,6 +320,11 @@ def create_static_routes_router(
|
|
|
291
320
|
quiet()
|
|
292
321
|
except Exception as e:
|
|
293
322
|
result["error"] = str(e)
|
|
323
|
+
result["readiness"] = host_capacity_readiness(
|
|
324
|
+
cpu_pct=float(result.get("cpu_pct") or 0.0),
|
|
325
|
+
ram_pct=float(result.get("ram_pct") or 0.0),
|
|
326
|
+
gpu_mem_pct=float(result.get("gpu_mem_pct") or 0.0),
|
|
327
|
+
)
|
|
294
328
|
return result
|
|
295
329
|
|
|
296
330
|
return StaticRoutesBundle(
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from copy import deepcopy
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
|
-
MARKETPLACE_VERSION = "10.
|
|
13
|
+
MARKETPLACE_VERSION = "10.7.0"
|
|
14
14
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -883,6 +883,9 @@ class WorkspaceOSStore:
|
|
|
883
883
|
def list_workflow_runs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
884
884
|
return self.runs.list_workflow_runs(*args, **kwargs)
|
|
885
885
|
|
|
886
|
+
def list_combined_runs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
887
|
+
return self.runs.list_combined_runs(*args, **kwargs)
|
|
888
|
+
|
|
886
889
|
def mark_workflow_run_resolved(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
887
890
|
return self.runs.mark_workflow_run_resolved(*args, **kwargs)
|
|
888
891
|
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Dict
|
|
12
12
|
|
|
13
|
-
WORKSPACE_OS_VERSION = "10.
|
|
13
|
+
WORKSPACE_OS_VERSION = "10.7.0"
|
|
14
14
|
|
|
15
15
|
# Workspace types separate single-user Personal workspaces from shared
|
|
16
16
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|