ltcai 10.8.0 → 10.9.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 +54 -52
- package/docs/CHANGELOG.md +37 -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/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 +1 -1
- package/package.json +3 -2
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_server_i18n.mjs +96 -0
- package/scripts/release_screen_claims.json +17 -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-CS9IeqUX.js} +1 -1
- package/static/app/assets/{AdminConsole-Bs8SR8fE.js → AdminConsole-3UkIEWGA.js} +1 -1
- package/static/app/assets/{Brain-BZIPsZlH.js → Brain-B22EmNqS.js} +1 -1
- package/static/app/assets/{BrainHome-BaH5MDce.js → BrainHome-CMDqgJF4.js} +2 -2
- package/static/app/assets/{BrainSignals-aDt-nSsW.js → BrainSignals-BeE8RJo3.js} +1 -1
- package/static/app/assets/{Capture-DDv940Bv.js → Capture-ZX9bQh68.js} +1 -1
- package/static/app/assets/{CommandPalette-Dq_MmPBl.js → CommandPalette-86m4FCcN.js} +1 -1
- package/static/app/assets/{Library-BkShbqmN.js → Library-Bhz5LUca.js} +1 -1
- package/static/app/assets/{LivingBrain-CXb8094H.js → LivingBrain-BSa0wpFG.js} +1 -1
- package/static/app/assets/{ProductFlow-BaiqFWLO.js → ProductFlow-IP4Q-5aQ.js} +1 -1
- package/static/app/assets/{ReviewCard-Be7E7X18.js → ReviewCard-BepjSDpN.js} +1 -1
- package/static/app/assets/{System-DCstbTxC.js → System-Bwx1h_jT.js} +1 -1
- package/static/app/assets/arrow-left-ig7AZU8B.js +1 -0
- package/static/app/assets/{bot-BR0eoeXY.js → bot-DQj0-LkM.js} +1 -1
- package/static/app/assets/{brain-BmWa1wea.js → brain-D8OEwmVj.js} +1 -1
- package/static/app/assets/{button-U1nU7xX4.js → button-CmTknyAP.js} +1 -1
- package/static/app/assets/{circle-pause-DG5KHjDL.js → circle-pause-yTCWRziJ.js} +1 -1
- package/static/app/assets/{circle-play-Chhcuq90.js → circle-play-Ccrva84R.js} +1 -1
- package/static/app/assets/{cpu-pkOSlT9h.js → cpu-CbJqWTlS.js} +1 -1
- package/static/app/assets/{download-CSRwz923.js → download-CkSzbzU-.js} +1 -1
- package/static/app/assets/{folder-open-DiMJ7W_7.js → folder-open-CKyjQ4PU.js} +1 -1
- package/static/app/assets/{hard-drive-iyLPAPdp.js → hard-drive-DAzk9um0.js} +1 -1
- package/static/app/assets/{index-BZRvGOM2.css → index-BfD-jhA9.css} +1 -1
- package/static/app/assets/{index-_ldE9TAX.js → index-CxOcwsHV.js} +3 -3
- package/static/app/assets/{input-e_r2hFAY.js → input-DcMETmZ7.js} +1 -1
- package/static/app/assets/{permissionCopy-9uOHktz3.js → permissionCopy-BVf13_25.js} +1 -1
- package/static/app/assets/{primitives-CnEAOmg2.js → primitives-CQV9Q2YM.js} +1 -1
- package/static/app/assets/search-CXGASMMH.js +1 -0
- package/static/app/assets/{share-2-DLDJJKmI.js → share-2-COWCHNZm.js} +1 -1
- package/static/app/assets/{shield-alert-BTtE-2EZ.js → shield-alert-BDrvilyK.js} +1 -1
- package/static/app/assets/{textarea-Bbzkcgqe.js → textarea-rUmsc8cP.js} +1 -1
- package/static/app/assets/{useFocusTrap-DJzQd-Za.js → useFocusTrap-Bi5UY_8v.js} +1 -1
- package/static/app/assets/{useQuery-6R_tqHRe.js → useQuery-C-AicB-3.js} +1 -1
- package/static/app/assets/{utils-XDsjcrhS.js → utils-BMwWg78e.js} +1 -1
- package/static/app/assets/{workspace-CwrG8LHq.js → workspace-Y93tls8P.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/arrow-left-DC9Oquhs.js +0 -1
- package/static/app/assets/search-veep4n6f.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,63 +58,64 @@ 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.9.0/SCREENSHOT_INDEX.md](output/release/v10.9.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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
- **Evidence bound to this build.** `output/release/v10.
|
|
68
|
+
The current release is **10.9.0 — Never Blocks**:
|
|
69
|
+
|
|
70
|
+
Lattice AI runs one event loop on your computer. Several things you could ask
|
|
71
|
+
it to do were being done *on* that loop, so while they ran the server could not
|
|
72
|
+
answer anything else. Downloading a model has a fifteen-minute timeout; that is
|
|
73
|
+
how long the whole product could be frozen by one click.
|
|
74
|
+
|
|
75
|
+
- **Nothing long runs on the event loop.** Model pulls, engine installs, MCP
|
|
76
|
+
package installs and the CPU/RAM probe behind the System screen all moved to
|
|
77
|
+
a worker thread. `/local/sysinfo` is the one most people met without knowing:
|
|
78
|
+
it is read by the System screen *and* by first-run analysis, so its ~2s
|
|
79
|
+
freeze landed exactly while an answer was streaming. Two things keep it
|
|
80
|
+
fixed — ruff's `ASYNC` blocking-call rules, and
|
|
81
|
+
`tests/unit/test_event_loop_not_blocked.py`, which runs a ticker coroutine
|
|
82
|
+
during the handler and asserts the loop kept getting control.
|
|
83
|
+
- **A focus ring you can see.** 10.8.0 added `border-color` to the transition
|
|
84
|
+
on the capture pills, so keyboard focus faded in over 150ms and at the
|
|
85
|
+
instant it landed the pill still looked idle. Focus rings may not ease.
|
|
86
|
+
- **The Brain stops thinking when it has answered.** A retrieval pulse parked a
|
|
87
|
+
900ms timer that put the organism back into "thinking"; if the answer
|
|
88
|
+
finished inside that window the timer still fired, and the Brain sat visibly
|
|
89
|
+
working on a question it had already answered.
|
|
90
|
+
- **One language, further in.** Fourteen more routers moved to the message
|
|
91
|
+
catalog — chat, memory, graph, files, portability, review, models, tools,
|
|
92
|
+
MCP, setup. `models.py`, `mcp.py` and `tools.py` had been answering some
|
|
93
|
+
errors in Korean and others in English on the same screen. Two gates keep
|
|
94
|
+
them migrated, and a third fails if the two gates disagree.
|
|
95
|
+
- **The welcome screen fits.** It measured 770px against a 747px viewport, so
|
|
96
|
+
the line explaining what you are agreeing to sat under the fold. Nothing was
|
|
97
|
+
removed; the whitespace gave back the 25px.
|
|
98
|
+
- **The streaming answer path has tests.** `useBrainChat` was 12% covered
|
|
99
|
+
because asserting anything about a stream needs frames arriving over time.
|
|
100
|
+
`frontend/src/test/fakeChatStream.ts` is that harness; eleven cases now cover
|
|
101
|
+
the answer building up, the stop button, refusals, grounding badges and live
|
|
102
|
+
step frames.
|
|
103
|
+
- **Evidence bound to this build.** `output/release/v10.9.0/` holds the twelve
|
|
104
104
|
capture screens, walkthrough gif/webm, and the asset-manifest / mock-server
|
|
105
|
-
fingerprints that lint re-checks before merge.
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
fingerprints that lint re-checks before merge. 10.9.0 claims no screen
|
|
106
|
+
redesign, and `scripts/release_screen_claims.json` says so explicitly.
|
|
107
|
+
- **Exact artifact names only.** Publish paths list `dist/ltcai-10.9.0-*` and
|
|
108
|
+
`ltcai-10.9.0.tgz` — never `dist/*`.
|
|
108
109
|
|
|
109
110
|
Release notes: [RELEASE.md](RELEASE.md) · Full history: [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
|
110
111
|
|
|
111
|
-
Expected artifacts for 10.
|
|
112
|
+
Expected artifacts for 10.9.0 release must use exact filenames:
|
|
112
113
|
|
|
113
|
-
- `dist/ltcai-10.
|
|
114
|
-
- `dist/ltcai-10.
|
|
115
|
-
- `ltcai-10.
|
|
116
|
-
- `dist/ltcai-10.
|
|
117
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_10.
|
|
114
|
+
- `dist/ltcai-10.9.0-py3-none-any.whl`
|
|
115
|
+
- `dist/ltcai-10.9.0.tar.gz`
|
|
116
|
+
- `ltcai-10.9.0.tgz`
|
|
117
|
+
- `dist/ltcai-10.9.0.vsix`
|
|
118
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_10.9.0_aarch64.dmg`
|
|
118
119
|
|
|
119
120
|
Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
|
|
120
121
|
|
|
@@ -149,6 +150,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for details and
|
|
|
149
150
|
|
|
150
151
|
| Version | Theme |
|
|
151
152
|
| --- | --- |
|
|
153
|
+
| 10.9.0 | Never Blocks |
|
|
152
154
|
| 10.8.0 | Within Reach |
|
|
153
155
|
| 10.7.0 | Plain Surface |
|
|
154
156
|
| 10.6.4 | Loud Limits |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ 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.9.0] - 2026-08-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 모델 내려받기(`ollama pull`, Hugging Face), 엔진 설치, MCP `pip`/`npm` 설치,
|
|
11
|
+
`/local/sysinfo` 호스트 측정이 `async def` 안에서 그대로 실행되어 실행 중
|
|
12
|
+
서버 전체가 다른 요청을 처리하지 못하던 문제 — 전부 `asyncio.to_thread`로
|
|
13
|
+
이관. 최악의 경우 15분(다운로드 타임아웃)간 채팅·`/health`·UI가 모두 멈췄습니다.
|
|
14
|
+
- Telegram 브리지의 사진/문서 업로드가 파일을 이벤트 루프에서 읽던 문제.
|
|
15
|
+
- 캡처 필의 키보드 포커스 테두리가 150ms에 걸쳐 서서히 나타나, 포커스가 닿는
|
|
16
|
+
순간에는 여전히 idle 색이던 문제(`home-simple.css` transition에서
|
|
17
|
+
`border-color` 제거).
|
|
18
|
+
- 답변이 끝난 뒤에도 Brain 유기체가 "생각 중"에 머물던 문제 — 회상 펄스가
|
|
19
|
+
건 900ms 타이머가 스트림 종료 시 취소되지 않았습니다.
|
|
20
|
+
- 같은 틱에 두 번 전송될 때 `stopStreaming`이 중단 핸들을 잃던 문제.
|
|
21
|
+
- 환영 화면이 747px 뷰포트에서 770px이라 마지막 안내 문장이 접힌 부분 아래에
|
|
22
|
+
있던 문제(`@media (max-height: 820px)` 단계 추가).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `tests/unit/test_event_loop_not_blocked.py` — 핸들러 실행 중 티커 코루틴을
|
|
26
|
+
돌려 이벤트 루프가 실제로 제어권을 받았는지 측정하는 회귀 테스트.
|
|
27
|
+
- `frontend/src/test/fakeChatStream.ts` — 프레임이 시간에 걸쳐 도착하는 가짜
|
|
28
|
+
SSE 하네스. `useBrainChat.test.tsx` 11개 시나리오.
|
|
29
|
+
- `scripts/check_server_i18n.mjs` — 이관 완료 선언된 라우터에서 문자열 리터럴
|
|
30
|
+
detail을 금지하는 lint 게이트(`npm run lint`에 편입).
|
|
31
|
+
- `tests/visual/v3.spec.js` — 환영 화면 fold 테스트, 캡처 필 포커스 테두리.
|
|
32
|
+
- ruff `ASYNC210/220/221/222/230/251` 규칙.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- 서버 메시지 카탈로그를 14개 라우터로 확대(chat, chat_history, chat_intents,
|
|
36
|
+
memory, knowledge_graph, local_files, portability, review_queue,
|
|
37
|
+
project_sessions, network_boundary, models, tools, mcp, setup) — 총 17개.
|
|
38
|
+
`MIGRATED_ROUTERS`와 lint 게이트 목록이 어긋나면 테스트가 실패합니다.
|
|
39
|
+
- `latticeai/services/model_loading.py`: 엔진 준비/다운로드 블록을
|
|
40
|
+
`_prepare_model_sources()`로 추출(워커 스레드에서 실행 가능한 동기 함수).
|
|
41
|
+
- `latticeai/api/static_routes.py`: 호스트 측정을 `_probe_host_capacity()`로
|
|
42
|
+
추출.
|
|
43
|
+
|
|
7
44
|
## [10.8.0] - 2026-08-04
|
|
8
45
|
|
|
9
46
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Community And Plugins
|
|
2
2
|
|
|
3
|
-
Current release: **10.
|
|
3
|
+
Current release: **10.9.0 — Never Blocks**.
|
|
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.9.0 — Never Blocks**.
|
|
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.9.0 — Never Blocks**.
|
|
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
|
@@ -49,7 +49,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
49
49
|
from ..utils import now_iso as _now
|
|
50
50
|
from .contracts import multi_agent_contract
|
|
51
51
|
|
|
52
|
-
MULTI_AGENT_VERSION = "10.
|
|
52
|
+
MULTI_AGENT_VERSION = "10.9.0"
|
|
53
53
|
|
|
54
54
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
55
55
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/chat.py
CHANGED
|
@@ -51,6 +51,7 @@ from latticeai.api.chat_hybrid import (
|
|
|
51
51
|
)
|
|
52
52
|
from latticeai.api.chat_intents import ChatIntentController
|
|
53
53
|
from latticeai.api.chat_stream import stream_chat
|
|
54
|
+
from latticeai.core.messages import DEFAULT_LANGUAGE, resolve_language, translate
|
|
54
55
|
from latticeai.core.project_sessions import ProjectSessionStore
|
|
55
56
|
from latticeai.core.run_store import AgentRunStore
|
|
56
57
|
from latticeai.services.app_context import AppContext
|
|
@@ -122,12 +123,13 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
122
123
|
def authenticated_identity(
|
|
123
124
|
current_user: str,
|
|
124
125
|
claimed_email: Optional[str],
|
|
126
|
+
language: str = DEFAULT_LANGUAGE,
|
|
125
127
|
) -> Optional[str]:
|
|
126
128
|
if current_user and claimed_email:
|
|
127
129
|
if current_user.strip().lower() != claimed_email.strip().lower():
|
|
128
130
|
raise HTTPException(
|
|
129
131
|
status_code=403,
|
|
130
|
-
detail="
|
|
132
|
+
detail=translate("common.user_mismatch", language),
|
|
131
133
|
)
|
|
132
134
|
return current_user or claimed_email or None
|
|
133
135
|
|
|
@@ -239,12 +241,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
239
241
|
notify=notify_chat_message,
|
|
240
242
|
)
|
|
241
243
|
|
|
242
|
-
def request_model(model_id: Optional[str]) -> Optional[str]:
|
|
244
|
+
def request_model(model_id: Optional[str], language: str = DEFAULT_LANGUAGE) -> Optional[str]:
|
|
243
245
|
selected = model_id or model_router.current_model_id
|
|
244
246
|
if model_id and model_id not in model_router.loaded_model_ids:
|
|
245
247
|
raise HTTPException(
|
|
246
248
|
status_code=404,
|
|
247
|
-
detail=
|
|
249
|
+
detail=translate("chat.model_not_loaded", language, model=model_id),
|
|
248
250
|
)
|
|
249
251
|
return selected
|
|
250
252
|
|
|
@@ -258,7 +260,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
258
260
|
len(req.image_data) if req.image_data else 0,
|
|
259
261
|
len(req.message or ""),
|
|
260
262
|
)
|
|
261
|
-
effective_email = authenticated_identity(current_user, req.user_email)
|
|
263
|
+
effective_email = authenticated_identity(current_user, req.user_email, resolve_language(request))
|
|
262
264
|
workspace_id = write_workspace(
|
|
263
265
|
workspace_scope_from_request(request),
|
|
264
266
|
current_user,
|
|
@@ -293,7 +295,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
293
295
|
history_user=history_user,
|
|
294
296
|
)
|
|
295
297
|
|
|
296
|
-
selected_model_id = request_model(req.model)
|
|
298
|
+
selected_model_id = request_model(req.model, resolve_language(request))
|
|
297
299
|
file_intent = is_file_action_request(req.message)
|
|
298
300
|
if file_intent and context.funnel_metrics is not None:
|
|
299
301
|
context.funnel_metrics.increment("file_requests")
|
|
@@ -303,11 +305,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
303
305
|
model_id=selected_model_id,
|
|
304
306
|
effective_email=effective_email,
|
|
305
307
|
workspace_id=workspace_id,
|
|
308
|
+
language=resolve_language(request),
|
|
306
309
|
)
|
|
307
310
|
if direct_response is not None:
|
|
308
311
|
return direct_response
|
|
309
312
|
if not selected_model_id:
|
|
310
|
-
return intent_controller.no_model_response()
|
|
313
|
+
return intent_controller.no_model_response(resolve_language(request))
|
|
311
314
|
if file_intent:
|
|
312
315
|
return await intent_controller.route_file_to_agent(
|
|
313
316
|
req,
|
|
@@ -29,6 +29,7 @@ from latticeai.api.chat_helpers import (
|
|
|
29
29
|
)
|
|
30
30
|
from latticeai.api.chat_stream import agent_live_stream
|
|
31
31
|
from latticeai.core.agent import AgentRunContext, AgentState, normalize_plan
|
|
32
|
+
from latticeai.core.messages import resolve_language
|
|
32
33
|
from latticeai.core.quiet import quiet
|
|
33
34
|
from latticeai.core.run_explain import explain_run
|
|
34
35
|
|
|
@@ -266,7 +267,7 @@ class AgentHTTPController:
|
|
|
266
267
|
"""
|
|
267
268
|
current_user = self.require_user(request)
|
|
268
269
|
self.enforce_rate_limit(current_user, "agent")
|
|
269
|
-
effective_email = self.authenticated_identity(current_user, req.user_email)
|
|
270
|
+
effective_email = self.authenticated_identity(current_user, req.user_email, resolve_language(request))
|
|
270
271
|
header_workspace = workspace_scope_from_request(request)
|
|
271
272
|
if req.workspace_id and header_workspace and req.workspace_id != header_workspace:
|
|
272
273
|
raise HTTPException(
|
|
@@ -9,7 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import Any, Callable, Dict
|
|
11
11
|
|
|
12
|
-
from fastapi import APIRouter,
|
|
12
|
+
from fastapi import APIRouter, Request
|
|
13
|
+
|
|
14
|
+
from latticeai.core.messages import http_error, resolve_language
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@dataclass(frozen=True)
|
|
@@ -48,7 +50,7 @@ def register_history_routes(router: APIRouter, deps: HistoryRouteDependencies) -
|
|
|
48
50
|
**deps.scope_for_user(current_user),
|
|
49
51
|
)
|
|
50
52
|
if not messages:
|
|
51
|
-
raise
|
|
53
|
+
raise http_error(404, "chat.conversation_not_found", resolve_language(request))
|
|
52
54
|
return {"id": conversation_id, "messages": messages}
|
|
53
55
|
|
|
54
56
|
@router.delete("/history/conversations/{conversation_id:path}")
|
|
@@ -31,9 +31,10 @@ from latticeai.core.file_generation import (
|
|
|
31
31
|
repair_bundle_references,
|
|
32
32
|
validate_project_bundle,
|
|
33
33
|
)
|
|
34
|
+
from latticeai.core.messages import DEFAULT_LANGUAGE, translate
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def next_available_path(root: Path, target: str) -> str:
|
|
37
|
+
def next_available_path(root: Path, target: str, language: str = DEFAULT_LANGUAGE) -> str:
|
|
37
38
|
"""Return ``target`` unchanged, or a ``name_2.ext``-style variant when the
|
|
38
39
|
file already exists in the workspace.
|
|
39
40
|
|
|
@@ -54,7 +55,7 @@ def next_available_path(root: Path, target: str) -> str:
|
|
|
54
55
|
return str(Path(target).with_name(variant.name))
|
|
55
56
|
raise HTTPException(
|
|
56
57
|
status_code=409,
|
|
57
|
-
detail=
|
|
58
|
+
detail=translate("chat.file_name_collision", language, name=target),
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
|
|
@@ -118,14 +119,10 @@ class ChatIntentController:
|
|
|
118
119
|
except Exception as exc: # noqa: BLE001 — metrics must never break chat
|
|
119
120
|
logging.warning("funnel metrics increment failed: %s", exc)
|
|
120
121
|
|
|
121
|
-
def no_model_response(self) -> JSONResponse:
|
|
122
|
-
detail = "
|
|
122
|
+
def no_model_response(self, language: str = DEFAULT_LANGUAGE) -> JSONResponse:
|
|
123
|
+
detail = translate("chat.no_model_loaded", language)
|
|
123
124
|
if self.config.is_public:
|
|
124
|
-
detail = (
|
|
125
|
-
"No public model loaded. Set OPENAI_API_KEY and "
|
|
126
|
-
f"LATTICEAI_PUBLIC_MODEL={self.public_model}, or call /models/load "
|
|
127
|
-
"with an OpenAI-compatible model."
|
|
128
|
-
)
|
|
125
|
+
detail = translate("models.public_model_missing", language, model=self.public_model)
|
|
129
126
|
return JSONResponse(
|
|
130
127
|
status_code=400,
|
|
131
128
|
content={
|
|
@@ -249,6 +246,7 @@ class ChatIntentController:
|
|
|
249
246
|
model_id: Optional[str],
|
|
250
247
|
effective_email: Optional[str] = None,
|
|
251
248
|
workspace_id: Optional[str] = None,
|
|
249
|
+
language: str = DEFAULT_LANGUAGE,
|
|
252
250
|
):
|
|
253
251
|
# An explicit path ("report.txt") wins; otherwise infer one from an
|
|
254
252
|
# explicit type keyword ("html 파일 만들어줘") so weak models never
|
|
@@ -266,19 +264,20 @@ class ChatIntentController:
|
|
|
266
264
|
model_id=model_id,
|
|
267
265
|
effective_email=effective_email,
|
|
268
266
|
workspace_id=workspace_id,
|
|
267
|
+
language=language,
|
|
269
268
|
)
|
|
270
269
|
target_path = explicit_target or infer_file_target(req.message)
|
|
271
270
|
if not target_path:
|
|
272
271
|
return None
|
|
273
272
|
# Never silently overwrite: an existing target gets a _2/_3 suffix so
|
|
274
273
|
# regeneration is safe and repeatable without the proposal flow.
|
|
275
|
-
deduped_path = next_available_path(self.agent_root, target_path)
|
|
274
|
+
deduped_path = next_available_path(self.agent_root, target_path, language)
|
|
276
275
|
renamed = deduped_path != target_path
|
|
277
276
|
target_path = deduped_path
|
|
278
277
|
content = inline_file_action_content(req.message)
|
|
279
278
|
generation_meta: Optional[Dict[str, Any]] = None
|
|
280
279
|
if content is None and not model_id:
|
|
281
|
-
return self.no_model_response()
|
|
280
|
+
return self.no_model_response(language)
|
|
282
281
|
if content is None and model_id:
|
|
283
282
|
# Model-agnostic pipeline: strict extension-aware prompt →
|
|
284
283
|
# extraction → validation → one corrective retry → deterministic
|
|
@@ -302,7 +301,7 @@ class ChatIntentController:
|
|
|
302
301
|
if content is None:
|
|
303
302
|
raise HTTPException(
|
|
304
303
|
status_code=400,
|
|
305
|
-
detail="
|
|
304
|
+
detail=translate("chat.file_generation_failed", language),
|
|
306
305
|
)
|
|
307
306
|
try:
|
|
308
307
|
result = self.execute_tool(
|
|
@@ -395,6 +394,7 @@ class ChatIntentController:
|
|
|
395
394
|
model_id: Optional[str],
|
|
396
395
|
effective_email: Optional[str] = None,
|
|
397
396
|
workspace_id: Optional[str] = None,
|
|
397
|
+
language: str = DEFAULT_LANGUAGE,
|
|
398
398
|
):
|
|
399
399
|
"""Artifact Loop: manifest → per-file generate/validate → bundle write.
|
|
400
400
|
|
|
@@ -405,8 +405,8 @@ class ChatIntentController:
|
|
|
405
405
|
standard ``artifacts[]`` contract, and downloadable as a zip.
|
|
406
406
|
"""
|
|
407
407
|
if not model_id:
|
|
408
|
-
return self.no_model_response()
|
|
409
|
-
project_dir = next_available_path(self.agent_root, manifest["name"])
|
|
408
|
+
return self.no_model_response(language)
|
|
409
|
+
project_dir = next_available_path(self.agent_root, manifest["name"], language)
|
|
410
410
|
bundle_files = [str(f["path"]) for f in manifest["files"]]
|
|
411
411
|
|
|
412
412
|
async def _generate(context: str) -> str:
|
|
@@ -19,6 +19,7 @@ from latticeai.api.workspace_scope import (
|
|
|
19
19
|
resolve_workspace_scope,
|
|
20
20
|
workspace_scope_from_request,
|
|
21
21
|
)
|
|
22
|
+
from latticeai.core.messages import http_error, resolve_language
|
|
22
23
|
from latticeai.core.quiet import quiet
|
|
23
24
|
|
|
24
25
|
|
|
@@ -456,9 +457,9 @@ def create_knowledge_graph_router(
|
|
|
456
457
|
async def knowledge_graph_neighbors(node_id: str, request: Request):
|
|
457
458
|
kg, allowed = _scoped(request)
|
|
458
459
|
if not node_id:
|
|
459
|
-
raise
|
|
460
|
+
raise http_error(400, "graph.node_id_required", resolve_language(request))
|
|
460
461
|
if allowed is not None and not _filter_scoped(kg, [{"id": node_id}], allowed):
|
|
461
|
-
raise
|
|
462
|
+
raise http_error(404, "graph.node_not_found", resolve_language(request))
|
|
462
463
|
payload = kg.neighbors(node_id)
|
|
463
464
|
if allowed is not None:
|
|
464
465
|
neighbors = _filter_scoped(kg, payload.get("neighbors", []), allowed)
|
|
@@ -476,13 +477,13 @@ def create_knowledge_graph_router(
|
|
|
476
477
|
current_user = require_user(request)
|
|
477
478
|
if current_user and req.user_email:
|
|
478
479
|
if current_user.strip().lower() != req.user_email.strip().lower():
|
|
479
|
-
raise
|
|
480
|
+
raise http_error(403, "common.user_mismatch", resolve_language(request))
|
|
480
481
|
effective_user = current_user or req.user_email or None
|
|
481
482
|
kg = graph()
|
|
482
483
|
workspace_id = _write_workspace(request, current_user)
|
|
483
484
|
event_type = (req.type or "").strip().lower()
|
|
484
485
|
if event_type not in {"message", "ai_response", "note"}:
|
|
485
|
-
raise
|
|
486
|
+
raise http_error(400, "graph.unsupported_type", resolve_language(request))
|
|
486
487
|
role = req.role or ("assistant" if event_type == "ai_response" else "user")
|
|
487
488
|
if ingestion_pipeline is not None:
|
|
488
489
|
source_type = "chat_message" if event_type in {"message", "ai_response"} else "note"
|