ltcai 8.3.0 → 8.4.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 +22 -16
- package/docs/CHANGELOG.md +27 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
- package/docs/DEVELOPMENT.md +8 -8
- package/docs/LEGACY_COMPATIBILITY.md +3 -3
- package/docs/ONBOARDING.md +2 -2
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +2 -2
- package/docs/kg-schema.md +2 -2
- 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 +230 -51
- package/latticeai/core/legacy_compatibility.py +2 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/model_catalog.py +6 -0
- package/latticeai/services/model_runtime.py +13 -0
- package/latticeai/services/product_readiness.py +22 -13
- package/package.json +1 -1
- 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 +1 -1
- package/static/sw.js +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI
|
|
2
2
|
|
|
3
|
-
**Lattice AI 8.
|
|
3
|
+
**Lattice AI 8.4.0 is the local-first Digital Brain platform with action-aware Brain Chat that creates files through the governed workspace file tool instead of only returning code.**
|
|
4
4
|
|
|
5
5
|
**Lattice AI는 모델이 바뀌어도 내 지식과 맥락을 보존하는 로컬 우선 AI 브레인입니다.**
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ downloads, update checks, and other external communication happen only after
|
|
|
11
11
|
explicit consent.
|
|
12
12
|
|
|
13
13
|
It is not a ChatGPT clone, a model launcher, a graph database, or a note app.
|
|
14
|
-
It is the finished private AI memory layer wrapped in a Living Brain experience — now with an 8.
|
|
14
|
+
It is the finished private AI memory layer wrapped in a Living Brain experience — now with an 8.4.0 action-readiness contract behind chat-to-tool file creation, onboarding, orchestration, graph ingestion, and the plugin/community path.
|
|
15
15
|
|
|
16
16
|
[](https://pypi.org/project/ltcai/)
|
|
17
17
|
[](https://www.npmjs.com/package/ltcai)
|
|
@@ -204,27 +204,32 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
|
|
|
204
204
|
|
|
205
205
|
## Current Release
|
|
206
206
|
|
|
207
|
-
The current release is **8.
|
|
207
|
+
The current release is **8.4.0 — Action-Aware Brain Chat**:
|
|
208
208
|
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
- Brain Chat now routes explicit file create/write/save/edit requests through
|
|
210
|
+
the governed workspace file tool instead of treating them as ordinary prose
|
|
211
|
+
generation.
|
|
212
|
+
- Requests such as `hello.md 파일 만들어줘` are executed through
|
|
213
|
+
`write_file`/`edit_file` style tool paths, and created artifacts are returned
|
|
214
|
+
in the chat response.
|
|
215
|
+
- Normal Q&A stays on the low-friction `/chat` generation path; only explicit
|
|
216
|
+
side-effect requests move into the tool-running agent loop.
|
|
217
|
+
- The release keeps the 8.3.0 managed legacy inventory, AgentRuntime/workflow
|
|
218
|
+
boundaries, unified graph ingestion, and workspace-safe duplicate content
|
|
219
|
+
protections active.
|
|
215
220
|
|
|
216
|
-
Expected artifacts for 8.
|
|
221
|
+
Expected artifacts for 8.4.0 release must use exact filenames:
|
|
217
222
|
|
|
218
|
-
- `dist/ltcai-8.
|
|
219
|
-
- `dist/ltcai-8.
|
|
220
|
-
- `ltcai-8.
|
|
221
|
-
- `dist/ltcai-8.
|
|
222
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.
|
|
223
|
+
- `dist/ltcai-8.4.0-py3-none-any.whl`
|
|
224
|
+
- `dist/ltcai-8.4.0.tar.gz`
|
|
225
|
+
- `ltcai-8.4.0.tgz`
|
|
226
|
+
- `dist/ltcai-8.4.0.vsix`
|
|
227
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.4.0_aarch64.dmg`
|
|
223
228
|
|
|
224
229
|
Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
|
|
225
230
|
|
|
226
231
|
See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
|
|
227
|
-
strategic roadmap slices applied through 8.
|
|
232
|
+
strategic roadmap slices applied through 8.4.0 and the follow-up tracks.
|
|
228
233
|
|
|
229
234
|
## Known Limitations
|
|
230
235
|
|
|
@@ -241,6 +246,7 @@ strategic roadmap slices applied through 8.3.0 and the follow-up tracks.
|
|
|
241
246
|
|
|
242
247
|
| Version | Theme |
|
|
243
248
|
| --- | --- |
|
|
249
|
+
| 8.4.0 | Action-Aware Brain Chat: explicit file create/write/save/edit requests from Brain Chat route into the governed workspace file tool so files are actually created instead of returned as code-only answers |
|
|
244
250
|
| 8.3.0 | Orchestrated Brain Readiness: managed legacy shim inventory, stronger AgentRuntime/workflow boundaries, unified graph ingestion, workspace-safe duplicate content, first-run onboarding, and explicit community/plugin growth path |
|
|
245
251
|
| 8.2.0 | Brain Brief: evidence-backed home briefing, honest empty-state guidance, recall/graph/model-proof next actions, and continued model/workspace runtime extraction |
|
|
246
252
|
| 8.1.0 | Intuitive Brain Home: living Brain, recent memory, connected topic, next action, and composer are visible in one product-first screen with refreshed 8.1.0 evidence and artifacts |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,33 @@ existed at that release.
|
|
|
8
8
|
|
|
9
9
|
No unreleased changes.
|
|
10
10
|
|
|
11
|
+
## [8.4.0] - 2026-07-01
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Added a chat-to-agent file action gate so explicit file create/write/save/edit
|
|
15
|
+
requests from Brain Chat execute through the governed workspace file tool.
|
|
16
|
+
- Added regression coverage that verifies `/chat` routes file creation intent to
|
|
17
|
+
the workspace file tool and returns created artifact metadata.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Kept ordinary Q&A on `/chat` while routing only explicit side-effect file
|
|
21
|
+
requests into the planner/executor/reviewer tool loop.
|
|
22
|
+
- Synchronized package/runtime/static/Tauri metadata and current-release docs to
|
|
23
|
+
8.4.0.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Allowed literal `/chat` file writes with user-provided content to execute
|
|
27
|
+
before model loading, while still using a loaded model when content must be
|
|
28
|
+
synthesized.
|
|
29
|
+
- Narrowed file target and content parsing to avoid treating surrounding prose
|
|
30
|
+
as the workspace path or literal file body.
|
|
31
|
+
- Restored model loading dependency exports after the runtime/loading split so
|
|
32
|
+
`/models/load` can prepare local MLX models again.
|
|
33
|
+
- Added the common Gemma 4 26B shorthand alias to the canonical
|
|
34
|
+
`mlx-community/gemma-4-26b-a4b-it-4bit` model id.
|
|
35
|
+
- Updated the local server launcher to run `python -m uvicorn` from the active
|
|
36
|
+
virtual environment, avoiding stale console-script interpreter bindings.
|
|
37
|
+
|
|
11
38
|
## [8.3.0] - 2026-07-01
|
|
12
39
|
|
|
13
40
|
### Added
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Community And Plugins
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
4
4
|
|
|
5
|
-
LatticeAI 8.
|
|
5
|
+
LatticeAI 8.4.0 defines the path from a strong local-first framework to a
|
|
6
6
|
product ecosystem. The immediate goal is small and practical: make it clear how
|
|
7
7
|
contributors can extend the Brain without weakening local-first trust,
|
|
8
8
|
workspace scoping, or release quality.
|
|
@@ -34,7 +34,7 @@ Plugins should add one bounded capability at a time:
|
|
|
34
34
|
- Compatibility requirements belong in `lattice_version`, and examples should
|
|
35
35
|
use current host-compatible values.
|
|
36
36
|
|
|
37
|
-
## 8.
|
|
37
|
+
## 8.4.0 Ecosystem Tasks
|
|
38
38
|
|
|
39
39
|
- Keep the hello-world plugin valid and boring.
|
|
40
40
|
- Expand examples only when they exercise real extension seams.
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Lattice AI Development
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
4
4
|
|
|
5
5
|
This document is for contributors working on the local-first Digital Brain
|
|
6
6
|
codebase. Product positioning and quick start stay in `README.md`; release
|
|
7
|
-
history is intentionally limited to 7.0.0-8.
|
|
7
|
+
history is intentionally limited to 7.0.0-8.4.0 in `docs/CHANGELOG.md` and
|
|
8
8
|
`RELEASE.md`.
|
|
9
9
|
|
|
10
10
|
## Product Contract
|
|
@@ -112,10 +112,10 @@ For user-facing, API, runtime, release, or packaging changes, check:
|
|
|
112
112
|
Release/publish examples must use exact target-version filenames. Do not
|
|
113
113
|
document wildcard artifact upload commands.
|
|
114
114
|
|
|
115
|
-
For 8.
|
|
115
|
+
For 8.4.0 release work, exact artifacts are:
|
|
116
116
|
|
|
117
|
-
- `dist/ltcai-8.
|
|
118
|
-
- `dist/ltcai-8.
|
|
119
|
-
- `ltcai-8.
|
|
120
|
-
- `dist/ltcai-8.
|
|
121
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.
|
|
117
|
+
- `dist/ltcai-8.4.0-py3-none-any.whl`
|
|
118
|
+
- `dist/ltcai-8.4.0.tar.gz`
|
|
119
|
+
- `ltcai-8.4.0.tgz`
|
|
120
|
+
- `dist/ltcai-8.4.0.vsix`
|
|
121
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.4.0_aarch64.dmg`
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Legacy Compatibility Map
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
4
4
|
|
|
5
5
|
Lattice AI is moving toward a smaller, modular architecture centered on
|
|
6
6
|
`lattice_brain`, `latticeai.services`, `latticeai.api`, and `latticeai.runtime`.
|
|
7
7
|
Some root-level modules remain packaged for compatibility with older imports,
|
|
8
8
|
CLI entrypoints, or extension workflows. Their presence does not define the
|
|
9
|
-
current 8.
|
|
9
|
+
current 8.4.0 architecture.
|
|
10
10
|
|
|
11
|
-
8.
|
|
11
|
+
8.4.0 also tracks the inner compatibility layers that sit below the root
|
|
12
12
|
modules. The managed inventory lives in `latticeai.core.legacy_compatibility`
|
|
13
13
|
and groups shims by layer:
|
|
14
14
|
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Onboarding
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
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
|
|
@@ -32,7 +32,7 @@ read the docs first.
|
|
|
32
32
|
|
|
33
33
|
## Release Gate
|
|
34
34
|
|
|
35
|
-
8.
|
|
35
|
+
8.4.0 treats onboarding as a release gate, not marketing copy. The current
|
|
36
36
|
machine-checkable product readiness report requires this five-minute contract,
|
|
37
37
|
the Brain Home surface, setup helpers, graph ingestion tests, and exact release
|
|
38
38
|
artifact documentation before the release can be called complete.
|
package/docs/TRUST_MODEL.md
CHANGED
package/docs/WHY_LATTICE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Why Lattice AI Exists
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
4
4
|
|
|
5
5
|
**Lattice AI is a local-first Digital Brain that keeps your knowledge durable
|
|
6
6
|
across any AI model.**
|
|
@@ -32,7 +32,7 @@ The graph matters, but it is not the product identity. The product identity is a
|
|
|
32
32
|
local-first Digital Brain: a place where the user can talk, add sources, watch
|
|
33
33
|
memory grow, and inspect the Knowledge Graph when they need proof.
|
|
34
34
|
|
|
35
|
-
In 8.
|
|
35
|
+
In 8.4.0 the first screen is intentionally not a dashboard. The living Brain,
|
|
36
36
|
conversation composer, evidence-backed Brain Brief, and five-minute onboarding
|
|
37
37
|
path appear together so the user immediately knows what matters, what to add,
|
|
38
38
|
and why the answer is grounded in local memory.
|
package/docs/kg-schema.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Knowledge Graph Schema
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.4.0 - Action-Aware Brain Chat**.
|
|
4
4
|
|
|
5
5
|
명세 출처: `lattice_ai_full_spec.pptx` 슬라이드 20·21·22
|
|
6
6
|
구현: `kg_schema.py`
|
|
@@ -90,7 +90,7 @@ Edge {
|
|
|
90
90
|
|
|
91
91
|
## Current Knowledge Graph First 엔티티/관계
|
|
92
92
|
|
|
93
|
-
8.
|
|
93
|
+
8.4.0 은 "모든 데이터 소스가 Knowledge Graph 로 수렴한다"는 원칙을 1급 스키마로
|
|
94
94
|
승격한다. 아래 타입은 **추가형(additive)**이다 — 기존 enum/legacy 매핑을 깨지 않고
|
|
95
95
|
`from_legacy` 가 무손실로 정규화하며, 알 수 없는 타입은 여전히 `CONCEPT`/`MENTIONS` 로
|
|
96
96
|
폴백한다. 스키마는 **확장 가능**하게 유지한다: 새 도메인 엔티티는 enum 멤버 1개 +
|
|
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
21
21
|
from .contracts import multi_agent_contract
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
MULTI_AGENT_VERSION = "8.
|
|
24
|
+
MULTI_AGENT_VERSION = "8.4.0"
|
|
25
25
|
|
|
26
26
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
27
27
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/chat.py
CHANGED
|
@@ -123,6 +123,90 @@ def is_current_url_request(text: str) -> bool:
|
|
|
123
123
|
def is_clear_command(text: str) -> bool:
|
|
124
124
|
return (text or "").strip().lower() in {"/clear", "/clear_all"}
|
|
125
125
|
|
|
126
|
+
# Path segments intentionally exclude spaces: allowing spaces let the target
|
|
127
|
+
# match swallow preceding words (e.g. "create a text file report.txt" resolved
|
|
128
|
+
# to the whole phrase as the path). Chat file targets are single tokens.
|
|
129
|
+
_FILE_TARGET_RE = re.compile(
|
|
130
|
+
r"(?<![\w.-])(?:[~./\\]?[\w.@()+-]+[\\/])*[\w.@()+-]+"
|
|
131
|
+
r"\.(?:py|js|jsx|ts|tsx|md|markdown|txt|json|yaml|yml|toml|html|css|csv|xml|pdf|docx|xlsx|pptx|sh|sql)",
|
|
132
|
+
re.IGNORECASE,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def is_file_action_request(text: str) -> bool:
|
|
137
|
+
"""Return True for chat requests that should execute file tools, not prose.
|
|
138
|
+
|
|
139
|
+
The Brain chat composer posts to ``/chat``. Without this gate, explicit
|
|
140
|
+
side-effect requests such as "create hello.md" are handled as plain model
|
|
141
|
+
generation, which commonly produces a code block instead of a real file.
|
|
142
|
+
Keep the gate narrow so normal Q&A and "how do I create a file?" stay in
|
|
143
|
+
ordinary chat.
|
|
144
|
+
"""
|
|
145
|
+
raw = (text or "").strip()
|
|
146
|
+
if not raw:
|
|
147
|
+
return False
|
|
148
|
+
lower = raw.lower()
|
|
149
|
+
|
|
150
|
+
if any(phrase in lower for phrase in (
|
|
151
|
+
"how to ", "how do i ", "방법", "어떻게", "예시", "sample", "example",
|
|
152
|
+
)) and not any(phrase in lower for phrase in (
|
|
153
|
+
"actually create", "real file", "실제로", "파일로", "저장해", "만들어",
|
|
154
|
+
)):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
has_target = bool(_FILE_TARGET_RE.search(raw))
|
|
158
|
+
has_file_word = any(word in lower for word in (
|
|
159
|
+
"file", "파일", "문서", "artifact", "아티팩트", "save as", "저장",
|
|
160
|
+
))
|
|
161
|
+
has_action = any(word in lower for word in (
|
|
162
|
+
"create", "make", "write", "save", "generate", "edit", "update",
|
|
163
|
+
"만들", "생성", "작성", "저장", "수정", "써줘", "만들어줘",
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
if not has_action:
|
|
167
|
+
return False
|
|
168
|
+
if has_target and has_action:
|
|
169
|
+
return True
|
|
170
|
+
return has_file_word and has_action
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def file_action_target(text: str) -> Optional[str]:
|
|
174
|
+
"""Extract the first explicit workspace file target from a request."""
|
|
175
|
+
match = _FILE_TARGET_RE.search(text or "")
|
|
176
|
+
if not match:
|
|
177
|
+
return None
|
|
178
|
+
return match.group(0).strip().strip("`'\".,:;)]}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def inline_file_action_content(text: str) -> Optional[str]:
|
|
182
|
+
"""Extract short user-provided content for deterministic file writes."""
|
|
183
|
+
raw = (text or "").strip()
|
|
184
|
+
# Each pattern requires an explicit binder so ambiguous words like "text"
|
|
185
|
+
# ("create a text file report.txt") or a bare "with" ("report.md with a
|
|
186
|
+
# summary of X") do not get captured as literal file content.
|
|
187
|
+
patterns = [
|
|
188
|
+
r"(?:내용|본문)\s*(?:은|는|이에요|입니다)\s*(.+)$",
|
|
189
|
+
r"(?:내용|본문|content|body|text)\s*[:=]\s*(.+)$",
|
|
190
|
+
r"(?:content|body)\s+(?:is|as)\s+(.+)$",
|
|
191
|
+
r"(?:with the content|with content|containing)\s+(.+)$",
|
|
192
|
+
]
|
|
193
|
+
for pattern in patterns:
|
|
194
|
+
match = re.search(pattern, raw, flags=re.IGNORECASE | re.DOTALL)
|
|
195
|
+
if match:
|
|
196
|
+
content = match.group(1).strip()
|
|
197
|
+
return content.strip("`'\"")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def strip_generated_file_content(text: str) -> str:
|
|
202
|
+
"""Remove common chat wrappers when a model is asked for file content only."""
|
|
203
|
+
content = (text or "").strip()
|
|
204
|
+
fenced = re.search(r"```(?:[\w.+-]+)?\s*(.*?)\s*```", content, flags=re.DOTALL)
|
|
205
|
+
if fenced:
|
|
206
|
+
content = fenced.group(1).strip()
|
|
207
|
+
return content
|
|
208
|
+
|
|
209
|
+
|
|
126
210
|
def format_network_status(info: Dict) -> str:
|
|
127
211
|
lines = [
|
|
128
212
|
f"내부 IP: {info.get('local_ip') or '확인 안 됨'}",
|
|
@@ -243,11 +327,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
243
327
|
user_email=user_email,
|
|
244
328
|
conversation_id=conversation_id,
|
|
245
329
|
)
|
|
246
|
-
|
|
330
|
+
|
|
247
331
|
def extract_screenshot_context(image_data: Optional[str]) -> str:
|
|
248
332
|
if not image_data:
|
|
249
333
|
return ""
|
|
250
|
-
|
|
334
|
+
|
|
251
335
|
lines = ["[SCREENSHOT INGESTION]"]
|
|
252
336
|
image_bytes = b""
|
|
253
337
|
try:
|
|
@@ -258,18 +342,18 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
258
342
|
except Exception as e:
|
|
259
343
|
lines.append(f"- image_decode_error: {e}")
|
|
260
344
|
return "\n".join(lines)
|
|
261
|
-
|
|
345
|
+
|
|
262
346
|
tesseract_path = shutil.which("tesseract")
|
|
263
347
|
if not tesseract_path:
|
|
264
348
|
lines.append("- ocr: unavailable; install `tesseract` to enable OCR text extraction.")
|
|
265
349
|
return "\n".join(lines)
|
|
266
|
-
|
|
350
|
+
|
|
267
351
|
temp_path = None
|
|
268
352
|
try:
|
|
269
353
|
with tempfile.NamedTemporaryFile(prefix="ltcai-screenshot-", suffix=".png", delete=False) as temp:
|
|
270
354
|
temp.write(image_bytes)
|
|
271
355
|
temp_path = temp.name
|
|
272
|
-
|
|
356
|
+
|
|
273
357
|
ocr_text = ""
|
|
274
358
|
for lang in ("kor+eng", "eng"):
|
|
275
359
|
completed = subprocess.run(
|
|
@@ -283,7 +367,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
283
367
|
ocr_text = completed.stdout.strip()
|
|
284
368
|
lines.append(f"- ocr_language: {lang}")
|
|
285
369
|
break
|
|
286
|
-
|
|
370
|
+
|
|
287
371
|
if ocr_text:
|
|
288
372
|
lines.append("- ocr_text:")
|
|
289
373
|
lines.append(ocr_text[:4000])
|
|
@@ -297,7 +381,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
297
381
|
Path(temp_path).unlink()
|
|
298
382
|
except OSError:
|
|
299
383
|
pass
|
|
300
|
-
|
|
384
|
+
|
|
301
385
|
return "\n".join(lines)
|
|
302
386
|
|
|
303
387
|
_AGENT_RUNTIME = context.chat_agent_runtime
|
|
@@ -329,7 +413,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
329
413
|
"conversation_id": req.conversation_id,
|
|
330
414
|
"workspace_id": workspace_scope_from_request(request),
|
|
331
415
|
}
|
|
332
|
-
|
|
416
|
+
|
|
333
417
|
if is_network_status_request(req.message):
|
|
334
418
|
history_message = f"{req.message}\n[Image attached]" if req.image_data else req.message
|
|
335
419
|
save_to_history("user", history_message, **history_meta, **history_user)
|
|
@@ -347,7 +431,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
347
431
|
headers={"X-Model": "network_status"},
|
|
348
432
|
)
|
|
349
433
|
return JSONResponse(content={"response": answer})
|
|
350
|
-
|
|
434
|
+
|
|
351
435
|
if is_clear_command(req.message):
|
|
352
436
|
command = req.message.strip().lower()
|
|
353
437
|
clear_scope = "all" if command == "/clear_all" else "conversation"
|
|
@@ -392,7 +476,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
392
476
|
headers={"X-Model": "history"},
|
|
393
477
|
)
|
|
394
478
|
return JSONResponse(content={"response": answer})
|
|
395
|
-
|
|
479
|
+
|
|
396
480
|
if is_current_url_request(req.message) and req.client_url:
|
|
397
481
|
answer = f"현재 페이지 URL: {req.client_url}"
|
|
398
482
|
save_to_history("user", req.message, **history_meta, **history_user)
|
|
@@ -406,7 +490,74 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
406
490
|
headers={"X-Model": "client_url"},
|
|
407
491
|
)
|
|
408
492
|
return JSONResponse(content={"response": answer})
|
|
409
|
-
|
|
493
|
+
|
|
494
|
+
if is_file_action_request(req.message):
|
|
495
|
+
target_path = file_action_target(req.message)
|
|
496
|
+
if target_path:
|
|
497
|
+
content = inline_file_action_content(req.message)
|
|
498
|
+
if content is None and router.current_model_id:
|
|
499
|
+
if req.model and req.model != router.current_model_id:
|
|
500
|
+
if req.model not in router.loaded_model_ids:
|
|
501
|
+
raise HTTPException(status_code=404, detail=f"Model '{req.model}' not loaded.")
|
|
502
|
+
router.switch_model(req.model)
|
|
503
|
+
generation_context = (
|
|
504
|
+
"Create the exact content for the requested file. "
|
|
505
|
+
"Return only the file bytes as plain text. "
|
|
506
|
+
"Do not wrap the answer in Markdown fences, commentary, or explanations.\n\n"
|
|
507
|
+
f"Target path: {target_path}\n"
|
|
508
|
+
f"User request: {req.message}"
|
|
509
|
+
)
|
|
510
|
+
raw_content = await router.generate_as(
|
|
511
|
+
router.current_model_id,
|
|
512
|
+
message="Return only the requested file content.",
|
|
513
|
+
context=generation_context,
|
|
514
|
+
max_tokens=req.max_tokens,
|
|
515
|
+
temperature=req.temperature,
|
|
516
|
+
)
|
|
517
|
+
content = strip_generated_file_content(str(raw_content))
|
|
518
|
+
if content is None:
|
|
519
|
+
content = ""
|
|
520
|
+
try:
|
|
521
|
+
result = execute_tool("write_file", {"path": target_path, "content": content})
|
|
522
|
+
except ToolError as exc:
|
|
523
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
524
|
+
answer = f"{result.get('path') or target_path} 파일을 만들었습니다."
|
|
525
|
+
created_files = [{
|
|
526
|
+
"path": result.get("path") or target_path,
|
|
527
|
+
"filename": Path(result.get("path") or target_path).name,
|
|
528
|
+
"bytes": result.get("bytes", 0),
|
|
529
|
+
"action": "write_file",
|
|
530
|
+
}]
|
|
531
|
+
notify_chat_message("user", req.message, req.source)
|
|
532
|
+
notify_chat_message("assistant", answer, req.source)
|
|
533
|
+
payload = {
|
|
534
|
+
"status": "ok",
|
|
535
|
+
"response": answer,
|
|
536
|
+
"workspace": str(AGENT_ROOT),
|
|
537
|
+
"steps": [{
|
|
538
|
+
"state": AgentState.EXECUTING.value,
|
|
539
|
+
"action": "write_file",
|
|
540
|
+
"args": {"path": target_path},
|
|
541
|
+
"result": result,
|
|
542
|
+
}],
|
|
543
|
+
"state_history": [AgentState.EXECUTING.value, AgentState.DONE.value],
|
|
544
|
+
"final_state": AgentState.DONE.value,
|
|
545
|
+
"created_files": created_files,
|
|
546
|
+
"routed_to_agent": True,
|
|
547
|
+
"action_route": "direct_write_file",
|
|
548
|
+
}
|
|
549
|
+
if req.stream:
|
|
550
|
+
async def _stream_file_result():
|
|
551
|
+
yield f"data: {json.dumps({'chunk': answer, 'model': router.current_model_id, 'agent': payload}, ensure_ascii=False)}\n\n"
|
|
552
|
+
yield f"data: {json.dumps({'chunk': '', 'model': router.current_model_id, 'agent': payload}, ensure_ascii=False)}\n\n"
|
|
553
|
+
yield "data: [DONE]\n\n"
|
|
554
|
+
return StreamingResponse(
|
|
555
|
+
_stream_file_result(),
|
|
556
|
+
media_type="text/event-stream",
|
|
557
|
+
headers={"X-Model": router.current_model_id or "tool", "X-Routed-To": "agent"},
|
|
558
|
+
)
|
|
559
|
+
return JSONResponse(content=payload)
|
|
560
|
+
|
|
410
561
|
if not router.current_model_id:
|
|
411
562
|
detail = "No model loaded. Call /models/load first."
|
|
412
563
|
if CONFIG.is_public:
|
|
@@ -420,12 +571,40 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
420
571
|
"action": "load_model",
|
|
421
572
|
},
|
|
422
573
|
)
|
|
423
|
-
|
|
574
|
+
|
|
424
575
|
if req.model and req.model != router.current_model_id:
|
|
425
576
|
if req.model not in router.loaded_model_ids:
|
|
426
577
|
raise HTTPException(status_code=404, detail=f"Model '{req.model}' not loaded.")
|
|
427
578
|
router.switch_model(req.model)
|
|
428
|
-
|
|
579
|
+
|
|
580
|
+
if is_file_action_request(req.message):
|
|
581
|
+
agent_req = AgentRequest(
|
|
582
|
+
message=req.message,
|
|
583
|
+
conversation_id=req.conversation_id,
|
|
584
|
+
source=req.source or "web",
|
|
585
|
+
max_steps=25,
|
|
586
|
+
temperature=min(req.temperature, 0.2),
|
|
587
|
+
user_email=effective_email,
|
|
588
|
+
user_nickname=req.user_nickname,
|
|
589
|
+
workspace_id=workspace_scope_from_request(request),
|
|
590
|
+
)
|
|
591
|
+
result = await agent(agent_req, request)
|
|
592
|
+
answer = str(result.get("response") or "파일 작업을 처리했습니다.")
|
|
593
|
+
notify_chat_message("user", req.message, req.source)
|
|
594
|
+
notify_chat_message("assistant", answer, req.source)
|
|
595
|
+
result["routed_to_agent"] = True
|
|
596
|
+
if req.stream:
|
|
597
|
+
async def _stream_agent_result():
|
|
598
|
+
yield f"data: {json.dumps({'chunk': answer, 'model': router.current_model_id, 'agent': result}, ensure_ascii=False)}\n\n"
|
|
599
|
+
yield f"data: {json.dumps({'chunk': '', 'model': router.current_model_id, 'agent': result}, ensure_ascii=False)}\n\n"
|
|
600
|
+
yield "data: [DONE]\n\n"
|
|
601
|
+
return StreamingResponse(
|
|
602
|
+
_stream_agent_result(),
|
|
603
|
+
media_type="text/event-stream",
|
|
604
|
+
headers={"X-Model": router.current_model_id, "X-Routed-To": "agent"},
|
|
605
|
+
)
|
|
606
|
+
return JSONResponse(content=result)
|
|
607
|
+
|
|
429
608
|
lang = detect_language(req.message)
|
|
430
609
|
context = f"[LANGUAGE: {_LANG_HINT[lang]}]\n" + (req.context or "")
|
|
431
610
|
# v4 Context System: one budgeted, provenance-carrying assembly
|
|
@@ -462,12 +641,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
462
641
|
print("📝 Document generation context retrieved from knowledge graph.")
|
|
463
642
|
except Exception as e:
|
|
464
643
|
logging.warning("Knowledge graph reinforcement skipped: %s", e)
|
|
465
|
-
|
|
644
|
+
|
|
466
645
|
if req.image_data:
|
|
467
646
|
screenshot_context = extract_screenshot_context(req.image_data)
|
|
468
647
|
if screenshot_context:
|
|
469
648
|
context += f"\n\n{screenshot_context}"
|
|
470
|
-
|
|
649
|
+
|
|
471
650
|
if CONFIG.auto_read_chat_paths:
|
|
472
651
|
_file_path_re = re.compile(r'(?:^|[\s\'\"(])((~|/[\w.])[^\s\'")\]]*)', re.MULTILINE)
|
|
473
652
|
requested_paths = [_m.group(1).strip() for _m in _file_path_re.finditer(req.message or "")]
|
|
@@ -487,7 +666,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
487
666
|
"Attach the file, upload it, or use an approved local-file tool flow."
|
|
488
667
|
),
|
|
489
668
|
)
|
|
490
|
-
|
|
669
|
+
|
|
491
670
|
trace_seed = CHAT_SERVICE.build_graph_trace(
|
|
492
671
|
req.message,
|
|
493
672
|
KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
|
|
@@ -497,11 +676,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
497
676
|
# Persisted with the answer trace: 'why is this in my context?'
|
|
498
677
|
# is answerable from the stored record (UI surface lands in T9b).
|
|
499
678
|
trace_seed["context_assembly"] = context_trace
|
|
500
|
-
|
|
679
|
+
|
|
501
680
|
history_message = f"{req.message}\n[Image attached]" if req.image_data else req.message
|
|
502
681
|
save_to_history("user", history_message, **history_meta, **history_user)
|
|
503
682
|
notify_chat_message("user", req.message, req.source)
|
|
504
|
-
|
|
683
|
+
|
|
505
684
|
if is_doc_gen and ENABLE_GRAPH and KNOWLEDGE_GRAPH:
|
|
506
685
|
conv_key = req.conversation_id or "default"
|
|
507
686
|
session = _doc_gen_sessions.get(conv_key)
|
|
@@ -512,7 +691,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
512
691
|
system_prompt = session.get_system_prompt(graph_md)
|
|
513
692
|
sources = (doc_gen_context_result or {}).get("sources", [])
|
|
514
693
|
footnote = format_sources_footnote(sources)
|
|
515
|
-
|
|
694
|
+
|
|
516
695
|
if req.stream:
|
|
517
696
|
async def _stream_doc_gen():
|
|
518
697
|
collected = []
|
|
@@ -563,7 +742,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
563
742
|
)
|
|
564
743
|
notify_chat_message("assistant", str(result), req.source)
|
|
565
744
|
return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
|
|
566
|
-
|
|
745
|
+
|
|
567
746
|
if req.stream:
|
|
568
747
|
recent_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
|
|
569
748
|
stream_context = context
|
|
@@ -593,9 +772,9 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
593
772
|
else:
|
|
594
773
|
history_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
|
|
595
774
|
full_context = f"{history_context}\n{context}" if context else history_context
|
|
596
|
-
|
|
775
|
+
|
|
597
776
|
result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
|
|
598
|
-
|
|
777
|
+
|
|
599
778
|
save_to_history("assistant", str(result), **history_meta, **history_user)
|
|
600
779
|
trace_record = CHAT_SERVICE.record_trace(
|
|
601
780
|
question=req.message,
|
|
@@ -605,22 +784,22 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
605
784
|
trace=trace_seed,
|
|
606
785
|
)
|
|
607
786
|
notify_chat_message("assistant", str(result), req.source)
|
|
608
|
-
|
|
787
|
+
|
|
609
788
|
return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
|
|
610
|
-
|
|
789
|
+
|
|
611
790
|
|
|
612
791
|
@api_router.get("/history")
|
|
613
792
|
async def fetch_history(request: Request):
|
|
614
793
|
"""웹 화면에서 이전 대화를 불러올 수 있도록 히스토리를 반환합니다."""
|
|
615
794
|
require_user(request)
|
|
616
795
|
return get_history()
|
|
617
|
-
|
|
796
|
+
|
|
618
797
|
@api_router.get("/history/conversations")
|
|
619
798
|
async def fetch_history_conversations(request: Request):
|
|
620
799
|
"""저장된 히스토리를 대화 단위로 묶어 반환합니다."""
|
|
621
800
|
require_user(request)
|
|
622
801
|
return group_history_conversations()
|
|
623
|
-
|
|
802
|
+
|
|
624
803
|
@api_router.get("/history/conversations/{conversation_id:path}")
|
|
625
804
|
async def fetch_history_conversation(conversation_id: str, request: Request):
|
|
626
805
|
"""선택한 대화의 메시지를 반환합니다."""
|
|
@@ -629,7 +808,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
629
808
|
if not messages:
|
|
630
809
|
raise HTTPException(status_code=404, detail="대화를 찾을 수 없습니다.")
|
|
631
810
|
return {"id": conversation_id, "messages": messages}
|
|
632
|
-
|
|
811
|
+
|
|
633
812
|
|
|
634
813
|
@api_router.delete("/history/conversations/{conversation_id:path}")
|
|
635
814
|
async def delete_history_conversation(conversation_id: str, request: Request):
|
|
@@ -645,7 +824,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
645
824
|
kept=result.get("kept", 0),
|
|
646
825
|
)
|
|
647
826
|
return result
|
|
648
|
-
|
|
827
|
+
|
|
649
828
|
|
|
650
829
|
@api_router.delete("/history")
|
|
651
830
|
async def delete_history(request: Request, keep_last: int = 0):
|
|
@@ -659,7 +838,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
659
838
|
kept=result.get("kept", 0),
|
|
660
839
|
)
|
|
661
840
|
return result
|
|
662
|
-
|
|
841
|
+
|
|
663
842
|
@api_router.get("/history/search")
|
|
664
843
|
async def search_history(q: str, request: Request):
|
|
665
844
|
"""키워드로 채팅 히스토리를 검색합니다."""
|
|
@@ -676,7 +855,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
676
855
|
grouped[cid] = {"conversation_id": cid, "title": conversation_title(item), "messages": []}
|
|
677
856
|
grouped[cid]["messages"].append(item)
|
|
678
857
|
return {"results": list(grouped.values())[-30:], "query": q}
|
|
679
|
-
|
|
858
|
+
|
|
680
859
|
async def _stream_chat(
|
|
681
860
|
req: ChatRequest,
|
|
682
861
|
context: str = "",
|
|
@@ -696,7 +875,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
696
875
|
clean_chunk = chunk.split("text='")[1].split("', token=")[0].replace('\\n', '\n').replace('\\\\n', '\n')
|
|
697
876
|
except Exception:
|
|
698
877
|
pass
|
|
699
|
-
|
|
878
|
+
|
|
700
879
|
full_response += str(clean_chunk)
|
|
701
880
|
yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
|
|
702
881
|
history_user = get_history_user(effective_email or req.user_email, req.user_nickname)
|
|
@@ -724,7 +903,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
724
903
|
schema_path = skill_dir / "schema.json"
|
|
725
904
|
if not schema_path.exists():
|
|
726
905
|
raise HTTPException(404, detail=f"Skill '{req.skill}' not found or missing schema.json")
|
|
727
|
-
|
|
906
|
+
|
|
728
907
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
729
908
|
eval_cases = schema.get("evals", [])
|
|
730
909
|
if req.case_id:
|
|
@@ -732,7 +911,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
732
911
|
if not eval_cases:
|
|
733
912
|
return {"skill": req.skill, "total": 0, "passed": 0, "failed": 0, "results": [],
|
|
734
913
|
"message": "No eval cases defined in schema.json"}
|
|
735
|
-
|
|
914
|
+
|
|
736
915
|
action_name = schema.get("action", req.skill)
|
|
737
916
|
results = []
|
|
738
917
|
for case in eval_cases:
|
|
@@ -754,19 +933,19 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
754
933
|
results.append({"id": case_id, "description": case.get("description", ""),
|
|
755
934
|
"passed": False, "error": str(exc),
|
|
756
935
|
"pass_criteria": case.get("pass_criteria", "")})
|
|
757
|
-
|
|
936
|
+
|
|
758
937
|
n_passed = sum(1 for r in results if r.get("passed") is True)
|
|
759
938
|
return {
|
|
760
939
|
"skill": req.skill, "action": action_name,
|
|
761
940
|
"total": len(results), "passed": n_passed, "failed": len(results) - n_passed,
|
|
762
941
|
"results": results,
|
|
763
942
|
}
|
|
764
|
-
|
|
943
|
+
|
|
765
944
|
|
|
766
945
|
@api_router.post("/agent")
|
|
767
946
|
async def agent(req: AgentRequest, request: Request):
|
|
768
947
|
"""Natural-language local agent.
|
|
769
|
-
|
|
948
|
+
|
|
770
949
|
State machine:
|
|
771
950
|
IDLE → PLANNING → WAITING_APPROVAL → EXECUTING → VERIFYING
|
|
772
951
|
↓ ↓
|
|
@@ -779,22 +958,22 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
779
958
|
req.workspace_id = req.workspace_id or workspace_scope_from_request(request)
|
|
780
959
|
if not router.current_model_id:
|
|
781
960
|
raise HTTPException(status_code=400, detail="No model loaded. Call /models/load first.")
|
|
782
|
-
|
|
961
|
+
|
|
783
962
|
ensure_agent_root()
|
|
784
963
|
lang = detect_language(req.message)
|
|
785
964
|
lang_hint = _LANG_HINT[lang]
|
|
786
965
|
max_steps = max(1, min(req.max_steps, 50))
|
|
787
966
|
max_retry = 3
|
|
788
|
-
|
|
967
|
+
|
|
789
968
|
ctx = AgentRunContext()
|
|
790
969
|
ctx.executing_model = req.executing_model
|
|
791
970
|
ctx.reviewing_model = req.reviewing_model
|
|
792
|
-
|
|
971
|
+
|
|
793
972
|
# PLANNING phase
|
|
794
973
|
ctx.state = AgentState.PLANNING
|
|
795
974
|
ctx.state_history.append(ctx.state.value)
|
|
796
975
|
await _AGENT_RUNTIME.plan(ctx, req, lang_hint, current_user, model_id=req.planning_model)
|
|
797
|
-
|
|
976
|
+
|
|
798
977
|
# Human-in-the-loop: pause after planning, return plan to UI
|
|
799
978
|
if req.human_in_loop:
|
|
800
979
|
context_id = secrets.token_urlsafe(16)
|
|
@@ -810,11 +989,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
810
989
|
"executing_model": req.executing_model or router.current_model_id,
|
|
811
990
|
"reviewing_model": req.reviewing_model or router.current_model_id,
|
|
812
991
|
}
|
|
813
|
-
|
|
992
|
+
|
|
814
993
|
# Auto-approve and run to completion (default behaviour)
|
|
815
994
|
_AGENT_RUNTIME.approve(ctx, current_user)
|
|
816
995
|
return await _agent_finish(ctx, req, lang_hint, current_user, max_steps, max_retry)
|
|
817
|
-
|
|
996
|
+
|
|
818
997
|
|
|
819
998
|
async def _agent_finish(
|
|
820
999
|
ctx: AgentRunContext, req: AgentRequest, lang_hint: str,
|
|
@@ -823,7 +1002,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
823
1002
|
"""HTTP glue: drive the runtime to a terminal state, persist, shape the response."""
|
|
824
1003
|
await _AGENT_RUNTIME.run_to_completion(ctx, req, lang_hint, current_user, max_steps, max_retry)
|
|
825
1004
|
asyncio.create_task(_AGENT_RUNTIME.memory_update(ctx, req, current_user))
|
|
826
|
-
|
|
1005
|
+
|
|
827
1006
|
message = ctx.final_message or "작업을 완료했습니다."
|
|
828
1007
|
save_to_history("user", req.message, source=req.source or "web", conversation_id=req.conversation_id, workspace_id=req.workspace_id)
|
|
829
1008
|
save_to_history("assistant", message, source=req.source or "web", conversation_id=req.conversation_id, workspace_id=req.workspace_id)
|
|
@@ -850,33 +1029,33 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
850
1029
|
"final_state": ctx.state.value,
|
|
851
1030
|
"created_files": created_files,
|
|
852
1031
|
}
|
|
853
|
-
|
|
1032
|
+
|
|
854
1033
|
|
|
855
1034
|
@api_router.post("/agent/resume")
|
|
856
1035
|
async def agent_resume(req: AgentResumeRequest, request: Request):
|
|
857
1036
|
"""Resume a paused agent after human approval of the plan."""
|
|
858
1037
|
current_user = require_user(request)
|
|
859
|
-
|
|
1038
|
+
|
|
860
1039
|
with _pending_agents_lock:
|
|
861
1040
|
entry = _pending_agents.pop(req.context_id, None)
|
|
862
1041
|
if not entry:
|
|
863
1042
|
raise HTTPException(status_code=404, detail="Agent context not found or expired. Start a new request.")
|
|
864
|
-
|
|
1043
|
+
|
|
865
1044
|
ctx, orig_req, lang_hint, _orig_user = entry
|
|
866
|
-
|
|
1045
|
+
|
|
867
1046
|
if not req.approved:
|
|
868
1047
|
return {"status": "cancelled", "response": "사용자가 계획을 취소했습니다."}
|
|
869
|
-
|
|
1048
|
+
|
|
870
1049
|
if req.modified_plan:
|
|
871
1050
|
ctx.plan = req.modified_plan
|
|
872
1051
|
ctx.transcript[-1].update(ctx.plan) # keep transcript in sync
|
|
873
|
-
|
|
1052
|
+
|
|
874
1053
|
# Apply model overrides from resume request (takes priority over original request)
|
|
875
1054
|
ctx.executing_model = req.executing_model or ctx.executing_model
|
|
876
1055
|
ctx.reviewing_model = req.reviewing_model or ctx.reviewing_model
|
|
877
|
-
|
|
1056
|
+
|
|
878
1057
|
_AGENT_RUNTIME.approve(ctx, current_user)
|
|
879
|
-
|
|
1058
|
+
|
|
880
1059
|
max_steps = max(1, min(orig_req.max_steps, 50))
|
|
881
1060
|
max_retry = 3
|
|
882
1061
|
return await _agent_finish(ctx, orig_req, lang_hint, current_user, max_steps, max_retry)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Managed legacy compatibility surface for legacy import shims.
|
|
2
2
|
|
|
3
3
|
Compatibility modules are intentionally still present for old scripts, VS Code
|
|
4
|
-
extension paths, historical integrations, and pre-graph-package imports. 8.
|
|
4
|
+
extension paths, historical integrations, and pre-graph-package imports. 8.4.0
|
|
5
5
|
stops treating them as vague technical debt: every tracked shim has an owner,
|
|
6
6
|
migration target, removal phase, and replacement import that can be surfaced in
|
|
7
7
|
docs and tests.
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, List
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
LEGACY_COMPATIBILITY_VERSION = "8.
|
|
17
|
+
LEGACY_COMPATIBILITY_VERSION = "8.4.0"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass(frozen=True)
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
"remove_skill_directory",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
WORKSPACE_OS_VERSION = "8.
|
|
52
|
+
WORKSPACE_OS_VERSION = "8.4.0"
|
|
53
53
|
|
|
54
54
|
# Workspace types separate single-user Personal workspaces from shared
|
|
55
55
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Machine-checkable architecture readiness gates for release work.
|
|
2
2
|
|
|
3
|
-
8.
|
|
3
|
+
8.4.0 keeps the major architecture priorities under an explicit release
|
|
4
4
|
contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
|
|
5
5
|
central Config, decomposed server runtime, and Knowledge Graph stabilization
|
|
6
6
|
must remain discoverable, ordered, and backed by tests before the release can be
|
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
|
|
|
17
17
|
from latticeai.core.legacy_compatibility import legacy_shim_report
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
ARCHITECTURE_VERSION_TARGET = "8.
|
|
20
|
+
ARCHITECTURE_VERSION_TARGET = "8.4.0"
|
|
21
21
|
|
|
22
22
|
PREFERRED_REFACTORING_ORDER = [
|
|
23
23
|
"agent-runtime",
|
|
@@ -102,6 +102,12 @@ MODEL_ENGINE_ALIASES = {
|
|
|
102
102
|
"lmstudio": "ggml-org/gemma-4-12B-it-GGUF",
|
|
103
103
|
"llamacpp": "ggml-org/gemma-4-12B-it-GGUF",
|
|
104
104
|
},
|
|
105
|
+
"gemma-4-26b-it-4bit": {
|
|
106
|
+
"local_mlx": "mlx-community/gemma-4-26b-a4b-it-4bit",
|
|
107
|
+
},
|
|
108
|
+
"mlx-community/gemma-4-26b-a4b-it-4bit": {
|
|
109
|
+
"local_mlx": "mlx-community/gemma-4-26b-a4b-it-4bit",
|
|
110
|
+
},
|
|
105
111
|
"gemma-4-31b-it-4bit": {
|
|
106
112
|
"local_mlx": "mlx-community/gemma-4-31b-it-4bit",
|
|
107
113
|
"ollama": "hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M",
|
|
@@ -31,6 +31,11 @@ from latticeai.models.router import (
|
|
|
31
31
|
parse_model_ref,
|
|
32
32
|
)
|
|
33
33
|
from latticeai.core.model_resolution import ModelResolution as _ModelResolution
|
|
34
|
+
from latticeai.core.model_compat import (
|
|
35
|
+
friendly_model_runtime_error as _friendly_model_runtime_error,
|
|
36
|
+
model_runtime_compatibility as _model_runtime_compatibility,
|
|
37
|
+
SMOKE_PROMPT as _SMOKE_PROMPT,
|
|
38
|
+
)
|
|
34
39
|
from .model_engines import (
|
|
35
40
|
ensure_lmstudio_server as _ensure_lmstudio_server,
|
|
36
41
|
ensure_ollama_server as _ensure_ollama_server,
|
|
@@ -50,6 +55,14 @@ from .model_engines import (
|
|
|
50
55
|
LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
|
|
51
56
|
)
|
|
52
57
|
|
|
58
|
+
# ``model_loading._get_model_runtime_deps`` imports these private names from
|
|
59
|
+
# this module to preserve the historical model_runtime wiring surface.
|
|
60
|
+
_MODEL_LOADING_COMPAT_EXPORTS = (
|
|
61
|
+
_friendly_model_runtime_error,
|
|
62
|
+
_model_runtime_compatibility,
|
|
63
|
+
_SMOKE_PROMPT,
|
|
64
|
+
)
|
|
65
|
+
|
|
53
66
|
|
|
54
67
|
def _missing_current_user(_request: Request) -> Optional[str]:
|
|
55
68
|
return None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Machine-checkable *product* readiness gates for the 8.
|
|
1
|
+
"""Machine-checkable *product* readiness gates for the 8.4 line.
|
|
2
2
|
|
|
3
3
|
Where ``architecture_readiness`` proves the internal structure is sound, this
|
|
4
|
-
module answers the product question the 8.
|
|
4
|
+
module answers the product question the 8.4 release exists to settle: *does the
|
|
5
5
|
app now feel like a finished product rather than only a strong framework?*
|
|
6
6
|
It does so honestly: every gate is backed by
|
|
7
7
|
evidence that is probed on disk, so a gate only reports ``complete`` when its
|
|
@@ -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 = "8.
|
|
21
|
+
PRODUCT_VERSION_TARGET = "8.4.0"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass(frozen=True)
|
|
@@ -52,6 +52,15 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
52
52
|
"scripts/brain_quality_eval.py",
|
|
53
53
|
],
|
|
54
54
|
),
|
|
55
|
+
ProductGate(
|
|
56
|
+
id="action-aware-chat",
|
|
57
|
+
title="File action requests create artifacts instead of code-only answers",
|
|
58
|
+
evidence=[
|
|
59
|
+
"latticeai/api/chat.py::is_file_action_request",
|
|
60
|
+
"latticeai/api/chat.py::direct_write_file",
|
|
61
|
+
"tests/unit/test_chat_telegram_decoupling.py::test_chat_file_creation_intent_writes_real_file",
|
|
62
|
+
],
|
|
63
|
+
),
|
|
55
64
|
ProductGate(
|
|
56
65
|
id="local-first-trust",
|
|
57
66
|
title="Local-first privacy is stated and bounded",
|
|
@@ -67,10 +76,10 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
67
76
|
evidence=[
|
|
68
77
|
"package.json::release:artifacts",
|
|
69
78
|
"package.json::release:validate",
|
|
70
|
-
"README.md::dist/ltcai-8.
|
|
71
|
-
"README.md::dist/ltcai-8.
|
|
72
|
-
"README.md::dist/ltcai-8.
|
|
73
|
-
"README.md::ltcai-8.
|
|
79
|
+
"README.md::dist/ltcai-8.4.0-py3-none-any.whl",
|
|
80
|
+
"README.md::dist/ltcai-8.4.0.tar.gz",
|
|
81
|
+
"README.md::dist/ltcai-8.4.0.vsix",
|
|
82
|
+
"README.md::ltcai-8.4.0.tgz",
|
|
74
83
|
"scripts/validate_release_artifacts.py",
|
|
75
84
|
"scripts/release_smoke.py",
|
|
76
85
|
"Dockerfile",
|
|
@@ -86,12 +95,12 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
86
95
|
title="Release story is documented and honest",
|
|
87
96
|
evidence=[
|
|
88
97
|
"README.md",
|
|
89
|
-
"README.md::The current release is **8.
|
|
90
|
-
"SECURITY.md::8.
|
|
91
|
-
"vscode-extension/README.md::**8.
|
|
92
|
-
"docs/CHANGELOG.md::## [8.
|
|
98
|
+
"README.md::The current release is **8.4.0",
|
|
99
|
+
"SECURITY.md::8.4.x (latest)",
|
|
100
|
+
"vscode-extension/README.md::**8.4.0",
|
|
101
|
+
"docs/CHANGELOG.md::## [8.4.0]",
|
|
93
102
|
"FEATURE_STATUS.md",
|
|
94
|
-
"RELEASE_NOTES_v8.
|
|
103
|
+
"RELEASE_NOTES_v8.4.0.md",
|
|
95
104
|
"latticeai/core/agent.py::SingleAgentRuntime",
|
|
96
105
|
"latticeai/core/agent.py::AgentRuntime = SingleAgentRuntime",
|
|
97
106
|
"lattice_brain/runtime/contracts.py::runtime-boundary/v1",
|
|
@@ -106,7 +115,7 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
106
115
|
id="ecosystem-path",
|
|
107
116
|
title="Community and plugin growth path is explicit",
|
|
108
117
|
evidence=[
|
|
109
|
-
"docs/COMMUNITY_AND_PLUGINS.md::LatticeAI 8.
|
|
118
|
+
"docs/COMMUNITY_AND_PLUGINS.md::LatticeAI 8.4.0",
|
|
110
119
|
"docs/PLUGIN_SDK.md",
|
|
111
120
|
"plugins/README.md",
|
|
112
121
|
"plugins/hello-world/plugin.json",
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
package/static/sw.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Lattice Service Worker — PWA install + offline shell for the /app SPA.
|
|
2
2
|
// Strategy: precache the Vite app bundle from its asset manifest,
|
|
3
3
|
// cache-first for static assets, network-only for everything dynamic.
|
|
4
|
-
const CACHE = "lattice-
|
|
4
|
+
const CACHE = "lattice-v840";
|
|
5
5
|
const MANIFEST_URL = "/static/app/asset-manifest.json";
|
|
6
6
|
|
|
7
7
|
// Non-manifest assets the shell needs offline.
|