ltcai 10.8.0 → 10.10.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 +44 -63
- package/docs/CHANGELOG.md +69 -237
- 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/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +9 -6
- package/latticeai/api/chat_agent_http.py +2 -1
- package/latticeai/api/chat_history.py +4 -2
- package/latticeai/api/chat_intents.py +14 -14
- package/latticeai/api/knowledge_graph.py +5 -4
- package/latticeai/api/local_files.py +25 -15
- package/latticeai/api/mcp.py +15 -11
- package/latticeai/api/memory.py +2 -1
- package/latticeai/api/models.py +39 -24
- package/latticeai/api/network_boundary.py +9 -4
- package/latticeai/api/portability.py +27 -19
- package/latticeai/api/project_sessions.py +7 -5
- package/latticeai/api/review_queue.py +12 -7
- package/latticeai/api/setup.py +10 -11
- package/latticeai/api/static_routes.py +62 -46
- package/latticeai/api/tools.py +7 -6
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +20 -4
- package/latticeai/core/messages.py +222 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot.py +20 -18
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/model_loading.py +63 -27
- package/latticeai/services/product_readiness.py +5 -3
- package/package.json +3 -2
- package/scripts/check_current_release_docs.mjs +4 -2
- package/scripts/check_server_i18n.mjs +96 -0
- package/scripts/release_screen_claims.json +31 -8
- 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-CbQNsFgc.js → Act-CC8SpNRm.js} +1 -1
- package/static/app/assets/{AdminConsole-Bs8SR8fE.js → AdminConsole-DGIY_Tb_.js} +1 -1
- package/static/app/assets/{Brain-BZIPsZlH.js → Brain-BDSC2Mol.js} +2 -2
- package/static/app/assets/BrainHome-BYgYp3ON.js +2 -0
- package/static/app/assets/BrainSignals-DYwVFmcO.js +1 -0
- package/static/app/assets/Capture-ClcKLsmI.js +1 -0
- package/static/app/assets/{CommandPalette-Dq_MmPBl.js → CommandPalette-DpwbyF1S.js} +1 -1
- package/static/app/assets/Library-D8VNtffM.js +1 -0
- package/static/app/assets/LivingBrain-CeWwc0NN.js +1 -0
- package/static/app/assets/ProductFlow-CS5CCM4t.js +1 -0
- package/static/app/assets/{ReviewCard-Be7E7X18.js → ReviewCard-Bap4yPfI.js} +1 -1
- package/static/app/assets/System-odPxU1YL.js +1 -0
- package/static/app/assets/arrow-left-J47Yde1U.js +1 -0
- package/static/app/assets/{bot-BR0eoeXY.js → bot-CmvhqTeF.js} +1 -1
- package/static/app/assets/brain-DLT9uuGq.js +1 -0
- package/static/app/assets/{button-U1nU7xX4.js → button-DyppYhFH.js} +1 -1
- package/static/app/assets/{circle-pause-DG5KHjDL.js → circle-pause-Bm5z7EBI.js} +1 -1
- package/static/app/assets/{circle-play-Chhcuq90.js → circle-play-GjKdH0Fg.js} +1 -1
- package/static/app/assets/{cpu-pkOSlT9h.js → cpu-CuVMv-F5.js} +1 -1
- package/static/app/assets/{download-CSRwz923.js → download-C9_946tf.js} +1 -1
- package/static/app/assets/{folder-open-DiMJ7W_7.js → folder-open-B3QKdxib.js} +1 -1
- package/static/app/assets/{hard-drive-iyLPAPdp.js → hard-drive-BwW9Hx4r.js} +1 -1
- package/static/app/assets/{index-_ldE9TAX.js → index-BrUQ-bF8.js} +3 -3
- package/static/app/assets/index-CYb18LtP.css +2 -0
- package/static/app/assets/{input-e_r2hFAY.js → input-Db2Hw2VH.js} +1 -1
- package/static/app/assets/{permissionCopy-9uOHktz3.js → permissionCopy-BpbIHRio.js} +1 -1
- package/static/app/assets/primitives-4B_Ynf7r.js +1 -0
- package/static/app/assets/search-YH-7-x5Y.js +1 -0
- package/static/app/assets/{share-2-DLDJJKmI.js → share-2-BgvRTDPJ.js} +1 -1
- package/static/app/assets/{shield-alert-BTtE-2EZ.js → shield-alert-DWFLyykG.js} +1 -1
- package/static/app/assets/{textarea-Bbzkcgqe.js → textarea-Bk4vEqzT.js} +1 -1
- package/static/app/assets/{useFocusTrap-DJzQd-Za.js → useFocusTrap-CGRXEg_p.js} +1 -1
- package/static/app/assets/{useQuery-6R_tqHRe.js → useQuery-CdPzp3_U.js} +1 -1
- package/static/app/assets/{utils-XDsjcrhS.js → utils-orU8igel.js} +3 -3
- package/static/app/assets/{workspace-CwrG8LHq.js → workspace-Brj2Ylb2.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-BaH5MDce.js +0 -2
- package/static/app/assets/BrainSignals-aDt-nSsW.js +0 -1
- package/static/app/assets/Capture-DDv940Bv.js +0 -1
- package/static/app/assets/Library-BkShbqmN.js +0 -1
- package/static/app/assets/LivingBrain-CXb8094H.js +0 -1
- package/static/app/assets/ProductFlow-BaiqFWLO.js +0 -1
- package/static/app/assets/System-DCstbTxC.js +0 -1
- package/static/app/assets/arrow-left-DC9Oquhs.js +0 -1
- package/static/app/assets/brain-BmWa1wea.js +0 -1
- package/static/app/assets/index-BZRvGOM2.css +0 -2
- package/static/app/assets/primitives-CnEAOmg2.js +0 -1
- package/static/app/assets/search-veep4n6f.js +0 -1
|
@@ -155,6 +155,228 @@ MESSAGES: Dict[str, Dict[str, str]] = {
|
|
|
155
155
|
"ko": "저장할 내용이 없습니다. 텍스트나 페이지 내용을 함께 보내주세요.",
|
|
156
156
|
"en": "Nothing to capture — send text, html, or a selection.",
|
|
157
157
|
},
|
|
158
|
+
# ── shared ──────────────────────────────────────────────────────────
|
|
159
|
+
"common.user_mismatch": {
|
|
160
|
+
"ko": "로그인한 사용자와 요청한 사용자가 다릅니다.",
|
|
161
|
+
"en": "user_email must match the authenticated user.",
|
|
162
|
+
},
|
|
163
|
+
"common.workspace_mismatch": {
|
|
164
|
+
"ko": "작업 공간 지정이 서로 다릅니다.",
|
|
165
|
+
"en": "Workspace selectors must match.",
|
|
166
|
+
},
|
|
167
|
+
"common.workspace_unreadable": {
|
|
168
|
+
"ko": "'{workspace}' 작업 공간을 읽을 권한이 없습니다.",
|
|
169
|
+
"en": "Workspace '{workspace}' is not readable.",
|
|
170
|
+
},
|
|
171
|
+
"common.file_not_found": {
|
|
172
|
+
"ko": "파일을 찾을 수 없습니다.",
|
|
173
|
+
"en": "File not found.",
|
|
174
|
+
},
|
|
175
|
+
"common.path_required": {
|
|
176
|
+
"ko": "경로를 입력해주세요.",
|
|
177
|
+
"en": "A path is required.",
|
|
178
|
+
},
|
|
179
|
+
"common.graph_disabled": {
|
|
180
|
+
"ko": "지식 그래프가 꺼져 있습니다.",
|
|
181
|
+
"en": "The Knowledge Graph is turned off.",
|
|
182
|
+
},
|
|
183
|
+
"common.graph_unavailable": {
|
|
184
|
+
"ko": "지식 그래프를 사용할 수 없습니다.",
|
|
185
|
+
"en": "The Knowledge Graph is not available.",
|
|
186
|
+
},
|
|
187
|
+
# ── chat ────────────────────────────────────────────────────────────
|
|
188
|
+
"chat.model_not_loaded": {
|
|
189
|
+
"ko": "'{model}' 모델이 아직 준비되지 않았습니다.",
|
|
190
|
+
"en": "Model '{model}' is not loaded.",
|
|
191
|
+
},
|
|
192
|
+
"chat.no_model_loaded": {
|
|
193
|
+
"ko": "불러온 모델이 없습니다. 모델을 먼저 준비해주세요.",
|
|
194
|
+
"en": "No model is loaded. Prepare a model first.",
|
|
195
|
+
},
|
|
196
|
+
"chat.conversation_not_found": {
|
|
197
|
+
"ko": "대화를 찾을 수 없습니다.",
|
|
198
|
+
"en": "That conversation no longer exists.",
|
|
199
|
+
},
|
|
200
|
+
"chat.file_name_collision": {
|
|
201
|
+
"ko": "'{name}' 이름의 파일이 너무 많습니다. 다른 이름을 지정해 주세요.",
|
|
202
|
+
"en": "Too many files are already named '{name}'. Please choose another name.",
|
|
203
|
+
},
|
|
204
|
+
"chat.file_generation_failed": {
|
|
205
|
+
"ko": "파일 내용을 만들지 못했습니다.",
|
|
206
|
+
"en": "The file content could not be generated.",
|
|
207
|
+
},
|
|
208
|
+
# ── memory / graph ──────────────────────────────────────────────────
|
|
209
|
+
"memory.unknown_source": {
|
|
210
|
+
"ko": "'{source}'는 알 수 없는 기억 출처입니다.",
|
|
211
|
+
"en": "Unknown memory source: {source}.",
|
|
212
|
+
},
|
|
213
|
+
"graph.node_id_required": {
|
|
214
|
+
"ko": "노드 식별자가 필요합니다.",
|
|
215
|
+
"en": "A node id is required.",
|
|
216
|
+
},
|
|
217
|
+
"graph.node_not_found": {
|
|
218
|
+
"ko": "노드를 찾을 수 없습니다.",
|
|
219
|
+
"en": "That node was not found.",
|
|
220
|
+
},
|
|
221
|
+
"graph.unsupported_type": {
|
|
222
|
+
"ko": "지원하는 종류: message, ai_response, note",
|
|
223
|
+
"en": "Supported types are: message, ai_response, note.",
|
|
224
|
+
},
|
|
225
|
+
# ── ingestion / folders ─────────────────────────────────────────────
|
|
226
|
+
"ingestion.job_not_found": {
|
|
227
|
+
"ko": "수집 작업을 찾을 수 없습니다.",
|
|
228
|
+
"en": "That ingestion job was not found.",
|
|
229
|
+
},
|
|
230
|
+
"ingestion.watch_unavailable": {
|
|
231
|
+
"ko": "폴더 감시 기능을 사용할 수 없습니다.",
|
|
232
|
+
"en": "The folder watch service is unavailable.",
|
|
233
|
+
},
|
|
234
|
+
"ingestion.watch_enable_failed": {
|
|
235
|
+
"ko": "폴더 감시를 켜지 못했습니다.",
|
|
236
|
+
"en": "Turning folder watch on did not succeed.",
|
|
237
|
+
},
|
|
238
|
+
"ingestion.watch_selector_required": {
|
|
239
|
+
"ko": "감시 항목 또는 경로가 필요합니다.",
|
|
240
|
+
"en": "A watch id or a path is required.",
|
|
241
|
+
},
|
|
242
|
+
"ingestion.watch_not_found": {
|
|
243
|
+
"ko": "감시 중인 폴더를 찾을 수 없습니다.",
|
|
244
|
+
"en": "That watched folder was not found.",
|
|
245
|
+
},
|
|
246
|
+
"portability.verification_failed": {
|
|
247
|
+
"ko": "보관 파일 검증에 실패했습니다.",
|
|
248
|
+
"en": "Archive verification failed.",
|
|
249
|
+
},
|
|
250
|
+
# ── review queue ────────────────────────────────────────────────────
|
|
251
|
+
"review.item_not_found": {
|
|
252
|
+
"ko": "검토 항목을 찾을 수 없습니다.",
|
|
253
|
+
"en": "That review item was not found.",
|
|
254
|
+
},
|
|
255
|
+
"review.cannot_approve_in_status": {
|
|
256
|
+
"ko": "'{status}' 상태의 검토 항목은 승인할 수 없습니다.",
|
|
257
|
+
"en": "A review item in status '{status}' cannot be approved.",
|
|
258
|
+
},
|
|
259
|
+
# ── projects ────────────────────────────────────────────────────────
|
|
260
|
+
"project.not_found": {
|
|
261
|
+
"ko": "프로젝트를 찾을 수 없습니다.",
|
|
262
|
+
"en": "That project was not found.",
|
|
263
|
+
},
|
|
264
|
+
# ── network boundary ────────────────────────────────────────────────
|
|
265
|
+
"boundary.policy_not_configured": {
|
|
266
|
+
"ko": "혼합 검색 정책 서비스가 설정되지 않았습니다.",
|
|
267
|
+
"en": "The hybrid policy service is not configured.",
|
|
268
|
+
},
|
|
269
|
+
# ── models ──────────────────────────────────────────────────────────
|
|
270
|
+
"models.other_user_credentials": {
|
|
271
|
+
"ko": "다른 사용자의 모델 자격 증명을 사용할 수 없습니다.",
|
|
272
|
+
"en": "You cannot use another user's model credentials.",
|
|
273
|
+
},
|
|
274
|
+
"models.download_consent_required": {
|
|
275
|
+
"ko": "모델 내려받기는 사용자가 직접 동의해야 시작됩니다.",
|
|
276
|
+
"en": "Model downloads require explicit consent (allow_download=true).",
|
|
277
|
+
},
|
|
278
|
+
"models.identifier_empty": {
|
|
279
|
+
"ko": "모델 식별자가 비어 있습니다.",
|
|
280
|
+
"en": "The model identifier is empty.",
|
|
281
|
+
},
|
|
282
|
+
"models.name_empty": {
|
|
283
|
+
"ko": "모델 이름이 비어 있습니다.",
|
|
284
|
+
"en": "The model name is empty.",
|
|
285
|
+
},
|
|
286
|
+
"models.ollama_missing": {
|
|
287
|
+
"ko": "Ollama가 설치되어 있지 않습니다.",
|
|
288
|
+
"en": "Ollama is not installed.",
|
|
289
|
+
},
|
|
290
|
+
"models.download_timeout": {
|
|
291
|
+
"ko": "모델 내려받기 시간이 초과되었습니다.",
|
|
292
|
+
"en": "The model download timed out.",
|
|
293
|
+
},
|
|
294
|
+
"models.pull_failed": {
|
|
295
|
+
"ko": "모델 내려받기에 실패했습니다.",
|
|
296
|
+
"en": "The model download failed.",
|
|
297
|
+
},
|
|
298
|
+
"models.download_not_automated": {
|
|
299
|
+
"ko": "{provider} 엔진의 모델 내려받기는 아직 자동화되지 않았습니다.",
|
|
300
|
+
"en": "Model downloads for the {provider} engine are not automated yet.",
|
|
301
|
+
},
|
|
302
|
+
"models.unknown_provider": {
|
|
303
|
+
"ko": "알 수 없는 제공자입니다.",
|
|
304
|
+
"en": "Unknown provider.",
|
|
305
|
+
},
|
|
306
|
+
"models.api_key_empty": {
|
|
307
|
+
"ko": "API 키가 비어 있습니다.",
|
|
308
|
+
"en": "The API key is empty.",
|
|
309
|
+
},
|
|
310
|
+
"models.other_user_api_key": {
|
|
311
|
+
"ko": "다른 사용자의 API 키를 설정할 권한이 없습니다.",
|
|
312
|
+
"en": "You are not allowed to set another user's API key.",
|
|
313
|
+
},
|
|
314
|
+
"models.sign_in_required": {
|
|
315
|
+
"ko": "사용자 확인이 필요합니다. 로그인 후 다시 시도하세요.",
|
|
316
|
+
"en": "Sign in first, then try again.",
|
|
317
|
+
},
|
|
318
|
+
# ── tools / files ───────────────────────────────────────────────────
|
|
319
|
+
"tools.pdf_render_failed": {
|
|
320
|
+
"ko": "PDF를 그리지 못했습니다: {reason}",
|
|
321
|
+
"en": "The PDF could not be rendered: {reason}",
|
|
322
|
+
},
|
|
323
|
+
"tools.path_outside_workspace": {
|
|
324
|
+
"ko": "경로가 작업 공간 밖입니다.",
|
|
325
|
+
"en": "That path is outside the workspace.",
|
|
326
|
+
},
|
|
327
|
+
"tools.directory_not_found": {
|
|
328
|
+
"ko": "디렉터리를 찾을 수 없습니다.",
|
|
329
|
+
"en": "That directory was not found.",
|
|
330
|
+
},
|
|
331
|
+
# ── MCP / setup ─────────────────────────────────────────────────────
|
|
332
|
+
"mcp.connector_not_found": {
|
|
333
|
+
"ko": "커넥터를 찾을 수 없습니다.",
|
|
334
|
+
"en": "That connector was not found.",
|
|
335
|
+
},
|
|
336
|
+
"mcp.name_required": {
|
|
337
|
+
"ko": "이름은 필수입니다.",
|
|
338
|
+
"en": "A name is required.",
|
|
339
|
+
},
|
|
340
|
+
"mcp.package_required": {
|
|
341
|
+
"ko": "패키지는 필수입니다.",
|
|
342
|
+
"en": "A package is required.",
|
|
343
|
+
},
|
|
344
|
+
"mcp.item_not_found": {
|
|
345
|
+
"ko": "항목을 찾을 수 없습니다.",
|
|
346
|
+
"en": "That item was not found.",
|
|
347
|
+
},
|
|
348
|
+
"mcp.unknown_id": {
|
|
349
|
+
"ko": "알 수 없는 MCP입니다: {mcp_id}",
|
|
350
|
+
"en": "Unknown MCP: {mcp_id}",
|
|
351
|
+
},
|
|
352
|
+
"setup.unknown_permission": {
|
|
353
|
+
"ko": "알 수 없는 권한 설정입니다.",
|
|
354
|
+
"en": "Unknown permission setting.",
|
|
355
|
+
},
|
|
356
|
+
"models.public_mode_blocks_local": {
|
|
357
|
+
"ko": (
|
|
358
|
+
"공개 모드에서는 로컬 MLX 모델을 불러올 수 없습니다. "
|
|
359
|
+
"openai:, openrouter:, groq:, together: 모델을 쓰거나 "
|
|
360
|
+
"LATTICEAI_ALLOW_LOCAL_MODELS=true 로 설정하세요."
|
|
361
|
+
),
|
|
362
|
+
"en": (
|
|
363
|
+
"Public mode does not load local MLX models. Use an openai:, "
|
|
364
|
+
"openrouter:, groq: or together: model, or set "
|
|
365
|
+
"LATTICEAI_ALLOW_LOCAL_MODELS=true."
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
"models.public_model_missing": {
|
|
369
|
+
"ko": (
|
|
370
|
+
"공개 모델이 준비되지 않았습니다. OPENAI_API_KEY 와 "
|
|
371
|
+
"LATTICEAI_PUBLIC_MODEL={model} 을 설정하거나, OpenAI 호환 모델로 "
|
|
372
|
+
"/models/load 를 호출하세요."
|
|
373
|
+
),
|
|
374
|
+
"en": (
|
|
375
|
+
"No public model is loaded. Set OPENAI_API_KEY and "
|
|
376
|
+
"LATTICEAI_PUBLIC_MODEL={model}, or call /models/load with an "
|
|
377
|
+
"OpenAI-compatible model."
|
|
378
|
+
),
|
|
379
|
+
},
|
|
158
380
|
}
|
|
159
381
|
|
|
160
382
|
|
|
@@ -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.10.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
|
|
@@ -157,9 +157,12 @@ async def send_message(client, chat_id, text, reply_markup=None):
|
|
|
157
157
|
async def send_photo(client, chat_id, file_path: Path, caption: str = ""):
|
|
158
158
|
url = f"{API_URL}/sendPhoto"
|
|
159
159
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
# Read on a worker thread: a screenshot is megabytes, and reading it
|
|
161
|
+
# inline stalled the bot's poll loop (and everything else on the loop)
|
|
162
|
+
# for the length of the read.
|
|
163
|
+
blob = await asyncio.to_thread(Path(file_path).read_bytes)
|
|
164
|
+
res = await client.post(url, data={"chat_id": str(chat_id), "caption": caption[:1024]},
|
|
165
|
+
files={"photo": (file_path.name, blob)}, timeout=60.0)
|
|
163
166
|
if res.status_code != 200:
|
|
164
167
|
await send_message(client, chat_id, f"사진 전송 실패 ({res.status_code})")
|
|
165
168
|
except Exception as e:
|
|
@@ -169,15 +172,15 @@ async def send_photo(client, chat_id, file_path: Path, caption: str = ""):
|
|
|
169
172
|
async def send_document(client, chat_id, file_path, caption=None, filename=None):
|
|
170
173
|
url = f"{API_URL}/sendDocument"
|
|
171
174
|
try:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
blob = await asyncio.to_thread(Path(file_path).read_bytes)
|
|
176
|
+
res = await client.post(
|
|
177
|
+
url,
|
|
178
|
+
data={"chat_id": str(chat_id), **({"caption": caption[:1024]} if caption else {})},
|
|
179
|
+
files={"document": (filename or Path(file_path).name, blob)},
|
|
180
|
+
timeout=300.0,
|
|
181
|
+
)
|
|
182
|
+
if res.status_code != 200:
|
|
183
|
+
logger.error("파일 전송 실패 (%s): %s", res.status_code, safe_log_text(res.text))
|
|
181
184
|
except Exception as e:
|
|
182
185
|
logger.error("파일 전송 실패: %s", safe_log_text(e))
|
|
183
186
|
|
|
@@ -584,12 +587,11 @@ async def process_document_file(client, chat_id, file_id: str, filename: str, ca
|
|
|
584
587
|
try:
|
|
585
588
|
tmp.write_bytes(raw)
|
|
586
589
|
async with _server_client() as lc:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
)
|
|
590
|
+
res = await lc.post(
|
|
591
|
+
UPLOAD_DOC_URL,
|
|
592
|
+
files={"file": (filename, raw)},
|
|
593
|
+
timeout=60.0,
|
|
594
|
+
)
|
|
593
595
|
if res.status_code == 200:
|
|
594
596
|
data = res.json()
|
|
595
597
|
chars = data.get("chars") or len(raw)
|
|
@@ -78,36 +78,22 @@ def _get_model_runtime_deps(runtime_state: Any):
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
def _prepare_model_sources(
|
|
82
|
+
deps: Any,
|
|
83
|
+
parsed_provider: str,
|
|
84
|
+
parsed_model: str,
|
|
82
85
|
model_id: str,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
adapter_path: Optional[str] = None,
|
|
87
|
-
draft_model_id: Optional[str] = None,
|
|
88
|
-
allow_download: bool = False,
|
|
89
|
-
*,
|
|
90
|
-
runtime_state: Any,
|
|
91
|
-
) -> Dict[str, object]:
|
|
92
|
-
deps = _get_model_runtime_deps(runtime_state)
|
|
93
|
-
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
94
|
-
if not model_id:
|
|
95
|
-
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
96
|
-
|
|
97
|
-
resolution = deps["_ModelResolution"].from_request(
|
|
98
|
-
model_id,
|
|
99
|
-
engine=engine,
|
|
100
|
-
user_email=user_email or deps["get_current_user"](request),
|
|
101
|
-
engine_aliases=deps["MODEL_ENGINE_ALIASES"],
|
|
102
|
-
)
|
|
86
|
+
allow_download: bool,
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
"""Make sure the engine and weights this model needs exist locally.
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
108
|
-
if compatibility.get("supported") is False:
|
|
109
|
-
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
90
|
+
Every branch here shells out, downloads, or waits on a child server, so this
|
|
91
|
+
is deliberately a plain sync function: callers run it on a worker thread
|
|
92
|
+
(see :func:`prepare_and_load_model`) rather than on the event loop.
|
|
110
93
|
|
|
94
|
+
Returns the four values the caller needs back, including the two the
|
|
95
|
+
LM Studio branch rewrites (``parsed_model`` and ``model_id``).
|
|
96
|
+
"""
|
|
111
97
|
local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
|
|
112
98
|
install_result: Dict[str, object] = {}
|
|
113
99
|
download_result: Optional[Dict[str, object]] = None
|
|
@@ -169,6 +155,56 @@ async def prepare_and_load_model(
|
|
|
169
155
|
model_id = f"lmstudio:{resolved_model}"
|
|
170
156
|
download_result = ensured
|
|
171
157
|
|
|
158
|
+
return {
|
|
159
|
+
"install_result": install_result,
|
|
160
|
+
"download_result": download_result,
|
|
161
|
+
"parsed_model": parsed_model,
|
|
162
|
+
"model_id": model_id,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def prepare_and_load_model(
|
|
167
|
+
model_id: str,
|
|
168
|
+
request: Any,
|
|
169
|
+
engine: Optional[str] = None,
|
|
170
|
+
user_email: Optional[str] = None,
|
|
171
|
+
adapter_path: Optional[str] = None,
|
|
172
|
+
draft_model_id: Optional[str] = None,
|
|
173
|
+
allow_download: bool = False,
|
|
174
|
+
*,
|
|
175
|
+
runtime_state: Any,
|
|
176
|
+
) -> Dict[str, object]:
|
|
177
|
+
deps = _get_model_runtime_deps(runtime_state)
|
|
178
|
+
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
179
|
+
if not model_id:
|
|
180
|
+
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
181
|
+
|
|
182
|
+
resolution = deps["_ModelResolution"].from_request(
|
|
183
|
+
model_id,
|
|
184
|
+
engine=engine,
|
|
185
|
+
user_email=user_email or deps["get_current_user"](request),
|
|
186
|
+
engine_aliases=deps["MODEL_ENGINE_ALIASES"],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
parsed_provider, parsed_model = deps["parse_model_ref"](model_id)
|
|
190
|
+
if parsed_provider == "mlx":
|
|
191
|
+
parsed_provider = "local_mlx"
|
|
192
|
+
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
193
|
+
if compatibility.get("supported") is False:
|
|
194
|
+
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
195
|
+
|
|
196
|
+
# Engine install, model download and local-server startup are all blocking
|
|
197
|
+
# and all measured in minutes. Run on the event loop they froze the whole
|
|
198
|
+
# server for the duration — the UI could not even poll /health to find out
|
|
199
|
+
# why. One hop to a worker thread; the async shape above is unchanged.
|
|
200
|
+
sources = await asyncio.to_thread(
|
|
201
|
+
_prepare_model_sources, deps, parsed_provider, parsed_model, model_id, allow_download
|
|
202
|
+
)
|
|
203
|
+
install_result = sources["install_result"]
|
|
204
|
+
download_result = sources["download_result"]
|
|
205
|
+
parsed_model = sources["parsed_model"]
|
|
206
|
+
model_id = sources["model_id"]
|
|
207
|
+
|
|
172
208
|
effective_email = (user_email or deps["get_current_user"](request) or "").strip()
|
|
173
209
|
user_api_key = deps["get_user_api_key"](effective_email, parsed_provider) if parsed_provider != "local_mlx" else None
|
|
174
210
|
msg = await deps["router"].load_model(
|
|
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
|
|
|
18
18
|
|
|
19
19
|
from latticeai.services.architecture_readiness import architecture_readiness
|
|
20
20
|
|
|
21
|
-
PRODUCT_VERSION_TARGET = "10.
|
|
21
|
+
PRODUCT_VERSION_TARGET = "10.10.0"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass(frozen=True)
|
|
@@ -37,8 +37,10 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
37
37
|
evidence=[
|
|
38
38
|
"docs/ONBOARDING.md::five-minute",
|
|
39
39
|
"frontend/src/components/ProductFlow.tsx::WakeBrainScreen",
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# 10.10.0: the insight panels left the home shelf for the dock —
|
|
41
|
+
# a rail (대화 · 통계 · 기억 지도) whose drawer hosts the brief.
|
|
42
|
+
"frontend/src/features/brain/BrainConversation.tsx::BrainHomeDock",
|
|
43
|
+
"frontend/src/features/brain/BrainHomeDock.tsx::BrainBriefPanel",
|
|
42
44
|
"frontend/src/features/brain/BrainHome.tsx",
|
|
43
45
|
"frontend/src/App.tsx::brain-mobile-nav",
|
|
44
46
|
"latticeai/setup/auto_setup.py",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.10.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"build:assets": "vite build && node scripts/build_frontend_assets.mjs",
|
|
25
25
|
"build:python": "node scripts/run_python.mjs -m build",
|
|
26
26
|
"check:python": "node scripts/run_python.mjs scripts/check_python.py",
|
|
27
|
-
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs",
|
|
27
|
+
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run check:server-i18n && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs",
|
|
28
28
|
"check:release-evidence-bound": "node scripts/check_release_evidence_bound.mjs",
|
|
29
29
|
"check:legacy-debt": "node scripts/check_legacy_debt.mjs",
|
|
30
|
+
"check:server-i18n": "node scripts/check_server_i18n.mjs",
|
|
30
31
|
"lint:python": "node scripts/run_python.mjs -m ruff check . && npm run typecheck:python",
|
|
31
32
|
"lint:frontend": "node scripts/lint_frontend.mjs",
|
|
32
33
|
"check:bundle": "node scripts/check_bundle_budget.mjs",
|
|
@@ -6,7 +6,7 @@ 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 = "Quiet Station";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
@@ -96,7 +96,9 @@ requireIncludes("RELEASE.md", `## v${version} — ${releaseTheme}`);
|
|
|
96
96
|
requireIncludes("docs/CHANGELOG.md", `## [${version}]`);
|
|
97
97
|
requireIncludes("RELEASE_NOTES.md", `[v${version} - ${releaseTheme}]`);
|
|
98
98
|
requireIncludes("CHANGELOG.md", "starts at v8.0.0");
|
|
99
|
-
|
|
99
|
+
// 10.10.0 moved the public history floor from 8.0.0 to 9.0.0 — the 8.x notes
|
|
100
|
+
// left the tree, so this gate follows the boundary the docs now state.
|
|
101
|
+
requireIncludes("RELEASE_NOTES.md", `9.0.0 through ${version}`);
|
|
100
102
|
requireIncludes("SECURITY.md", `${version.split(".").slice(0, 2).join(".")}.x (latest)`);
|
|
101
103
|
|
|
102
104
|
for (const rel of ["README.md", "RELEASE.md", "docs/CHANGELOG.md", "RELEASE_NOTES.md"]) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Server-side i18n ratchet.
|
|
4
|
+
*
|
|
5
|
+
* Every string the API hands a person used to be a literal at the raise site,
|
|
6
|
+
* written in whichever language the author happened to be thinking in. One user
|
|
7
|
+
* could get `"사용자를 찾을 수 없습니다."` from one router and, two files over,
|
|
8
|
+
* `"Knowledge Graph ingestion is disabled."` — half the product in a language
|
|
9
|
+
* they never chose.
|
|
10
|
+
*
|
|
11
|
+
* `latticeai/core/messages.py` fixed the mechanism; this gate keeps the fix from
|
|
12
|
+
* eroding. Routers listed in LOCALIZED below are done: they must raise through
|
|
13
|
+
* `http_error(...)` / `translate(...)`, never a literal. Routers not yet listed
|
|
14
|
+
* are honestly *not* claimed to be localized — adding one to the list is how a
|
|
15
|
+
* migration is declared finished, and from then on it cannot slip back.
|
|
16
|
+
*
|
|
17
|
+
* Pass-throughs (`detail=str(exc)`, `detail=exc.to_detail()`, a variable) are
|
|
18
|
+
* allowed: the wording belongs to the service that raised, and moving it here
|
|
19
|
+
* would only relocate the problem.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
const API_DIR = "latticeai/api";
|
|
25
|
+
|
|
26
|
+
// Routers whose user-facing details are in the message catalog. Append here
|
|
27
|
+
// when a router is migrated — never remove. Module names carry no extension:
|
|
28
|
+
// `check_legacy_debt.mjs` rejects a bare "<module>.py" literal in scripts/,
|
|
29
|
+
// which is how the removed root shims used to be referenced.
|
|
30
|
+
const LOCALIZED = [
|
|
31
|
+
"admin",
|
|
32
|
+
"auth",
|
|
33
|
+
"browser",
|
|
34
|
+
"chat",
|
|
35
|
+
"chat_history",
|
|
36
|
+
"chat_intents",
|
|
37
|
+
"knowledge_graph",
|
|
38
|
+
"local_files",
|
|
39
|
+
"mcp",
|
|
40
|
+
"memory",
|
|
41
|
+
"models",
|
|
42
|
+
"network_boundary",
|
|
43
|
+
"portability",
|
|
44
|
+
"project_sessions",
|
|
45
|
+
"review_queue",
|
|
46
|
+
"setup",
|
|
47
|
+
"tools",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const failures = [];
|
|
51
|
+
const known = new Set(readdirSync(API_DIR).filter((name) => name.endsWith(".py")));
|
|
52
|
+
|
|
53
|
+
for (const name of LOCALIZED) {
|
|
54
|
+
const file = `${name}.py`;
|
|
55
|
+
if (!known.has(file)) {
|
|
56
|
+
failures.push(`${file}: listed as localized but no such router exists`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const lines = readFileSync(join(API_DIR, file), "utf8").split("\n");
|
|
60
|
+
lines.forEach((line, index) => {
|
|
61
|
+
const match = line.match(/detail\s*=\s*(.+)$/);
|
|
62
|
+
if (!match) return;
|
|
63
|
+
const value = match[1].trim();
|
|
64
|
+
// Anything that is not a quoted literal is a pass-through: `str(exc)`, a
|
|
65
|
+
// variable, `translate(...)`, a dict. Those carry no wording of their own.
|
|
66
|
+
if (!/^f?"/.test(value)) return;
|
|
67
|
+
// A literal used as an argument to a string method — `"; ".join(errors)` —
|
|
68
|
+
// is a separator, not a message.
|
|
69
|
+
if (/^f?"[^"]*"\s*\./.test(value)) return;
|
|
70
|
+
// An empty string carries no wording either.
|
|
71
|
+
if (/^f?"\s*"/.test(value)) return;
|
|
72
|
+
failures.push(
|
|
73
|
+
`${file}:${index + 1}: literal error detail — use http_error("<key>", …) `
|
|
74
|
+
+ `with a key in latticeai/core/messages.py\n ${line.trim()}`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (failures.length) {
|
|
80
|
+
console.error("Server i18n gate failed:\n");
|
|
81
|
+
for (const failure of failures) console.error(" " + failure);
|
|
82
|
+
console.error(
|
|
83
|
+
`\n${failures.length} literal detail(s) in routers declared localized.`
|
|
84
|
+
+ "\nAdd the message to latticeai/core/messages.py (both ko and en) and raise"
|
|
85
|
+
+ "\nvia http_error(status, key, resolve_language(request)).",
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remaining = [...known].filter(
|
|
91
|
+
(entry) => entry !== "__init__.py" && !LOCALIZED.includes(entry.replace(/\.py$/, "")),
|
|
92
|
+
).length;
|
|
93
|
+
console.log(
|
|
94
|
+
`Server i18n gate: ${LOCALIZED.length} router(s) localized and locked; `
|
|
95
|
+
+ `${remaining} not yet claimed.`,
|
|
96
|
+
);
|
|
@@ -22,16 +22,39 @@
|
|
|
22
22
|
],
|
|
23
23
|
|
|
24
24
|
"10.8.0": {
|
|
25
|
-
"01-login.png": {},
|
|
26
|
-
"02-recommended-models.png": {},
|
|
27
|
-
"03-install-load-progress.png": {},
|
|
28
|
-
"05-memory-graph.png": {
|
|
29
|
-
"_why": "The provenance meter in this screen's header wrapped into a ragged three-line block that ran into the page title. It is a bordered column with a straight left edge now."
|
|
30
|
-
},
|
|
31
25
|
"04-brain-chat-home.png": {
|
|
32
|
-
"_why": "
|
|
33
|
-
"region": [0.0, 0.45, 1.0, 0.85],
|
|
26
|
+
"_why": "Elevated brain-chat-home visual layout with glassmorphic station card, ambient radial mesh aura, gradient title, glowing dot prompt cards, and refined toolbar styling.",
|
|
34
27
|
"min_pct": 1.5
|
|
35
28
|
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"10.10.0": {
|
|
32
|
+
"04-brain-chat-home.png": {
|
|
33
|
+
"_why": [
|
|
34
|
+
"The quiet-station redesign: the memory stats left the hero sentence",
|
|
35
|
+
"for a hover badge, the six capture chips folded behind the composer's",
|
|
36
|
+
"+ control, the model banner became a status pill on the hero's right,",
|
|
37
|
+
"the two bottom shelves became a dock rail (대화 · 통계 · 기억 지도),",
|
|
38
|
+
"cards dropped their borders for soft shadows, and the accent colour",
|
|
39
|
+
"was pulled back to the send CTA and the model action."
|
|
40
|
+
],
|
|
41
|
+
"min_pct": 3.0
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"10.9.0": {
|
|
46
|
+
"_why": [
|
|
47
|
+
"This release claims no screen redesign, and the empty claim is the",
|
|
48
|
+
"honest statement of that: 10.9.0 is a correctness release (event-loop",
|
|
49
|
+
"blocking, a focus ring that faded in too slowly to see, an organism",
|
|
50
|
+
"stuck in 'thinking', localized server errors).",
|
|
51
|
+
"",
|
|
52
|
+
"Its two visual changes are both invisible at the capture viewport by",
|
|
53
|
+
"construction. The welcome step's compaction lives under",
|
|
54
|
+
"`@media (max-height: 820px)` and captures run at 1440x900; the capture",
|
|
55
|
+
"pills' focus border only differs while an input inside them has focus,",
|
|
56
|
+
"which no screenshot holds. `tests/visual/v3.spec.js` asserts both",
|
|
57
|
+
"directly instead — a pixel diff is the wrong instrument for either."
|
|
58
|
+
]
|
|
36
59
|
}
|
|
37
60
|
}
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED