ltcai 11.1.0 → 11.2.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 +53 -53
- package/docs/CHANGELOG.md +33 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -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/architecture.md +6 -2
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection.py +66 -8
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion.py +175 -27
- package/lattice_brain/multimodal.py +525 -5
- package/lattice_brain/portability.py +169 -32
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent.py +55 -2
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +143 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/runtime/build_phases.py +28 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence.py +123 -7
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime.py +1 -1
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard.py +77 -44
- package/package.json +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/release_screen_claims.json +13 -0
- package/scripts/verify_hf_model_registry.py +253 -218
- 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-D0HWqtn0.js → Act-AWf0SAKp.js} +1 -1
- package/static/app/assets/{AdminConsole-D-QDW-A4.js → AdminConsole-D0u8Tiyj.js} +1 -1
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-tuhI4sOC.js} +1 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +2 -0
- package/static/app/assets/{BrainSignals-2dHQNkns.js → BrainSignals-jMYgQ2Ar.js} +1 -1
- package/static/app/assets/{Capture-CT8v1StE.js → Capture-CqOSzyPr.js} +1 -1
- package/static/app/assets/{CommandPalette-DoLXC2KH.js → CommandPalette-DC0Bzh-I.js} +1 -1
- package/static/app/assets/{Library-DDoxFE5c.js → Library-CX-bbhmK.js} +1 -1
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-DBwhto14.js} +1 -1
- package/static/app/assets/{ProductFlow-DOYf7JIs.js → ProductFlow-BHA2cfKI.js} +1 -1
- package/static/app/assets/{ReviewCard-COQsqidK.js → ReviewCard-BUhCKRNM.js} +1 -1
- package/static/app/assets/{System-BRllvYXd.js → System-Bu2t5hn1.js} +1 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-Cia42c2h.js} +1 -1
- package/static/app/assets/brain-DJMoqrwx.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-2j2Ijzgq.js} +1 -1
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-BEFeWpVW.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-ujXMcHxl.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-k4awryFq.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-DFbLJ_ig.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-7y_b6xkM.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-Bidh02Kr.js} +1 -1
- package/static/app/assets/{index-0AvoEBzJ.js → index-BpYkzcVm.js} +3 -3
- package/static/app/assets/{index-vtEfYvQY.css → index-DwDl9-8Y.css} +1 -1
- package/static/app/assets/{input-B_5ZJ9oy.js → input-DSlJJxRs.js} +1 -1
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-Bpb83Hx9.js} +1 -1
- package/static/app/assets/{primitives-CVwew78r.js → primitives-BCx6TvfG.js} +1 -1
- package/static/app/assets/search-Cgy8cCFJ.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-BH1M-WNi.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-BlKdBXcG.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-CCWbUfFB.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-YdHQ7pJ1.js} +1 -1
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-CXQiwbVT.js} +1 -1
- package/static/app/assets/{utils-DcDMoZIe.js → utils-zqPZJxdx.js} +2 -2
- package/static/app/assets/{workspace-LtRRSKTf.js → workspace-DXTihhfU.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
package/latticeai/core/config.py
CHANGED
|
@@ -172,7 +172,10 @@ class Config:
|
|
|
172
172
|
trusted_proxies = [item.strip() for item in _value(env, "LATTICEAI_TRUSTED_PROXIES", "").split(",") if item.strip()]
|
|
173
173
|
|
|
174
174
|
public_model = _value(env, "LATTICEAI_PUBLIC_MODEL", _value(env, "LATTICEAI_DEFAULT_MODEL", "openai:gpt-4o-mini"))
|
|
175
|
-
|
|
175
|
+
# Canonical HF casing (the Hub answers `gemma-4-12b-it-4bit` with
|
|
176
|
+
# `gemma-4-12B-it-4bit`); keeping it identical everywhere means the
|
|
177
|
+
# download path, the on-disk cache dir and the catalog key all agree.
|
|
178
|
+
local_model = _value(env, "LATTICEAI_LOCAL_MODEL", "mlx-community/gemma-4-12B-it-4bit")
|
|
176
179
|
|
|
177
180
|
data_dir = Path(_value(env, "LATTICEAI_DATA_DIR", str(Path.home() / ".ltcai")))
|
|
178
181
|
static_dir = Path(_value(env, "LATTICEAI_STATIC_DIR", str(base_dir / "static")))
|
|
@@ -26,7 +26,7 @@ import re
|
|
|
26
26
|
from typing import Any, Dict, List
|
|
27
27
|
|
|
28
28
|
from lattice_brain.context import approx_tokens
|
|
29
|
-
from lattice_brain.graph.retrieval import context_quality_signal
|
|
29
|
+
from lattice_brain.graph.retrieval import context_quality_signal, multimodal_signal
|
|
30
30
|
from lattice_brain.self_model import DEFAULT_SUMMARY_TOKENS, summary_for_prompt
|
|
31
31
|
|
|
32
32
|
_CLEAN_RE = re.compile(r"\s+")
|
|
@@ -250,8 +250,11 @@ def retrieve_context_for_generation(
|
|
|
250
250
|
"graph_edges": len(hop_data.get("edges", [])),
|
|
251
251
|
"budget_trimmed": trimmed,
|
|
252
252
|
},
|
|
253
|
-
# Same honest signal chat reports for the same Brain
|
|
254
|
-
|
|
253
|
+
# Same honest signal chat reports for the same Brain — including the
|
|
254
|
+
# multimodal key when the document's context really rests on pictures.
|
|
255
|
+
"context_quality": context_quality_signal(
|
|
256
|
+
"hybrid", len(results), multimodal=multimodal_signal(results)
|
|
257
|
+
),
|
|
255
258
|
"trace": {
|
|
256
259
|
"budget_approx_tokens": budget,
|
|
257
260
|
"used_approx_tokens": approx_tokens(assembled),
|
|
@@ -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 = "11.
|
|
13
|
+
MARKETPLACE_VERSION = "11.2.0"
|
|
14
14
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -307,6 +307,31 @@ MESSAGES: Dict[str, Dict[str, str]] = {
|
|
|
307
307
|
"ko": "'{status}' 상태의 검토 항목은 승인할 수 없습니다.",
|
|
308
308
|
"en": "A review item in status '{status}' cannot be approved.",
|
|
309
309
|
},
|
|
310
|
+
"review.bulk_ids_required": {
|
|
311
|
+
"ko": "한꺼번에 처리할 검토 항목을 하나 이상 골라 주세요.",
|
|
312
|
+
"en": "Choose at least one review item to act on.",
|
|
313
|
+
},
|
|
314
|
+
"review.bulk_too_many": {
|
|
315
|
+
"ko": "한 번에 최대 {cap}개까지 처리할 수 있습니다.",
|
|
316
|
+
"en": "At most {cap} items can be handled in one request.",
|
|
317
|
+
},
|
|
318
|
+
# ── interop bridges (Notion export / git / mail / calendar) ──────────
|
|
319
|
+
"ingestion.interop_path_required": {
|
|
320
|
+
"ko": "불러올 파일이나 폴더 경로가 필요합니다.",
|
|
321
|
+
"en": "A file or folder path to read is required.",
|
|
322
|
+
},
|
|
323
|
+
"ingestion.interop_unknown_source": {
|
|
324
|
+
"ko": "알 수 없는 연동 종류입니다: {source}",
|
|
325
|
+
"en": "Unknown interop source: {source}",
|
|
326
|
+
},
|
|
327
|
+
"ingestion.vault_watch_unavailable": {
|
|
328
|
+
"ko": "보관함 자동 확인 기능을 사용할 수 없습니다.",
|
|
329
|
+
"en": "Vault watch is unavailable.",
|
|
330
|
+
},
|
|
331
|
+
"ingestion.vault_watch_disabled": {
|
|
332
|
+
"ko": "보관함 자동 확인은 기본으로 꺼져 있습니다. 설정에서 켜야 사용할 수 있습니다.",
|
|
333
|
+
"en": "Vault watch is off by default; turn it on in settings to use it.",
|
|
334
|
+
},
|
|
310
335
|
# ── projects ────────────────────────────────────────────────────────
|
|
311
336
|
"project.not_found": {
|
|
312
337
|
"ko": "프로젝트를 찾을 수 없습니다.",
|
|
@@ -317,6 +342,124 @@ MESSAGES: Dict[str, Dict[str, str]] = {
|
|
|
317
342
|
"ko": "혼합 검색 정책 서비스가 설정되지 않았습니다.",
|
|
318
343
|
"en": "The hybrid policy service is not configured.",
|
|
319
344
|
},
|
|
345
|
+
# ── feature toggles ─────────────────────────────────────────────────
|
|
346
|
+
# The switchboard renders from the server (see
|
|
347
|
+
# ``latticeai/services/feature_toggles.py``), so every label and one-line
|
|
348
|
+
# explanation a person reads lives here. The rule for the summaries: say
|
|
349
|
+
# what turning it *on* does, in the words someone who never read the
|
|
350
|
+
# environment-variable docs would use.
|
|
351
|
+
"features.note": {
|
|
352
|
+
"ko": "모두 지금 바로 적용됩니다. 다시 시작하지 않아도 됩니다.",
|
|
353
|
+
"en": "Every switch here takes effect right away — no restart needed.",
|
|
354
|
+
},
|
|
355
|
+
"features.unknown": {
|
|
356
|
+
"ko": "그런 기능은 없습니다: {feature}",
|
|
357
|
+
"en": "There is no such feature: {feature}",
|
|
358
|
+
},
|
|
359
|
+
"features.invalid_value": {
|
|
360
|
+
"ko": "이 기능에 쓸 수 없는 값입니다: {value}",
|
|
361
|
+
"en": "That is not a value this feature can take: {value}",
|
|
362
|
+
},
|
|
363
|
+
"features.choice.install_required": {
|
|
364
|
+
"ko": "설치 필요 — {reason}",
|
|
365
|
+
"en": "Install required — {reason}",
|
|
366
|
+
},
|
|
367
|
+
"features.allow_multimodal.label": {
|
|
368
|
+
"ko": "사진·녹음도 기억하기",
|
|
369
|
+
"en": "Remember pictures and recordings",
|
|
370
|
+
},
|
|
371
|
+
"features.allow_multimodal.summary": {
|
|
372
|
+
"ko": "폴더를 읽을 때 글뿐 아니라 사진과 녹음도 함께 저장합니다.",
|
|
373
|
+
"en": "A folder scan stores pictures and recordings too, not just text.",
|
|
374
|
+
},
|
|
375
|
+
"features.video_ingest.label": {
|
|
376
|
+
"ko": "영상도 함께",
|
|
377
|
+
"en": "Include videos",
|
|
378
|
+
},
|
|
379
|
+
"features.video_ingest.summary": {
|
|
380
|
+
"ko": "사진·녹음을 켠 상태에서, 영상은 장면과 자막으로 저장합니다.",
|
|
381
|
+
"en": "With the switch above on, videos are stored as keyframes and subtitles.",
|
|
382
|
+
},
|
|
383
|
+
"features.vault_watch.label": {
|
|
384
|
+
"ko": "노트 보관함 지켜보기",
|
|
385
|
+
"en": "Watch my notes vault",
|
|
386
|
+
},
|
|
387
|
+
"features.vault_watch.summary": {
|
|
388
|
+
"ko": "밖에 있는 노트 보관함이 바뀌면 알아서 다시 읽어옵니다.",
|
|
389
|
+
"en": "When an outside notes vault changes, it is re-read on its own.",
|
|
390
|
+
},
|
|
391
|
+
"features.brain_network.label": {
|
|
392
|
+
"ko": "골라서 나누기",
|
|
393
|
+
"en": "Share selected knowledge",
|
|
394
|
+
},
|
|
395
|
+
"features.brain_network.summary": {
|
|
396
|
+
"ko": "내가 고른 기억 묶음만 다른 기기로 내보내고 받아올 수 있습니다.",
|
|
397
|
+
"en": "Lets you export a hand-picked slice of memory to another device, and receive one.",
|
|
398
|
+
},
|
|
399
|
+
"features.brain_network.caution": {
|
|
400
|
+
"ko": "이 기능만 기억을 이 컴퓨터 밖으로 내보냅니다. 받은 내용은 바로 합쳐지지 않고 검토함으로 갑니다.",
|
|
401
|
+
"en": "This is the one switch that sends memory off this computer. Anything received waits in the review inbox instead of merging.",
|
|
402
|
+
},
|
|
403
|
+
"features.synthesis.label": {
|
|
404
|
+
"ko": "스스로 정리하기",
|
|
405
|
+
"en": "Tidy up on its own",
|
|
406
|
+
},
|
|
407
|
+
"features.synthesis.summary": {
|
|
408
|
+
"ko": "자료가 쌓이면 알아서 훑어보고, 고칠 거리를 검토함에 제안합니다.",
|
|
409
|
+
"en": "As material piles up, the Brain reviews it and proposes tidy-ups in the review inbox.",
|
|
410
|
+
},
|
|
411
|
+
"features.auto_vector_index.label": {
|
|
412
|
+
"ko": "넣자마자 검색 준비",
|
|
413
|
+
"en": "Make new material searchable at once",
|
|
414
|
+
},
|
|
415
|
+
"features.auto_vector_index.summary": {
|
|
416
|
+
"ko": "새 자료를 넣으면 바로 의미 검색까지 준비합니다. 끄면 나중에 한 번에 만듭니다.",
|
|
417
|
+
"en": "New material is prepared for meaning-based search immediately; off means you rebuild later.",
|
|
418
|
+
},
|
|
419
|
+
"features.auto_late_fusion.label": {
|
|
420
|
+
"ko": "글로 사진 찾기",
|
|
421
|
+
"en": "Find pictures by typing",
|
|
422
|
+
},
|
|
423
|
+
"features.auto_late_fusion.summary": {
|
|
424
|
+
"ko": "글로 물어봐도 사진까지 함께 찾습니다. 사진을 읽는 모델이 있어야 켜집니다.",
|
|
425
|
+
"en": "A typed question also searches pictures — needs a vision model that shares the same space.",
|
|
426
|
+
},
|
|
427
|
+
"features.fusion_rrf.label": {
|
|
428
|
+
"ko": "검색 결과 합치는 방식 바꾸기",
|
|
429
|
+
"en": "Blend search results by rank",
|
|
430
|
+
},
|
|
431
|
+
"features.fusion_rrf.summary": {
|
|
432
|
+
"ko": "점수 대신 순위로 합칩니다. 검색 채널마다 점수 크기가 달라도 흔들리지 않습니다.",
|
|
433
|
+
"en": "Combines channels by position instead of score, so mismatched score scales stop skewing results.",
|
|
434
|
+
},
|
|
435
|
+
"features.graph_expansion.label": {
|
|
436
|
+
"ko": "옆에 있는 기억까지 보기",
|
|
437
|
+
"en": "Look at neighbouring memories",
|
|
438
|
+
},
|
|
439
|
+
"features.graph_expansion.summary": {
|
|
440
|
+
"ko": "찾은 기억과 바로 이어진 기억도 후보로 넣습니다. 답은 넓어지고 조금 흐려집니다.",
|
|
441
|
+
"en": "Adds memories one link away from a hit as candidates — wider answers, slightly less focused.",
|
|
442
|
+
},
|
|
443
|
+
"features.vector_backend.label": {
|
|
444
|
+
"ko": "의미 검색 방식",
|
|
445
|
+
"en": "Meaning-search engine",
|
|
446
|
+
},
|
|
447
|
+
"features.vector_backend.summary": {
|
|
448
|
+
"ko": "빠르기와 정확함 사이에서 고릅니다. 기본값은 전부 훑어보는 정확한 방식입니다.",
|
|
449
|
+
"en": "Trade speed against exactness. The default compares everything and is exact.",
|
|
450
|
+
},
|
|
451
|
+
"features.vector_backend.choice.brute": {
|
|
452
|
+
"ko": "전부 비교 (정확)",
|
|
453
|
+
"en": "Compare everything (exact)",
|
|
454
|
+
},
|
|
455
|
+
"features.vector_backend.choice.quantized": {
|
|
456
|
+
"ko": "간추려 비교 (빠름)",
|
|
457
|
+
"en": "Compare compressed (faster)",
|
|
458
|
+
},
|
|
459
|
+
"features.vector_backend.choice.hnsw": {
|
|
460
|
+
"ko": "근사 검색 (가장 빠름)",
|
|
461
|
+
"en": "Approximate search (fastest)",
|
|
462
|
+
},
|
|
320
463
|
# ── models ──────────────────────────────────────────────────────────
|
|
321
464
|
"models.other_user_credentials": {
|
|
322
465
|
"ko": "다른 사용자의 모델 자격 증명을 사용할 수 없습니다.",
|
|
@@ -32,14 +32,54 @@ FAMILY_PATTERNS: List[Tuple[str, re.Pattern]] = [
|
|
|
32
32
|
("qwen", re.compile(r"qwen", re.I)),
|
|
33
33
|
("llama", re.compile(r"\bllama|meta[-_]?llama", re.I)),
|
|
34
34
|
("claude", re.compile(r"claude", re.I)),
|
|
35
|
+
# gpt-oss is its own local family with its own chat format, so it must be
|
|
36
|
+
# matched before the cloud `gpt` pattern rather than falling into it.
|
|
37
|
+
("gpt_oss", re.compile(r"gpt[-_]?oss", re.I)),
|
|
35
38
|
("gpt", re.compile(r"gpt[-_]?(?:4|5)|openai", re.I)),
|
|
36
39
|
("gemini", re.compile(r"gemini", re.I)),
|
|
37
40
|
("grok", re.compile(r"grok|x[-_]?ai", re.I)),
|
|
41
|
+
("lfm2", re.compile(r"\blfm[-_]?2", re.I)),
|
|
38
42
|
]
|
|
39
43
|
|
|
44
|
+
#: HF ``config.json`` ``model_type`` → family code. An id is a name somebody
|
|
45
|
+
#: chose; an architecture is what mlx-lm / mlx-vlm actually dispatches on, so it
|
|
46
|
+
#: is the more reliable signal when we have it. Values here are the architectures
|
|
47
|
+
#: pinned in the model capability registry (11.2.0) plus the ones that came
|
|
48
|
+
#: before, so a model already on disk keeps its profile after its generation
|
|
49
|
+
#: stops being offered.
|
|
50
|
+
ARCHITECTURE_FAMILIES: Dict[str, str] = {
|
|
51
|
+
"gemma2": "gemma",
|
|
52
|
+
"gemma3": "gemma",
|
|
53
|
+
"gemma4": "gemma",
|
|
54
|
+
"gemma4_unified": "gemma",
|
|
55
|
+
"qwen2_5_vl": "qwen",
|
|
56
|
+
"qwen3_vl": "qwen",
|
|
57
|
+
"qwen3_vl_moe": "qwen",
|
|
58
|
+
"qwen3_5": "qwen",
|
|
59
|
+
"qwen3_5_moe": "qwen",
|
|
60
|
+
"llama": "llama",
|
|
61
|
+
"llama4": "llama",
|
|
62
|
+
"mllama": "llama",
|
|
63
|
+
"gpt_oss": "gpt_oss",
|
|
64
|
+
"lfm2": "lfm2",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def family_for_architecture(architecture: Optional[str]) -> str:
|
|
69
|
+
"""Map an HF ``model_type`` to a family code (``"unknown"`` when unmapped)."""
|
|
70
|
+
return ARCHITECTURE_FAMILIES.get(str(architecture or "").strip().lower(), "unknown")
|
|
71
|
+
|
|
40
72
|
|
|
41
|
-
def detect_model_family(model_id: str) -> str:
|
|
42
|
-
"""주어진 model_id 문자열에서 family 코드를 추론한다.
|
|
73
|
+
def detect_model_family(model_id: str, architecture: Optional[str] = None) -> str:
|
|
74
|
+
"""주어진 model_id 문자열에서 family 코드를 추론한다.
|
|
75
|
+
|
|
76
|
+
``architecture`` (HF config ``model_type``) 가 주어지고 알려진 값이면 그것을
|
|
77
|
+
우선한다 — 로더가 실제로 분기하는 값이기 때문이다.
|
|
78
|
+
"""
|
|
79
|
+
if architecture:
|
|
80
|
+
by_arch = family_for_architecture(architecture)
|
|
81
|
+
if by_arch != "unknown":
|
|
82
|
+
return by_arch
|
|
43
83
|
if not model_id:
|
|
44
84
|
return "unknown"
|
|
45
85
|
raw = str(model_id)
|
|
@@ -96,6 +136,35 @@ FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
|
96
136
|
"disable_draft": False,
|
|
97
137
|
"postprocess": ["strip_role_tokens"],
|
|
98
138
|
},
|
|
139
|
+
# gpt-oss speaks the "harmony" response format: the channel markers are
|
|
140
|
+
# stop sequences, not text, so the reply never has to be salvaged from them.
|
|
141
|
+
"gpt_oss": {
|
|
142
|
+
"family": "gpt_oss",
|
|
143
|
+
"supports_system": True,
|
|
144
|
+
"supports_vision": False,
|
|
145
|
+
"chat_template": "tokenizer_default",
|
|
146
|
+
"preferred_engines": ["local_mlx", "ollama", "llamacpp"],
|
|
147
|
+
"temperature": 0.2,
|
|
148
|
+
"top_p": 0.9,
|
|
149
|
+
"max_tokens": 4096,
|
|
150
|
+
"stop_sequences": ["<|return|>", "<|call|>", "<|endoftext|>"],
|
|
151
|
+
"disable_draft": False,
|
|
152
|
+
"postprocess": ["strip_role_tokens"],
|
|
153
|
+
},
|
|
154
|
+
# LFM2 uses a ChatML-style template; text only, so no vision claim.
|
|
155
|
+
"lfm2": {
|
|
156
|
+
"family": "lfm2",
|
|
157
|
+
"supports_system": True,
|
|
158
|
+
"supports_vision": False,
|
|
159
|
+
"chat_template": "tokenizer_default",
|
|
160
|
+
"preferred_engines": ["local_mlx", "ollama", "llamacpp"],
|
|
161
|
+
"temperature": 0.2,
|
|
162
|
+
"top_p": 0.9,
|
|
163
|
+
"max_tokens": 4096,
|
|
164
|
+
"stop_sequences": ["<|im_end|>", "<|endoftext|>"],
|
|
165
|
+
"disable_draft": False,
|
|
166
|
+
"postprocess": ["strip_role_tokens"],
|
|
167
|
+
},
|
|
99
168
|
"unknown": {
|
|
100
169
|
"family": "unknown",
|
|
101
170
|
"supports_system": True,
|
|
@@ -623,9 +692,11 @@ def get_stop_sequences(model_id: str, engine: Optional[str] = None) -> List[str]
|
|
|
623
692
|
|
|
624
693
|
|
|
625
694
|
__all__ = [
|
|
695
|
+
"ARCHITECTURE_FAMILIES",
|
|
626
696
|
"FAMILY_PROFILES",
|
|
627
697
|
"CompatProfile",
|
|
628
698
|
"detect_model_family",
|
|
699
|
+
"family_for_architecture",
|
|
629
700
|
"friendly_model_runtime_error",
|
|
630
701
|
"get_model_profile",
|
|
631
702
|
"model_runtime_compatibility",
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Dict
|
|
12
12
|
|
|
13
|
-
WORKSPACE_OS_VERSION = "11.
|
|
13
|
+
WORKSPACE_OS_VERSION = "11.2.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
|
|
@@ -32,7 +32,11 @@ OPENAI_COMPATIBLE_PROVIDERS = {
|
|
|
32
32
|
"xai": {
|
|
33
33
|
"env_key": "XAI_API_KEY",
|
|
34
34
|
"base_url": "https://api.x.ai/v1",
|
|
35
|
-
|
|
35
|
+
# ``grok-beta`` was xAI's 2024 preview id and has been decommissioned;
|
|
36
|
+
# a request naming it now 404s at the provider. ``grok-4.5`` is the
|
|
37
|
+
# current flagship on api.x.ai and is multimodal, which is also why the
|
|
38
|
+
# separate ``grok-vision-beta`` row below is gone rather than renamed.
|
|
39
|
+
"default_model": "grok-4.5",
|
|
36
40
|
},
|
|
37
41
|
"ollama": {
|
|
38
42
|
"env_key": "OLLAMA_API_KEY",
|
|
@@ -83,7 +87,7 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
83
87
|
{"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
|
|
84
88
|
{"id": "qwen/qwen3-vl-235b-a22b-instruct", "name": "Qwen3-VL 235B A22B via OpenRouter", "family": "Qwen"},
|
|
85
89
|
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
|
|
86
|
-
{"id": "x-ai/grok-
|
|
90
|
+
{"id": "x-ai/grok-4.5", "name": "Grok 4.5 via OpenRouter", "family": "Grok"},
|
|
87
91
|
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
|
|
88
92
|
{"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
|
|
89
93
|
],
|
|
@@ -96,8 +100,12 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
96
100
|
{"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
97
101
|
],
|
|
98
102
|
"xai": [
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
# One current generation only, per MODEL_POLICY's "do not keep old
|
|
104
|
+
# same-family generations" rule. The retired preview ids
|
|
105
|
+
# (grok-beta / grok-vision-beta) are not carried as fallbacks: a model
|
|
106
|
+
# the provider no longer serves is not compatibility, it is a 404 the
|
|
107
|
+
# user has to discover for themselves.
|
|
108
|
+
{"id": "grok-4.5", "name": "Grok 4.5", "family": "Grok"},
|
|
101
109
|
],
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -621,6 +621,29 @@ def phase_domain(ctx: RuntimeContext) -> None:
|
|
|
621
621
|
)
|
|
622
622
|
|
|
623
623
|
|
|
624
|
+
def self_model_port(workspace_graph: Any) -> Any:
|
|
625
|
+
"""The agent loop's Self-Model port (v11.2.0).
|
|
626
|
+
|
|
627
|
+
11.1.0 built ``executor_prompt_for(self_model_summary=…)`` and had nothing
|
|
628
|
+
to pass it; this is what the composition root passes. ``summary_for_prompt``
|
|
629
|
+
never raises and answers ``""`` for a Brain that knows nothing about its
|
|
630
|
+
owner — which produces exactly the pre-11.2.0 prompt bytes.
|
|
631
|
+
|
|
632
|
+
A module-level factory rather than a closure inside ``phase_services``
|
|
633
|
+
because a port that cannot be called on its own cannot be tested on its own.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
def _summary(*, user_email: Any = None, workspace_id: Any = None) -> str:
|
|
637
|
+
from lattice_brain.self_model import summary_for_prompt
|
|
638
|
+
|
|
639
|
+
graph = workspace_graph()
|
|
640
|
+
if graph is None:
|
|
641
|
+
return ""
|
|
642
|
+
return summary_for_prompt(graph, workspace_id=workspace_id)
|
|
643
|
+
|
|
644
|
+
return _summary
|
|
645
|
+
|
|
646
|
+
|
|
624
647
|
# ── phase 6b: retrieval, agent runtime, and the typed AppContext ─────────────
|
|
625
648
|
def phase_services(ctx: RuntimeContext) -> None:
|
|
626
649
|
"""Retrieval/context assembly, the chat agent runtime, and the AppContext.
|
|
@@ -756,6 +779,9 @@ def phase_services(ctx: RuntimeContext) -> None:
|
|
|
756
779
|
audit=ctx.append_audit_event,
|
|
757
780
|
hooks=ctx.HOOKS_REGISTRY,
|
|
758
781
|
brain_memory=ctx.BRAIN_MEMORY,
|
|
782
|
+
self_model_summary=self_model_port(
|
|
783
|
+
lambda: ctx.KNOWLEDGE_GRAPH if ctx.ENABLE_GRAPH else None
|
|
784
|
+
),
|
|
759
785
|
)
|
|
760
786
|
)
|
|
761
787
|
|
|
@@ -803,6 +829,8 @@ def phase_services(ctx: RuntimeContext) -> None:
|
|
|
803
829
|
require_graph=ctx._require_graph,
|
|
804
830
|
workspace_graph=ctx._workspace_graph,
|
|
805
831
|
graph_stats=ctx._graph_stats_safe,
|
|
832
|
+
# Provider, not a value: REVIEW_QUEUE lands two phases later.
|
|
833
|
+
review_queue=lambda: ctx.REVIEW_QUEUE,
|
|
806
834
|
workspace_models=_workspace_models_payload,
|
|
807
835
|
workspace_settings=_workspace_settings_payload,
|
|
808
836
|
scan_environment=scan_environment,
|
|
@@ -25,6 +25,7 @@ def build_chat_agent_runtime_from_context(
|
|
|
25
25
|
audit: Any,
|
|
26
26
|
hooks: Any,
|
|
27
27
|
brain_memory: Any,
|
|
28
|
+
self_model_summary: Any = None,
|
|
28
29
|
) -> Any:
|
|
29
30
|
# Ensure dispatch + agent share the same autonomy dial before the runtime
|
|
30
31
|
# is constructed (process-wide service; data_dir refined later at router mount).
|
|
@@ -39,6 +40,9 @@ def build_chat_agent_runtime_from_context(
|
|
|
39
40
|
hooks=hooks,
|
|
40
41
|
brain_memory=brain_memory,
|
|
41
42
|
permission_mode=resolve_active_permission_mode,
|
|
43
|
+
# v11.2.0: a scoped resolver, not a snapshot — the profile a run sees is
|
|
44
|
+
# the one the Brain holds when the run starts, per user and workspace.
|
|
45
|
+
self_model_summary=self_model_summary,
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Wire the feature switchboard into the running app (v11.2.0).
|
|
2
|
+
|
|
3
|
+
Same shape as ``permission_mode_wiring``: one process-wide service, an
|
|
4
|
+
idempotent router mount guarded on ``app.state``, and explicit arguments that
|
|
5
|
+
*rebind* an already-created service rather than being dropped — a lazy first
|
|
6
|
+
caller must not pin the store to a fallback data dir with no audit sink.
|
|
7
|
+
|
|
8
|
+
The one job unique to this module is :func:`bind_feature_gates`: it points each
|
|
9
|
+
opt-in gate's resolver at the service, which is the step that turns a persisted
|
|
10
|
+
preference into behaviour. Every gate is imported inside the function so
|
|
11
|
+
importing this module costs nothing; ``lattice_brain.synthesis`` in particular
|
|
12
|
+
drags in the graph layer.
|
|
13
|
+
|
|
14
|
+
Unbinding is a supported operation (:func:`unbind_feature_gates`) because these
|
|
15
|
+
gates are module-level singletons: a test that bound them must be able to give
|
|
16
|
+
them back, and "the environment answers again" has to be reachable.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import threading
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from latticeai.services.feature_toggles import FeatureToggleService
|
|
27
|
+
|
|
28
|
+
_LOCK = threading.Lock()
|
|
29
|
+
_SHARED: Optional[FeatureToggleService] = None
|
|
30
|
+
|
|
31
|
+
#: ``feature id -> (module, attribute)`` for every boolean gate. Resolved lazily
|
|
32
|
+
#: by :func:`_boolean_gates` so the table stays readable next to the catalog it
|
|
33
|
+
#: mirrors, and a rename that breaks the pairing fails loudly at wiring time.
|
|
34
|
+
GATE_BINDINGS: Tuple[Tuple[str, str, str], ...] = (
|
|
35
|
+
("allow_multimodal", "lattice_brain.ingestion", "MULTIMODAL_GATE"),
|
|
36
|
+
("video_ingest", "lattice_brain.ingestion", "VIDEO_GATE"),
|
|
37
|
+
("auto_vector_index", "lattice_brain.ingestion", "AUTO_VECTOR_INDEX_GATE"),
|
|
38
|
+
("brain_network", "lattice_brain.portability", "BRAIN_NETWORK_GATE"),
|
|
39
|
+
("synthesis", "lattice_brain.synthesis", "SYNTHESIS_GATE"),
|
|
40
|
+
("fusion_rrf", "lattice_brain.graph.fusion", "FUSION_RRF_GATE"),
|
|
41
|
+
("graph_expansion", "lattice_brain.graph.fusion", "GRAPH_EXPANSION_GATE"),
|
|
42
|
+
("vault_watch", "latticeai.services.folder_watch", "VAULT_WATCH_GATE"),
|
|
43
|
+
("auto_late_fusion", "latticeai.services.search_service", "IMAGE_QUERY_FUSION_GATE"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#: The one non-boolean seam: the vector backend is a pick-one-of-three.
|
|
47
|
+
CHOICE_FEATURE = "vector_backend"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _default_data_dir() -> Path:
|
|
51
|
+
raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
|
|
52
|
+
if raw:
|
|
53
|
+
return Path(raw)
|
|
54
|
+
return Path.home() / ".ltcai"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _boolean_gates() -> List[Tuple[str, Any]]:
|
|
58
|
+
"""``[(feature id, gate)]`` — imports are here, not at module import."""
|
|
59
|
+
from importlib import import_module
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
(feature_id, getattr(import_module(module), attribute))
|
|
63
|
+
for feature_id, module, attribute in GATE_BINDINGS
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _bind_vector_index(resolver: Any) -> None:
|
|
68
|
+
from lattice_brain.graph.vector_index.selector import bind_vector_index_resolver
|
|
69
|
+
|
|
70
|
+
bind_vector_index_resolver(resolver)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_feature_toggle_service(
|
|
74
|
+
*,
|
|
75
|
+
data_dir: Optional[Path] = None,
|
|
76
|
+
audit: Optional[Callable[..., None]] = None,
|
|
77
|
+
) -> FeatureToggleService:
|
|
78
|
+
"""Process-wide singleton; explicit arguments rebind an existing one."""
|
|
79
|
+
global _SHARED
|
|
80
|
+
with _LOCK:
|
|
81
|
+
if _SHARED is None:
|
|
82
|
+
_SHARED = FeatureToggleService(
|
|
83
|
+
data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
|
|
84
|
+
audit=audit,
|
|
85
|
+
)
|
|
86
|
+
return _SHARED
|
|
87
|
+
if data_dir is not None:
|
|
88
|
+
_SHARED.rebind_data_dir(Path(data_dir))
|
|
89
|
+
if audit is not None:
|
|
90
|
+
_SHARED.rebind_audit(audit)
|
|
91
|
+
return _SHARED
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bind_feature_gates(service: Optional[FeatureToggleService] = None) -> None:
|
|
95
|
+
"""Point every opt-in gate at the switchboard.
|
|
96
|
+
|
|
97
|
+
After this, a persisted preference beats the environment variable for every
|
|
98
|
+
feature in the catalog — which is the whole point, since the panel is the
|
|
99
|
+
only control surface most people will ever use.
|
|
100
|
+
"""
|
|
101
|
+
resolved = service or get_feature_toggle_service()
|
|
102
|
+
for feature_id, gate in _boolean_gates():
|
|
103
|
+
# ``gate.local`` is the fallback on purpose: the switchboard speaks only
|
|
104
|
+
# for switches this person actually moved. Everything else still answers
|
|
105
|
+
# from the gate's own override → env → default, so binding changes
|
|
106
|
+
# nothing at all for an install that never opened the panel.
|
|
107
|
+
gate.bind(resolved.resolver(feature_id, gate.local))
|
|
108
|
+
_bind_vector_index(resolved.choice_resolver(CHOICE_FEATURE))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def unbind_feature_gates() -> None:
|
|
112
|
+
"""Hand every gate back to its environment variable."""
|
|
113
|
+
for _feature_id, gate in _boolean_gates():
|
|
114
|
+
gate.bind(None)
|
|
115
|
+
_bind_vector_index(None)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def reset_feature_toggle_service() -> None:
|
|
119
|
+
"""Drop the singleton *and* the bindings that pointed at it (tests)."""
|
|
120
|
+
global _SHARED
|
|
121
|
+
unbind_feature_gates()
|
|
122
|
+
with _LOCK:
|
|
123
|
+
_SHARED = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def register_features_router(
|
|
127
|
+
app: Any,
|
|
128
|
+
*,
|
|
129
|
+
require_user: Callable[..., str],
|
|
130
|
+
data_dir: Optional[Path] = None,
|
|
131
|
+
append_audit_event: Optional[Callable[..., None]] = None,
|
|
132
|
+
) -> FeatureToggleService:
|
|
133
|
+
"""Install GET/POST /api/features on ``app`` and bind the gates. Idempotent.
|
|
134
|
+
|
|
135
|
+
The guard is a flag on ``app.state`` rather than a scan of route paths: a
|
|
136
|
+
router included through fastapi >= 0.140 has no flat ``path`` to introspect,
|
|
137
|
+
so path scanning silently stopped seeing the mount (see
|
|
138
|
+
``permission_mode_wiring`` for the same note).
|
|
139
|
+
"""
|
|
140
|
+
from latticeai.api.features import create_features_router
|
|
141
|
+
|
|
142
|
+
service = get_feature_toggle_service(data_dir=data_dir, audit=append_audit_event)
|
|
143
|
+
bind_feature_gates(service)
|
|
144
|
+
state = getattr(app, "state", None)
|
|
145
|
+
if state is not None and getattr(state, "_ltcai_features_mounted", False):
|
|
146
|
+
return service
|
|
147
|
+
app.include_router(
|
|
148
|
+
create_features_router(service=service, require_user=require_user)
|
|
149
|
+
)
|
|
150
|
+
if state is not None:
|
|
151
|
+
state._ltcai_features_mounted = True
|
|
152
|
+
return service
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"CHOICE_FEATURE",
|
|
157
|
+
"GATE_BINDINGS",
|
|
158
|
+
"bind_feature_gates",
|
|
159
|
+
"get_feature_toggle_service",
|
|
160
|
+
"register_features_router",
|
|
161
|
+
"reset_feature_toggle_service",
|
|
162
|
+
"unbind_feature_gates",
|
|
163
|
+
]
|
|
@@ -675,4 +675,15 @@ def register_review_and_brain_tail_routers(
|
|
|
675
675
|
append_audit_event=append_audit_event,
|
|
676
676
|
knowledge_graph=knowledge_graph,
|
|
677
677
|
)
|
|
678
|
+
# The opt-in switchboard. Mounted last on purpose: binding the gates to the
|
|
679
|
+
# service is what makes a stored preference beat an env var, and every
|
|
680
|
+
# module that owns one of those gates has been imported by now.
|
|
681
|
+
from latticeai.runtime.feature_toggle_wiring import register_features_router
|
|
682
|
+
|
|
683
|
+
register_features_router(
|
|
684
|
+
app,
|
|
685
|
+
require_user=require_user,
|
|
686
|
+
data_dir=data_dir,
|
|
687
|
+
append_audit_event=append_audit_event,
|
|
688
|
+
)
|
|
678
689
|
return brain_network
|
|
@@ -76,6 +76,14 @@ class AppContext:
|
|
|
76
76
|
workspace_graph: Optional[Callable[[], Any]] = None
|
|
77
77
|
graph_stats: Optional[Callable[[], dict]] = None
|
|
78
78
|
|
|
79
|
+
# ── review center ─────────────────────────────────────────────────────
|
|
80
|
+
# ``ReviewQueueService``, reached through a provider like
|
|
81
|
+
# ``workspace_graph`` above: the queue is built in a later runtime phase
|
|
82
|
+
# than this context, so a value captured here would be ``None`` forever.
|
|
83
|
+
# Call it per request; ``None`` means no Review Center is wired (tests,
|
|
84
|
+
# headless helpers) and the callers stage nothing rather than writing.
|
|
85
|
+
review_queue: Optional[Callable[[], Any]] = None
|
|
86
|
+
|
|
79
87
|
# ── workspace payload providers / skills ──────────────────────────────
|
|
80
88
|
workspace_models: Optional[Callable[[], dict]] = None
|
|
81
89
|
workspace_settings: Optional[Callable[[], dict]] = None
|