ltcai 10.7.0 → 10.8.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 +52 -39
- package/docs/CHANGELOG.md +31 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -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/graph/retrieval_vector.py +69 -33
- package/lattice_brain/runtime/multi_agent.py +96 -15
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +9 -6
- package/latticeai/api/auth.py +37 -38
- package/latticeai/api/browser.py +7 -6
- package/latticeai/core/file_generation.py +141 -8
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +230 -0
- package/latticeai/core/workspace_computer_memory.py +84 -0
- package/latticeai/core/workspace_indexing.py +102 -0
- package/latticeai/core/workspace_onboarding.py +104 -0
- package/latticeai/core/workspace_os.py +24 -207
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_relationships.py +99 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/package.json +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_screenshot_pixel_delta.py +92 -10
- package/scripts/release_screen_claims.json +37 -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 -37
- package/static/app/assets/{Act-DIbkoJqs.js → Act-CbQNsFgc.js} +1 -1
- package/static/app/assets/{AdminConsole-Cbi6Jre6.js → AdminConsole-Bs8SR8fE.js} +1 -1
- package/static/app/assets/{Brain-BPfuXZ71.js → Brain-BZIPsZlH.js} +1 -1
- package/static/app/assets/{BrainHome-87XaAS4V.js → BrainHome-BaH5MDce.js} +1 -1
- package/static/app/assets/{BrainSignals-ExpcdiUi.js → BrainSignals-aDt-nSsW.js} +1 -1
- package/static/app/assets/{Capture-Bxe4WiVX.js → Capture-DDv940Bv.js} +1 -1
- package/static/app/assets/{CommandPalette-B1PV5Yd4.js → CommandPalette-Dq_MmPBl.js} +1 -1
- package/static/app/assets/{Library-BRlJJmwZ.js → Library-BkShbqmN.js} +1 -1
- package/static/app/assets/{LivingBrain-D61qG3WY.js → LivingBrain-CXb8094H.js} +1 -1
- package/static/app/assets/ProductFlow-BaiqFWLO.js +1 -0
- package/static/app/assets/{ReviewCard-BoiWv0Y1.js → ReviewCard-Be7E7X18.js} +1 -1
- package/static/app/assets/{System-Ci0HEtTJ.js → System-DCstbTxC.js} +1 -1
- package/static/app/assets/arrow-left-DC9Oquhs.js +1 -0
- package/static/app/assets/{bot-BaPt4PPn.js → bot-BR0eoeXY.js} +1 -1
- package/static/app/assets/{brain-BSkWaj3i.js → brain-BmWa1wea.js} +1 -1
- package/static/app/assets/{button-CKwtO1O0.js → button-U1nU7xX4.js} +1 -1
- package/static/app/assets/{circle-pause-CiUc5RRk.js → circle-pause-DG5KHjDL.js} +1 -1
- package/static/app/assets/{circle-play-BaTDxzR9.js → circle-play-Chhcuq90.js} +1 -1
- package/static/app/assets/{cpu-Dm1gHuP1.js → cpu-pkOSlT9h.js} +1 -1
- package/static/app/assets/{download-C32oGU-C.js → download-CSRwz923.js} +1 -1
- package/static/app/assets/{folder-open-Ch2jLVdD.js → folder-open-DiMJ7W_7.js} +1 -1
- package/static/app/assets/{hard-drive-BBjA5st8.js → hard-drive-iyLPAPdp.js} +1 -1
- package/static/app/assets/index-BZRvGOM2.css +2 -0
- package/static/app/assets/{index-J6h01X78.js → index-_ldE9TAX.js} +3 -3
- package/static/app/assets/{input-y6ABdKWY.js → input-e_r2hFAY.js} +1 -1
- package/static/app/assets/{permissionCopy-BEtc0Ihd.js → permissionCopy-9uOHktz3.js} +1 -1
- package/static/app/assets/{primitives-GSFXF2Pd.js → primitives-CnEAOmg2.js} +1 -1
- package/static/app/assets/search-veep4n6f.js +1 -0
- package/static/app/assets/{share-2-BgGq2A63.js → share-2-DLDJJKmI.js} +1 -1
- package/static/app/assets/{shield-alert-BYA491cj.js → shield-alert-BTtE-2EZ.js} +1 -1
- package/static/app/assets/{textarea-BAcim3fs.js → textarea-Bbzkcgqe.js} +1 -1
- package/static/app/assets/{useFocusTrap-qLg24jiv.js → useFocusTrap-DJzQd-Za.js} +1 -1
- package/static/app/assets/{useQuery-xkAu9eDX.js → useQuery-6R_tqHRe.js} +1 -1
- package/static/app/assets/{utils-CogdofAA.js → utils-XDsjcrhS.js} +1 -1
- package/static/app/assets/{workspace-BMvfx9_R.js → workspace-CwrG8LHq.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/ProductFlow-BxjxI8Aa.js +0 -1
- package/static/app/assets/arrow-left-DArdBUxd.js +0 -1
- package/static/app/assets/index-BV7Yf6Eq.css +0 -2
- package/static/app/assets/search-CaIZKyo_.js +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/TaeSooPark-PTS/LatticeAI/actions/workflows/ci.yml)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
|
-

|
|
15
15
|
|
|
16
16
|
Chat, files, folders, notes, and web pages all flow into one durable knowledge
|
|
17
17
|
graph on your computer. Any model — local MLX or cloud — can speak with that
|
|
@@ -24,10 +24,10 @@ memory. Nothing leaves your machine without explicit consent.
|
|
|
24
24
|
|
|
25
25
|
| | |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| **Chat with a Brain that remembers** — every conversation grows durable, source-linked memory  | **See how knowledge connects** — a real relationship graph, not a file list  |
|
|
28
|
+
| **Capture anything** — files, whole folders, notes, screenshots, web pages  | **Automate with review** — agent changes become proposals you approve first  |
|
|
29
|
+
| **Pick a model in one click** — recommended local models for your hardware  | **Stay in control** — audit, roles, retention in a separate admin surface  |
|
|
30
|
+
| **Watch a file become memory** — three named steps, not a pipeline diagram  | **Say how much it may do alone** — one dial in plain words; dangerous actions stay blocked either way  |
|
|
31
31
|
|
|
32
32
|
## Why Lattice AI
|
|
33
33
|
|
|
@@ -58,51 +58,63 @@ First-run flow — wake the Brain, pick the owner, load a recommended model:
|
|
|
58
58
|
|
|
59
59
|
| | | |
|
|
60
60
|
| --- | --- | --- |
|
|
61
|
-
|  |  |  |
|
|
62
62
|
|
|
63
63
|
Screenshot index and capture notes:
|
|
64
|
-
[output/release/v10.
|
|
64
|
+
[output/release/v10.8.0/SCREENSHOT_INDEX.md](output/release/v10.8.0/SCREENSHOT_INDEX.md)
|
|
65
65
|
|
|
66
66
|
## Current Release
|
|
67
67
|
|
|
68
|
-
The current release is **10.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
68
|
+
The current release is **10.8.0 — Within Reach**:
|
|
69
|
+
|
|
70
|
+
10.7.0 rearranged all twelve screens. 10.8.0 is about the things that were
|
|
71
|
+
already there but out of reach: a button below the fold, a message in a
|
|
72
|
+
language you did not choose, a file a small model nearly produced.
|
|
73
|
+
|
|
74
|
+
- **The first three screens fit on the screen.** Onboarding rendered a 390px
|
|
75
|
+
decorative Brain above *every* step, so on a 1440×900 display the login
|
|
76
|
+
form, the recommended model and the install progress all began below the
|
|
77
|
+
fold — the first thing the product asked a new person to do was scroll. The
|
|
78
|
+
organism is a hero on the welcome step and a 104px mark after it; the
|
|
79
|
+
welcome screen itself went from 1175px of content to 770px, so its primary
|
|
80
|
+
button is visible without scrolling.
|
|
81
|
+
- **One language per screen, including the ones the server writes.** API
|
|
82
|
+
messages were literals at the raise site, written in whichever language the
|
|
83
|
+
endpoint's author was thinking in — Korean from `auth.py`, English from
|
|
84
|
+
`browser.py`. `latticeai/core/messages.py` is a catalog with both, resolved
|
|
85
|
+
from `X-Lattice-Language` (falling back to `Accept-Language`); the browser
|
|
86
|
+
extension, which showed English and Korean in the same popup, has one too.
|
|
87
|
+
`tests/unit/test_server_messages.py` fails on a message that exists in only
|
|
88
|
+
one language, and on a router that reverts to a hardcoded literal.
|
|
89
|
+
- **A small model's near-miss is no longer thrown away.** File generation
|
|
90
|
+
picked the *longest* rejected reply to repair, so a 900-character apology
|
|
91
|
+
beat a truncated-but-real HTML document; it now scores candidates by how
|
|
92
|
+
close they are to being a file. A byte-identical repeated reply buys one
|
|
93
|
+
extra attempt with a prompt that names the repetition. Prose file types
|
|
94
|
+
(`.md`, `.txt`) had no validation at all and would save "Sure! Here is the
|
|
95
|
+
document:" as the document.
|
|
96
|
+
- **Re-indexing costs what changed.** The incremental vector rebuild
|
|
97
|
+
materialised every node and chunk in memory and asked one `SELECT` per item
|
|
98
|
+
whether it had changed. It streams now, with a single prefetched hash map —
|
|
99
|
+
and a settled index performs zero embeddings, asserted rather than assumed.
|
|
100
|
+
- **More ground under the frontend.** Statement coverage 47.35% → 52.26%,
|
|
101
|
+
424 → 504 tests, including the app shell (`App.tsx`, previously 0%), the
|
|
102
|
+
admin console (0%), the folder picker (2.7%) and the API client.
|
|
103
|
+
- **Evidence bound to this build.** `output/release/v10.8.0/` holds the twelve
|
|
92
104
|
capture screens, walkthrough gif/webm, and the asset-manifest / mock-server
|
|
93
105
|
fingerprints that lint re-checks before merge.
|
|
94
|
-
- **Exact artifact names only.** Publish paths list `dist/ltcai-10.
|
|
95
|
-
`ltcai-10.
|
|
106
|
+
- **Exact artifact names only.** Publish paths list `dist/ltcai-10.8.0-*` and
|
|
107
|
+
`ltcai-10.8.0.tgz` — never `dist/*`.
|
|
96
108
|
|
|
97
109
|
Release notes: [RELEASE.md](RELEASE.md) · Full history: [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
|
98
110
|
|
|
99
|
-
Expected artifacts for 10.
|
|
111
|
+
Expected artifacts for 10.8.0 release must use exact filenames:
|
|
100
112
|
|
|
101
|
-
- `dist/ltcai-10.
|
|
102
|
-
- `dist/ltcai-10.
|
|
103
|
-
- `ltcai-10.
|
|
104
|
-
- `dist/ltcai-10.
|
|
105
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_10.
|
|
113
|
+
- `dist/ltcai-10.8.0-py3-none-any.whl`
|
|
114
|
+
- `dist/ltcai-10.8.0.tar.gz`
|
|
115
|
+
- `ltcai-10.8.0.tgz`
|
|
116
|
+
- `dist/ltcai-10.8.0.vsix`
|
|
117
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_10.8.0_aarch64.dmg`
|
|
106
118
|
|
|
107
119
|
Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
|
|
108
120
|
|
|
@@ -137,6 +149,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for details and
|
|
|
137
149
|
|
|
138
150
|
| Version | Theme |
|
|
139
151
|
| --- | --- |
|
|
152
|
+
| 10.8.0 | Within Reach |
|
|
140
153
|
| 10.7.0 | Plain Surface |
|
|
141
154
|
| 10.6.4 | Loud Limits |
|
|
142
155
|
| 10.6.3 | Loud Limits |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ The top entry is either the current unreleased main-branch work or the current
|
|
|
4
4
|
release line. Older entries are historical and may describe behavior as it
|
|
5
5
|
existed at that release.
|
|
6
6
|
|
|
7
|
+
## [10.8.0] - 2026-08-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 온보딩 login/recommend/install 3개 화면에서 주 작업이 접힌 부분 아래에서
|
|
11
|
+
시작하던 문제 — 히어로 브레인을 단계별 크기(`data-scale`)로 분리.
|
|
12
|
+
- 설치 화면이 번역 키 원문(`flow.install.stage.idle`)을 사용자에게 출력하던
|
|
13
|
+
문제 — `t()`가 `defaultValue` 옵션을 읽지 않았고, 옵션 자체가 보간 값으로
|
|
14
|
+
새어 들어가고 있었습니다.
|
|
15
|
+
- 추천 모델 화면이 셸 중앙에서 왼쪽으로 치우쳐 있던 문제(`margin-inline`).
|
|
16
|
+
- Brain 대화 홈의 추천 칩이 4글자를 담은 채 952px로 늘어나던 문제 —
|
|
17
|
+
`@media (max-height)` 블록이 `display:flex`만 재선언하고 `flex-direction`을
|
|
18
|
+
다시 쓰지 않아 기본 규칙의 `column` 축을 물려받고 있었습니다.
|
|
19
|
+
- 브라우저 확장 팝업이 한 창 안에서 한국어와 영어를 섞어 보여주던 문제.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `latticeai/core/messages.py` — 서버 사이드 메시지 카탈로그(ko/en)와
|
|
23
|
+
요청 기반 언어 결정(`X-Lattice-Language` → `Accept-Language`).
|
|
24
|
+
- `frontend/src/styles/mediaQueryOverride.test.ts` — 미디어 쿼리가 `display`를
|
|
25
|
+
재선언하면서 축을 다시 쓰지 않는 부분 오버라이드 가드.
|
|
26
|
+
- 신규 테스트 스위트: `App.tsx`, `AdminConsole.tsx`, `lib/folderPicker.ts`,
|
|
27
|
+
`api/client.ts` 확장, 멀티에이전트 JSON 복구, 증분 인덱스 재빌드.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- 파일 생성이 실패 응답 중 "가장 긴" 것이 아니라 "가장 파일에 가까운" 것을
|
|
31
|
+
복구에 넘깁니다. 동일 응답 반복 시 1회 한정 추가 시도.
|
|
32
|
+
- 산문 파일 타입(`.md`/`.txt`/…)에도 검증 추가 — 파일에 *대한* 답변은 파일이
|
|
33
|
+
아닙니다.
|
|
34
|
+
- 멀티에이전트 계획 파서: 균형 스팬 스캔 + `<think>` 제거 + trailing comma 복구.
|
|
35
|
+
- 증분 벡터 재빌드가 스트리밍 + 단일 해시맵 조회로 전환.
|
|
36
|
+
- `latticeai/core/workspace_os.py` 1128 → 945줄.
|
|
37
|
+
|
|
7
38
|
## [10.7.0] - 2026-08-04
|
|
8
39
|
|
|
9
40
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Community And Plugins
|
|
2
2
|
|
|
3
|
-
Current release: **10.
|
|
3
|
+
Current release: **10.8.0 — Within Reach**.
|
|
4
4
|
|
|
5
5
|
LatticeAI defines the path from a strong local-first framework (8.4.0
|
|
6
6
|
action-aware baseline, 8.5.0 registry+DI hardening, 8.6.0 capture/navigation
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **Status: canonical** — current contributor guidance, kept in sync with the
|
|
4
4
|
> current release.
|
|
5
5
|
|
|
6
|
-
Current release: **10.
|
|
6
|
+
Current release: **10.8.0 — Within Reach**.
|
|
7
7
|
|
|
8
8
|
This document is for contributors working on the local-first Digital Brain
|
|
9
9
|
codebase. Product positioning and quick start stay in `README.md`; release
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Onboarding
|
|
2
2
|
|
|
3
|
-
Current release: **10.
|
|
3
|
+
Current release: **10.8.0 — Within Reach**.
|
|
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
package/docs/TRUST_MODEL.md
CHANGED
package/docs/WHY_LATTICE.md
CHANGED
package/docs/kg-schema.md
CHANGED
|
@@ -136,14 +136,45 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
136
136
|
)
|
|
137
137
|
return {"current": current, "recorded": recorded, "stale_embedder": stale}
|
|
138
138
|
|
|
139
|
+
def _vector_text_hashes(self, conn: sqlite3.Connection) -> Dict[str, str]:
|
|
140
|
+
"""``item_id -> text_hash`` for rows already embedded by *this* embedder.
|
|
141
|
+
|
|
142
|
+
The incremental rebuild's job is mostly deciding what it does *not*
|
|
143
|
+
have to do, and it used to ask that question with one ``SELECT`` per
|
|
144
|
+
candidate item — a round trip per node and per chunk on every run,
|
|
145
|
+
almost all of which answer "unchanged". One query returning two short
|
|
146
|
+
columns replaces all of them.
|
|
147
|
+
|
|
148
|
+
Rows written by a different embedder are left out, so they compare as
|
|
149
|
+
missing and get re-embedded, which is what an embedder swap requires.
|
|
150
|
+
"""
|
|
151
|
+
return {
|
|
152
|
+
row["item_id"]: row["text_hash"]
|
|
153
|
+
for row in conn.execute(
|
|
154
|
+
"""
|
|
155
|
+
SELECT item_id, text_hash
|
|
156
|
+
FROM vector_embeddings
|
|
157
|
+
WHERE embedding_model=? AND embedding_dim=?
|
|
158
|
+
""",
|
|
159
|
+
(self._embedding_model.model_id, self._embedding_model.dim),
|
|
160
|
+
).fetchall()
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
def _iter_vector_source_items(
|
|
140
164
|
self,
|
|
141
165
|
conn: sqlite3.Connection,
|
|
142
166
|
*,
|
|
143
167
|
include_nodes: bool = True,
|
|
144
168
|
include_chunks: bool = True,
|
|
145
|
-
) ->
|
|
146
|
-
|
|
169
|
+
) -> Iterator[Dict[str, Any]]:
|
|
170
|
+
"""Stream the graph's embeddable text, one item at a time.
|
|
171
|
+
|
|
172
|
+
Yields rather than returns a list. Every caller consumes this exactly
|
|
173
|
+
once in a ``for``, and building the list first meant a rebuild held
|
|
174
|
+
the full text of every node and chunk in memory simultaneously — the
|
|
175
|
+
one shape guaranteed to fail on precisely the large graph that most
|
|
176
|
+
needs the index.
|
|
177
|
+
"""
|
|
147
178
|
if include_nodes:
|
|
148
179
|
for row in conn.execute(
|
|
149
180
|
"""
|
|
@@ -160,15 +191,13 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
160
191
|
metadata=metadata,
|
|
161
192
|
)
|
|
162
193
|
if text:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
)
|
|
194
|
+
yield {
|
|
195
|
+
"item_id": row["id"],
|
|
196
|
+
"item_type": "node",
|
|
197
|
+
"source_node": row["id"],
|
|
198
|
+
"text": text,
|
|
199
|
+
"metadata": {"node_type": row["type"], **metadata},
|
|
200
|
+
}
|
|
172
201
|
if include_chunks:
|
|
173
202
|
for row in conn.execute(
|
|
174
203
|
"""
|
|
@@ -181,19 +210,16 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
181
210
|
metadata = _safe_loads(row["metadata_json"])
|
|
182
211
|
text = _clean_text(row["text"] or "")
|
|
183
212
|
if text:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
)
|
|
196
|
-
return items
|
|
213
|
+
yield {
|
|
214
|
+
"item_id": row["id"],
|
|
215
|
+
"item_type": "chunk",
|
|
216
|
+
"source_node": row["id"],
|
|
217
|
+
"text": text,
|
|
218
|
+
"metadata": {
|
|
219
|
+
**metadata,
|
|
220
|
+
"parent_source_node": row["parent_source_node"],
|
|
221
|
+
},
|
|
222
|
+
}
|
|
197
223
|
|
|
198
224
|
def index_node_incremental(self, node_id: str) -> Dict[str, Any]:
|
|
199
225
|
"""Embed/index only ``node_id`` and its chunks (incremental sync).
|
|
@@ -345,15 +371,22 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
345
371
|
conn.execute(
|
|
346
372
|
f"DELETE FROM vector_embeddings WHERE item_type IN ({','.join(filters)})"
|
|
347
373
|
)
|
|
348
|
-
|
|
374
|
+
# After a full wipe nothing is current by definition, so the
|
|
375
|
+
# prefetch would only be a wasted scan of a table we just
|
|
376
|
+
# emptied. Incremental is where it pays: it turns "one SELECT
|
|
377
|
+
# per item, nearly all of which say unchanged" into one query.
|
|
378
|
+
known = {} if full else self._vector_text_hashes(conn)
|
|
379
|
+
total = indexed = skipped = 0
|
|
380
|
+
for item in self._iter_vector_source_items(
|
|
349
381
|
conn,
|
|
350
382
|
include_nodes=include_nodes,
|
|
351
383
|
include_chunks=include_chunks,
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
384
|
+
):
|
|
385
|
+
total += 1
|
|
386
|
+
if known.get(item["item_id"]) == _sha256_text(_clean_text(item["text"])):
|
|
387
|
+
skipped += 1
|
|
388
|
+
continue
|
|
389
|
+
if self._upsert_vector_item(conn, **item):
|
|
357
390
|
indexed += 1
|
|
358
391
|
else:
|
|
359
392
|
skipped += 1
|
|
@@ -367,7 +400,7 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
367
400
|
""",
|
|
368
401
|
(
|
|
369
402
|
_now(),
|
|
370
|
-
|
|
403
|
+
total,
|
|
371
404
|
indexed,
|
|
372
405
|
skipped,
|
|
373
406
|
_json(
|
|
@@ -390,7 +423,7 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
390
423
|
"status": "completed",
|
|
391
424
|
"operation_id": op_id,
|
|
392
425
|
"full": bool(full),
|
|
393
|
-
"items_total":
|
|
426
|
+
"items_total": total,
|
|
394
427
|
"items_indexed": indexed,
|
|
395
428
|
"items_skipped": skipped,
|
|
396
429
|
"duration_ms": duration_ms,
|
|
@@ -442,7 +475,10 @@ class KnowledgeGraphVectorMixin(_Core):
|
|
|
442
475
|
"SELECT item_type, COUNT(*) AS count FROM vector_embeddings GROUP BY item_type"
|
|
443
476
|
)
|
|
444
477
|
}
|
|
445
|
-
|
|
478
|
+
# Materialised on purpose, unlike the rebuild path: status walks
|
|
479
|
+
# this set twice (once for ids, once to classify each item) and
|
|
480
|
+
# reports a count, none of which a one-shot iterator can serve.
|
|
481
|
+
source_items = list(self._iter_vector_source_items(conn))
|
|
446
482
|
vector_rows = {
|
|
447
483
|
row["item_id"]: row
|
|
448
484
|
for row in conn.execute(
|
|
@@ -41,13 +41,15 @@ orchestrator for the product ``/agents`` surface.
|
|
|
41
41
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
|
+
import json
|
|
45
|
+
import re
|
|
44
46
|
from dataclasses import dataclass, field
|
|
45
47
|
from typing import Any, Callable, Dict, List, Optional
|
|
46
48
|
|
|
47
49
|
from ..utils import now_iso as _now
|
|
48
50
|
from .contracts import multi_agent_contract
|
|
49
51
|
|
|
50
|
-
MULTI_AGENT_VERSION = "10.
|
|
52
|
+
MULTI_AGENT_VERSION = "10.8.0"
|
|
51
53
|
|
|
52
54
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
53
55
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -448,23 +450,102 @@ def default_role_runner(
|
|
|
448
450
|
return runner
|
|
449
451
|
|
|
450
452
|
|
|
453
|
+
def _json_object_candidates(text: str) -> List[str]:
|
|
454
|
+
"""Every balanced ``{...}`` span in ``text``, outermost first.
|
|
455
|
+
|
|
456
|
+
``find("{")`` to ``rfind("}")`` is one span, and it is the wrong one
|
|
457
|
+
whenever the model wrote anything after its object — a closing remark, a
|
|
458
|
+
second example, a stray ``}`` in prose — because the slice then contains
|
|
459
|
+
the object *and* the noise, and parses as neither. Scanning for balance
|
|
460
|
+
yields the actual objects, in the order they appear.
|
|
461
|
+
|
|
462
|
+
String literals are tracked so a brace inside ``"a } b"`` does not close a
|
|
463
|
+
span early.
|
|
464
|
+
"""
|
|
465
|
+
spans: List[str] = []
|
|
466
|
+
depth = 0
|
|
467
|
+
start = -1
|
|
468
|
+
in_string = False
|
|
469
|
+
escaped = False
|
|
470
|
+
for index, char in enumerate(text):
|
|
471
|
+
if in_string:
|
|
472
|
+
if escaped:
|
|
473
|
+
escaped = False
|
|
474
|
+
elif char == "\\":
|
|
475
|
+
escaped = True
|
|
476
|
+
elif char == '"':
|
|
477
|
+
in_string = False
|
|
478
|
+
continue
|
|
479
|
+
if char == '"':
|
|
480
|
+
in_string = True
|
|
481
|
+
elif char == "{":
|
|
482
|
+
if depth == 0:
|
|
483
|
+
start = index
|
|
484
|
+
depth += 1
|
|
485
|
+
elif char == "}":
|
|
486
|
+
if depth:
|
|
487
|
+
depth -= 1
|
|
488
|
+
if depth == 0 and start >= 0:
|
|
489
|
+
spans.append(text[start : index + 1])
|
|
490
|
+
return spans
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
#: A comma directly before a closing brace/bracket. Small local models emit
|
|
494
|
+
#: these constantly — it is the single most common reason an otherwise correct
|
|
495
|
+
#: plan is thrown away.
|
|
496
|
+
_TRAILING_COMMA_RE = re.compile(r",(\s*[}\]])")
|
|
497
|
+
|
|
498
|
+
#: Reasoning models wrap their scratchpad in these, and the scratchpad is full
|
|
499
|
+
#: of braces. Removed before anything else so it cannot poison a span.
|
|
500
|
+
_THINK_BLOCK_RE = re.compile(
|
|
501
|
+
r"<(think|thinking|reasoning|reflection)>.*?</\1>",
|
|
502
|
+
re.DOTALL | re.IGNORECASE,
|
|
503
|
+
)
|
|
504
|
+
_THINK_OPEN_RE = re.compile(r"<(think|thinking|reasoning)>.*\Z", re.DOTALL | re.IGNORECASE)
|
|
505
|
+
|
|
506
|
+
|
|
451
507
|
def _extract_json_object(raw: str) -> Dict[str, Any]:
|
|
452
|
-
"""Parse one JSON object out of an LLM response (fences/prose tolerated).
|
|
453
|
-
import json as _json
|
|
454
|
-
import re as _re
|
|
508
|
+
"""Parse one JSON object out of an LLM response (fences/prose tolerated).
|
|
455
509
|
|
|
510
|
+
Recovery only — never invention. Every strategy here returns something the
|
|
511
|
+
model actually wrote; when none of them parse, this still raises and the
|
|
512
|
+
run fails loudly with the raw output preserved, exactly as before. The
|
|
513
|
+
point is that a 1–4B model's plan should not be discarded over a trailing
|
|
514
|
+
comma or a farewell sentence.
|
|
515
|
+
"""
|
|
456
516
|
text = str(raw or "").strip()
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
517
|
+
text = _THINK_BLOCK_RE.sub("", text)
|
|
518
|
+
text = _THINK_OPEN_RE.sub("", text).strip()
|
|
519
|
+
|
|
520
|
+
candidates: List[str] = []
|
|
521
|
+
for fenced in re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL):
|
|
522
|
+
candidates.append(fenced)
|
|
523
|
+
candidates.extend(_json_object_candidates(text))
|
|
524
|
+
if text.startswith("{"):
|
|
525
|
+
candidates.append(text)
|
|
526
|
+
|
|
527
|
+
last_error: Optional[Exception] = None
|
|
528
|
+
empty_fallback: Optional[Dict[str, Any]] = None
|
|
529
|
+
for candidate in candidates:
|
|
530
|
+
for attempt in (candidate, _TRAILING_COMMA_RE.sub(r"\1", candidate)):
|
|
531
|
+
try:
|
|
532
|
+
parsed = json.loads(attempt)
|
|
533
|
+
except ValueError as exc:
|
|
534
|
+
last_error = exc
|
|
535
|
+
continue
|
|
536
|
+
if not isinstance(parsed, dict):
|
|
537
|
+
last_error = ValueError("model returned JSON that is not an object")
|
|
538
|
+
continue
|
|
539
|
+
if parsed:
|
|
540
|
+
return parsed
|
|
541
|
+
# A prose aside like "I considered using { } notation" is a
|
|
542
|
+
# syntactically valid empty object, and taking the first thing that
|
|
543
|
+
# parses handed that back as the plan. An empty object is only ever
|
|
544
|
+
# the answer when nothing else in the reply is one.
|
|
545
|
+
empty_fallback = parsed
|
|
546
|
+
if empty_fallback is not None:
|
|
547
|
+
return empty_fallback
|
|
548
|
+
raise last_error or ValueError("model returned no JSON object")
|
|
468
549
|
|
|
469
550
|
|
|
470
551
|
def llm_role_runner(
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/admin.py
CHANGED
|
@@ -5,9 +5,10 @@ from collections import defaultdict
|
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
from typing import Any, Callable, Dict, List, Optional
|
|
7
7
|
|
|
8
|
-
from fastapi import APIRouter,
|
|
8
|
+
from fastapi import APIRouter, Query, Request
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
+
from latticeai.core.messages import http_error, resolve_language
|
|
11
12
|
from latticeai.core.workspace_os import DEFAULT_WORKSPACE_ID
|
|
12
13
|
|
|
13
14
|
|
|
@@ -391,17 +392,18 @@ def create_admin_router(
|
|
|
391
392
|
|
|
392
393
|
@router.patch("/admin/users/{email:path}")
|
|
393
394
|
async def admin_update_user(email: str, req: AdminUserUpdate, request: Request):
|
|
395
|
+
lang = resolve_language(request)
|
|
394
396
|
admin_email, users = require_admin(request)
|
|
395
397
|
if email not in users:
|
|
396
|
-
raise
|
|
398
|
+
raise http_error(404, "auth.user_not_found", lang)
|
|
397
399
|
before = public_user(email, users[email], users)
|
|
398
400
|
if req.role is not None:
|
|
399
401
|
if req.role not in {"admin", "user"}:
|
|
400
|
-
raise
|
|
402
|
+
raise http_error(400, "admin.invalid_role", lang)
|
|
401
403
|
users[email]["role"] = req.role
|
|
402
404
|
if req.disabled is not None:
|
|
403
405
|
if email == admin_email and req.disabled:
|
|
404
|
-
raise
|
|
406
|
+
raise http_error(400, "admin.cannot_disable_self", lang)
|
|
405
407
|
users[email]["disabled"] = req.disabled
|
|
406
408
|
save_users(users)
|
|
407
409
|
after = public_user(email, users[email], users)
|
|
@@ -410,11 +412,12 @@ def create_admin_router(
|
|
|
410
412
|
|
|
411
413
|
@router.delete("/admin/users/{email:path}")
|
|
412
414
|
async def admin_delete_user(email: str, request: Request):
|
|
415
|
+
lang = resolve_language(request)
|
|
413
416
|
admin_email, users = require_admin(request)
|
|
414
417
|
if email == admin_email:
|
|
415
|
-
raise
|
|
418
|
+
raise http_error(400, "admin.cannot_delete_self", lang)
|
|
416
419
|
if email not in users:
|
|
417
|
-
raise
|
|
420
|
+
raise http_error(404, "auth.user_not_found", lang)
|
|
418
421
|
deleted = public_user(email, users[email], users)
|
|
419
422
|
append_audit_event("user_delete", user_email=admin_email, target_email=email, deleted_user=deleted)
|
|
420
423
|
del users[email]
|