ltcai 8.0.0 → 8.1.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 +32 -50
- package/docs/CHANGELOG.md +14 -0
- package/docs/V3_FRONTEND.md +11 -5
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +9 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/app_factory.py +13 -134
- package/latticeai/core/local_embeddings.py +8 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +140 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +6 -5
- package/latticeai/services/model_runtime.py +36 -2
- package/latticeai/services/product_readiness.py +18 -18
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +11 -2
- 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 +11 -102
- package/static/app/assets/Act-BOO66G-c.js +1 -0
- package/static/app/assets/Brain-C6lEYiD7.js +321 -0
- package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
- package/static/app/assets/Library-DK4FIp8a.js +1 -0
- package/static/app/assets/System-Bgs6Ql7x.js +1 -0
- package/static/app/assets/core-CwxXejkd.js +1 -2
- package/static/app/assets/index-Bvv79nre.js +16 -0
- package/static/app/assets/index-Dslqglia.css +2 -0
- package/static/app/assets/primitives-B70WOra0.js +1 -0
- package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
- package/static/app/index.html +3 -10
- package/static/app/theme-boot.js +8 -0
- package/static/sw.js +1 -1
- package/frontend/index.html +0 -24
- package/frontend/openapi.json +0 -15425
- package/frontend/src/App.tsx +0 -243
- package/frontend/src/api/client.ts +0 -580
- package/frontend/src/api/openapi.ts +0 -17892
- package/frontend/src/components/AdminAccessGate.tsx +0 -70
- package/frontend/src/components/FeedbackState.tsx +0 -45
- package/frontend/src/components/LanguageSwitcher.tsx +0 -23
- package/frontend/src/components/LivingBrain.tsx +0 -220
- package/frontend/src/components/ProductFlow.tsx +0 -171
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
- package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
- package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
- package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
- package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
- package/frontend/src/components/primitives.tsx +0 -392
- package/frontend/src/components/ui/badge.tsx +0 -27
- package/frontend/src/components/ui/button.tsx +0 -37
- package/frontend/src/components/ui/card.tsx +0 -22
- package/frontend/src/components/ui/input.tsx +0 -16
- package/frontend/src/components/ui/textarea.tsx +0 -16
- package/frontend/src/features/admin/AdminConsole.tsx +0 -334
- package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
- package/frontend/src/features/brain/BrainComposer.tsx +0 -85
- package/frontend/src/features/brain/BrainConversation.tsx +0 -688
- package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
- package/frontend/src/features/brain/BrainHome.tsx +0 -624
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
- package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
- package/frontend/src/features/brain/brainData.ts +0 -246
- package/frontend/src/features/brain/graphLayout.ts +0 -37
- package/frontend/src/features/brain/types.ts +0 -150
- package/frontend/src/features/review/ReviewCard.tsx +0 -100
- package/frontend/src/features/review/ReviewInbox.tsx +0 -127
- package/frontend/src/features/review/reviewHelpers.ts +0 -69
- package/frontend/src/i18n.ts +0 -1303
- package/frontend/src/lib/utils.ts +0 -33
- package/frontend/src/main.tsx +0 -23
- package/frontend/src/pages/Act.tsx +0 -458
- package/frontend/src/pages/Ask.tsx +0 -14
- package/frontend/src/pages/Brain.tsx +0 -914
- package/frontend/src/pages/Capture.tsx +0 -258
- package/frontend/src/pages/Library.tsx +0 -486
- package/frontend/src/pages/System.tsx +0 -621
- package/frontend/src/routes.ts +0 -92
- package/frontend/src/store/appStore.ts +0 -94
- package/frontend/src/styles.css +0 -6579
- package/static/app/assets/Act-DOvf59ru.js +0 -2
- package/static/app/assets/Act-DOvf59ru.js.map +0 -1
- package/static/app/assets/Brain-C7_0mEiI.js +0 -322
- package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
- package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
- package/static/app/assets/Library-BJPEEm5O.js +0 -2
- package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
- package/static/app/assets/System-D6t9jo9V.js +0 -2
- package/static/app/assets/System-D6t9jo9V.js.map +0 -1
- package/static/app/assets/core-CwxXejkd.js.map +0 -1
- package/static/app/assets/index-C7g26IF6.css +0 -2
- package/static/app/assets/index-DbcEYJQ2.js +0 -17
- package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
- package/static/app/assets/primitives-CD38lt4n.js +0 -2
- package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
- package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
package/README.md
CHANGED
|
@@ -64,9 +64,9 @@ You need Lattice AI when:
|
|
|
64
64
|
|
|
65
65
|
## Living Brain Flow
|
|
66
66
|
|
|
67
|
-
The screenshots below are release evidence captures. The 8.
|
|
68
|
-
|
|
69
|
-
composer are
|
|
67
|
+
The screenshots below are release evidence captures. The 8.1 line keeps the
|
|
68
|
+
first-run flow and refreshes Brain Home so the living Brain, recent memory,
|
|
69
|
+
next action, and composer are understandable in one glance.
|
|
70
70
|
|
|
71
71
|
### 1. Wake Brain
|
|
72
72
|
|
|
@@ -78,43 +78,36 @@ confirm owner, check the computer, choose the Brain voice.
|
|
|
78
78
|
Choose the owner of the Brain. The profile is not a SaaS account by default; it
|
|
79
79
|
is the local identity for the knowledge you keep.
|
|
80
80
|
|
|
81
|
-

|
|
82
82
|
|
|
83
|
-
### 3.
|
|
84
|
-
|
|
85
|
-
See what kind of local AI experience this computer can support before choosing a
|
|
86
|
-
model.
|
|
87
|
-
|
|
88
|
-

|
|
89
|
-
|
|
90
|
-
### 4. Recommended Models
|
|
83
|
+
### 3. Recommended Models
|
|
91
84
|
|
|
92
85
|
Start with a short list: safest recommendation, faster model, stronger model.
|
|
93
86
|
Advanced details stay available without overwhelming first-time users.
|
|
94
87
|
|
|
95
|
-

|
|
96
89
|
|
|
97
|
-
###
|
|
90
|
+
### 4. Install And Load
|
|
98
91
|
|
|
99
92
|
Download and load only after consent. Lattice explains model size, local
|
|
100
93
|
execution, and network use before work starts.
|
|
101
94
|
|
|
102
|
-

|
|
103
96
|
|
|
104
|
-
###
|
|
97
|
+
### 5. Brain Chat
|
|
105
98
|
|
|
106
99
|
Talk normally. Useful decisions and context become memory, then appear later as
|
|
107
100
|
topics, relationships, graph structure, and the concentric memory rings around
|
|
108
101
|
the Brain.
|
|
109
102
|
|
|
110
|
-

|
|
111
104
|
|
|
112
|
-
###
|
|
105
|
+
### 6. Review Center
|
|
113
106
|
|
|
114
107
|
Automation results are staged for review before they become durable decisions.
|
|
115
108
|
Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
|
|
116
109
|
|
|
117
|
-

|
|
118
111
|
|
|
119
112
|
## Brain Depths
|
|
120
113
|
|
|
@@ -130,10 +123,10 @@ The user travels inward from everyday memory to deeper structure:
|
|
|
130
123
|
|
|
131
124
|
Walkthrough:
|
|
132
125
|
|
|
133
|
-

|
|
134
127
|
|
|
135
128
|
Screenshot index and capture notes:
|
|
136
|
-
[output/release/
|
|
129
|
+
[output/release/v8.1.0/SCREENSHOT_INDEX.md](output/release/v8.1.0/SCREENSHOT_INDEX.md)
|
|
137
130
|
|
|
138
131
|
## Install
|
|
139
132
|
|
|
@@ -210,39 +203,27 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
|
|
|
210
203
|
|
|
211
204
|
## Current Release
|
|
212
205
|
|
|
213
|
-
The current release is **8.
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- The
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
-
|
|
225
|
-
|
|
226
|
-
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
- Product readiness is machine-checkable through `product_readiness()`, which
|
|
230
|
-
composes the architecture contract with exact artifact/docs/workflow evidence.
|
|
231
|
-
- CI and release workflows continue to run frontend lint/typecheck/build gates
|
|
232
|
-
plus the 8.0 product readiness score before a tag can be treated as release-safe.
|
|
233
|
-
|
|
234
|
-
Expected artifacts for 8.0.0 release must use exact filenames:
|
|
235
|
-
|
|
236
|
-
- `dist/ltcai-8.0.0-py3-none-any.whl`
|
|
237
|
-
- `dist/ltcai-8.0.0.tar.gz`
|
|
238
|
-
- `ltcai-8.0.0.tgz`
|
|
239
|
-
- `dist/ltcai-8.0.0.vsix`
|
|
240
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.0.0_aarch64.dmg`
|
|
206
|
+
The current release is **8.1.0 — Intuitive Brain Home**:
|
|
207
|
+
|
|
208
|
+
- Brain Home now opens with the living Brain directly above the conversation composer, so the primary action is visible before users read any utility panels.
|
|
209
|
+
- The previous dashboard-style growth strip is replaced by a product narrative: Brain state, what was just remembered, the strongest connected topic, and the next best action.
|
|
210
|
+
- Primary actions are explicit and click-tested: talk to Brain, add sources, or open the memory graph.
|
|
211
|
+
- Mobile and narrow layouts keep the Brain and composer in the first viewport, avoiding the previous clipped/overcrowded first screen.
|
|
212
|
+
- The 8.0 runtime architecture contract remains active: AgentRuntime, ToolRegistry, Config, server decomposition, and KG hardening stay machine-checkable through readiness gates.
|
|
213
|
+
- CI and release workflows continue to run frontend lint/typecheck/build gates plus product readiness before a tag can be treated as release-safe.
|
|
214
|
+
|
|
215
|
+
Expected artifacts for 8.1.0 release must use exact filenames:
|
|
216
|
+
|
|
217
|
+
- `dist/ltcai-8.1.0-py3-none-any.whl`
|
|
218
|
+
- `dist/ltcai-8.1.0.tar.gz`
|
|
219
|
+
- `ltcai-8.1.0.tgz`
|
|
220
|
+
- `dist/ltcai-8.1.0.vsix`
|
|
221
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.1.0_aarch64.dmg`
|
|
241
222
|
|
|
242
223
|
Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
|
|
243
224
|
|
|
244
225
|
See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
|
|
245
|
-
strategic roadmap slices applied through 8.
|
|
226
|
+
strategic roadmap slices applied through 8.1.0 and the follow-up tracks.
|
|
246
227
|
|
|
247
228
|
## Known Limitations
|
|
248
229
|
|
|
@@ -259,6 +240,7 @@ strategic roadmap slices applied through 8.0.0 and the follow-up tracks.
|
|
|
259
240
|
|
|
260
241
|
| Version | Theme |
|
|
261
242
|
| --- | --- |
|
|
243
|
+
| 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 |
|
|
262
244
|
| 8.0.0 | Runtime Architecture Contract: AgentRuntime, ToolRegistry, central Config, server decomposition, and KG hardening are captured as machine-checkable release boundaries with exact 8.0.0 artifacts |
|
|
263
245
|
| 7.9.0 | Agent Runtime Boundary Hardening: explicit `SingleAgentRuntime`, compatibility alias preservation, injected rollback port, and release/readiness docs aligned to the product AgentRuntime facade |
|
|
264
246
|
| 7.8.0 | Brain Chat Home UX Simplification: chat-first first viewport, visible workspace navigation, collapsed source/status utilities, hidden default depth controls, and removal of obsolete Brain UX components |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ existed at that release.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [8.1.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Rebuilt Brain Home around an intuitive first screen with LivingBrain, recent
|
|
13
|
+
memory, connected topic, next-best action, and the composer visible together.
|
|
14
|
+
- Replaced dashboard-style Brain growth metrics with narrative, product-facing
|
|
15
|
+
copy and focused primary actions.
|
|
16
|
+
- Tightened mobile and narrow viewport behavior so the Brain and composer remain
|
|
17
|
+
visible without horizontal overflow.
|
|
18
|
+
- Refreshed release screenshots, walkthrough GIF/WebM, static app assets, and
|
|
19
|
+
exact 8.1.0 artifact metadata.
|
|
20
|
+
- Synchronized Python, npm, VS Code extension, Tauri, workspace, readiness, and
|
|
21
|
+
current-release documentation versions to 8.1.0.
|
|
22
|
+
|
|
9
23
|
## [8.0.0] - 2026-06-24
|
|
10
24
|
|
|
11
25
|
### Changed
|
package/docs/V3_FRONTEND.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Lattice AI v3 — Frontend Product Shell
|
|
2
2
|
|
|
3
|
+
> Historical v3 shell notes. The current product shell is the React/Vite app
|
|
4
|
+
> built from `frontend/` into `static/app/` with `npm run build:assets`; current
|
|
5
|
+
> linting uses `scripts/lint_frontend.mjs`.
|
|
6
|
+
|
|
3
7
|
> A token-native single-page workspace shell for Lattice AI v3. It ships as the
|
|
4
8
|
> primary product experience and calls the real v3 retrieval/chat APIs while
|
|
5
9
|
> rendering clear unavailable states for local services that are not running.
|
|
6
10
|
|
|
7
11
|
Entry point: **`/app`** (served by `latticeai/api/static_routes.py` →
|
|
12
|
+
the current `static/app/index.html` bundle; the historical v3 shell used
|
|
8
13
|
`static/v3/index.html`). Login, auto-login after registration, SSO callback, and
|
|
9
14
|
the PWA manifest land on `/app`. Legacy multi-page static screens have been
|
|
10
15
|
deleted; compatibility routes (`/workspace`, `/chat`, `/graph`, `/admin`, …)
|
|
@@ -27,7 +32,8 @@ Deep-linking into an `admin/*` route auto-promotes the shell to Admin mode.
|
|
|
27
32
|
|
|
28
33
|
## Information architecture
|
|
29
34
|
|
|
30
|
-
One declarative table (`static/v3/js/core/routes.js
|
|
35
|
+
One declarative table (historically `static/v3/js/core/routes.js`, now
|
|
36
|
+
represented by the React routes under `frontend/src/`) drives the nav rail, the
|
|
31
37
|
command palette, the router, breadcrumbs, and lazy view loading.
|
|
32
38
|
|
|
33
39
|
```
|
|
@@ -50,8 +56,8 @@ Administration Users · Permissions · Audit Logs · Security · Policies · Pri
|
|
|
50
56
|
Token-native, layered on the existing **single color source**
|
|
51
57
|
(`static/css/tokens.css`, `data-lt-theme` light/dark). No legacy override layers
|
|
52
58
|
(`responsive.css` / `workspace.css` / `platform.css`) are loaded by v3.
|
|
53
|
-
Runtime assets are loaded through `static/
|
|
54
|
-
step writes hashed CSS/JS
|
|
59
|
+
Runtime assets are loaded through `static/app/asset-manifest.json`; the build
|
|
60
|
+
step writes hashed CSS/JS assets and `/app` reads the manifest instead of
|
|
55
61
|
using manual `?v=` query strings.
|
|
56
62
|
|
|
57
63
|
| File | Responsibility |
|
|
@@ -133,8 +139,8 @@ focus rings, skip link, keyboard command palette (⌘K / Ctrl-K).
|
|
|
133
139
|
|
|
134
140
|
## Validation
|
|
135
141
|
|
|
136
|
-
- `npm run lint` (
|
|
137
|
-
- `npm run build:assets` (writes hashed
|
|
142
|
+
- `npm run lint` (covers the current frontend through `scripts/lint_frontend.mjs`).
|
|
143
|
+
- `npm run build:assets` (writes hashed `static/app` assets and `asset-manifest.json`).
|
|
138
144
|
- `npm run test:visual` (`tests/visual/v3.spec.js` against the mock server, which
|
|
139
145
|
serves `/app` and mocks the future API surfaces).
|
|
140
146
|
- Browser-rendered smoke checks of every route in light and dark themes.
|
|
@@ -15,6 +15,10 @@ DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
|
|
|
15
15
|
EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def embedding_model_id(dim: int) -> str:
|
|
19
|
+
return f"lattice-local-hash-v1:{int(dim)}"
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
def _tokenize(text: str) -> List[str]:
|
|
19
23
|
raw = str(text or "").lower()
|
|
20
24
|
tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
|
|
@@ -49,6 +53,10 @@ class LocalEmbeddingModel:
|
|
|
49
53
|
dim: int = DEFAULT_EMBEDDING_DIM
|
|
50
54
|
model_id: str = EMBEDDING_MODEL_ID
|
|
51
55
|
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
|
|
58
|
+
object.__setattr__(self, "model_id", embedding_model_id(self.dim))
|
|
59
|
+
|
|
52
60
|
def embed(self, text: str) -> List[float]:
|
|
53
61
|
vector = [0.0] * self.dim
|
|
54
62
|
features = _tokenize(text)
|
|
@@ -79,4 +87,4 @@ class LocalEmbeddingModel:
|
|
|
79
87
|
return list(struct.unpack(f"<{count}f", payload[: count * 4]))
|
|
80
88
|
|
|
81
89
|
|
|
82
|
-
__all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel"]
|
|
90
|
+
__all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel", "embedding_model_id"]
|
|
@@ -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.1.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/app_factory.py
CHANGED
|
@@ -77,8 +77,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
77
77
|
import re
|
|
78
78
|
import secrets
|
|
79
79
|
import threading
|
|
80
|
-
import subprocess
|
|
81
|
-
import sys
|
|
82
80
|
import time
|
|
83
81
|
from pathlib import Path
|
|
84
82
|
|
|
@@ -195,8 +193,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
195
193
|
)
|
|
196
194
|
from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF # noqa: F401
|
|
197
195
|
from latticeai.core.mcp_registry import (
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
_fetch_skills_marketplace,
|
|
197
|
+
install_skill,
|
|
198
|
+
SKILLS_DIR,
|
|
199
|
+
create_mcp_install_state,
|
|
200
200
|
)
|
|
201
201
|
from latticeai.services.p_reinforce import PReinforceGardener
|
|
202
202
|
from setup_wizard import get_recommendations, scan_environment
|
|
@@ -322,6 +322,15 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
322
322
|
MCP_FILE = DATA_DIR / "mcp_installs.json"
|
|
323
323
|
AUDIT_FILE = DATA_DIR / "audit_log.json"
|
|
324
324
|
SSO_FILE = DATA_DIR / "sso_config.json"
|
|
325
|
+
|
|
326
|
+
# MCP state extracted to mcp_registry.create_mcp_install_state (server decomp)
|
|
327
|
+
_mcp_state = create_mcp_install_state(DATA_DIR)
|
|
328
|
+
load_mcp_installs = _mcp_state["load_mcp_installs"]
|
|
329
|
+
save_mcp_installs = _mcp_state["save_mcp_installs"]
|
|
330
|
+
mcp_public_item = _mcp_state["mcp_public_item"]
|
|
331
|
+
recommend_mcps = _mcp_state["recommend_mcps"]
|
|
332
|
+
install_mcp = _mcp_state["install_mcp"]
|
|
333
|
+
|
|
325
334
|
# Resolve the configured embedding provider once at startup. Degrades to the
|
|
326
335
|
# offline hash fallback when the requested provider is unavailable, while
|
|
327
336
|
# recording the requested-vs-active provider for the Embeddings status surface.
|
|
@@ -546,136 +555,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
546
555
|
with open(VPC_FILE, "w", encoding="utf-8") as f:
|
|
547
556
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
548
557
|
|
|
549
|
-
def load_mcp_installs() -> Dict:
|
|
550
|
-
if not os.path.exists(MCP_FILE):
|
|
551
|
-
return {"installed": {}, "updated_at": None}
|
|
552
|
-
try:
|
|
553
|
-
with open(MCP_FILE, "r", encoding="utf-8") as f:
|
|
554
|
-
data = json.load(f)
|
|
555
|
-
if "installed" not in data:
|
|
556
|
-
data["installed"] = {}
|
|
557
|
-
return data
|
|
558
|
-
except Exception as e:
|
|
559
|
-
logging.warning("load_mcp_installs failed: %s", e)
|
|
560
|
-
return {"installed": {}, "updated_at": None}
|
|
561
|
-
|
|
562
|
-
def save_mcp_installs(data: Dict):
|
|
563
|
-
data["updated_at"] = datetime.now().isoformat()
|
|
564
|
-
with open(MCP_FILE, "w", encoding="utf-8") as f:
|
|
565
|
-
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
566
|
-
|
|
567
|
-
def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
|
|
568
|
-
state = installed_state.get(item["id"]) or {}
|
|
569
|
-
installed = item["install_mode"] in {"builtin", "bundled"} or bool(state.get("installed"))
|
|
570
|
-
connector_pending = item["install_mode"] == "connector" and not state.get("authenticated")
|
|
571
|
-
authenticated = item["install_mode"] != "connector" or bool(state.get("authenticated"))
|
|
572
|
-
return {
|
|
573
|
-
"id": item["id"],
|
|
574
|
-
"name": item["name"],
|
|
575
|
-
"category": item.get("category", ""),
|
|
576
|
-
"install_mode": item["install_mode"],
|
|
577
|
-
"description": item.get("description", ""),
|
|
578
|
-
"capabilities": item.get("capabilities", []),
|
|
579
|
-
"connector_url": item.get("connector_url"),
|
|
580
|
-
"external_url": item.get("external_url"),
|
|
581
|
-
"package": item.get("package"),
|
|
582
|
-
"homepage": item.get("homepage"),
|
|
583
|
-
"source": item.get("source", "local"),
|
|
584
|
-
"installed": installed,
|
|
585
|
-
"status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
|
|
586
|
-
"authenticated": authenticated,
|
|
587
|
-
"updated_at": state.get("updated_at"),
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
|
|
591
|
-
text = (query or "").lower()
|
|
592
|
-
installed = load_mcp_installs().get("installed", {})
|
|
593
|
-
registry = await _get_combined_registry()
|
|
594
|
-
scored = []
|
|
595
|
-
for item in registry:
|
|
596
|
-
score = 0
|
|
597
|
-
hits = []
|
|
598
|
-
for keyword in item.get("keywords", []):
|
|
599
|
-
if keyword.lower() in text:
|
|
600
|
-
score += 3 if len(keyword) > 2 else 1
|
|
601
|
-
hits.append(keyword)
|
|
602
|
-
# description 키워드 매칭 (remote 항목 보완)
|
|
603
|
-
if not hits and text:
|
|
604
|
-
desc_words = item.get("description", "").lower().split()
|
|
605
|
-
for word in text.split():
|
|
606
|
-
if len(word) > 2 and word in desc_words:
|
|
607
|
-
score += 1
|
|
608
|
-
hits.append(word)
|
|
609
|
-
if item["id"] == "filesystem" and any(word in text for word in ["만들", "구현", "build", "deploy", "코드", "앱"]):
|
|
610
|
-
score += 2
|
|
611
|
-
if score:
|
|
612
|
-
public = mcp_public_item(item, installed)
|
|
613
|
-
public["score"] = score
|
|
614
|
-
public["matched_keywords"] = hits[:6]
|
|
615
|
-
scored.append(public)
|
|
616
|
-
if not scored:
|
|
617
|
-
fallback_ids = ["filesystem", "browser", "documents"]
|
|
618
|
-
scored = [
|
|
619
|
-
{**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
|
|
620
|
-
for item in registry
|
|
621
|
-
if item["id"] in fallback_ids
|
|
622
|
-
]
|
|
623
|
-
return sorted(scored, key=lambda item: item["score"], reverse=True)[: max(1, min(limit, 24))]
|
|
624
|
-
|
|
625
|
-
async def install_mcp(mcp_id: str) -> Dict:
|
|
626
|
-
registry = await _get_combined_registry()
|
|
627
|
-
item = next((entry for entry in registry if entry["id"] == mcp_id), None)
|
|
628
|
-
if not item:
|
|
629
|
-
raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
|
|
630
|
-
data = load_mcp_installs()
|
|
631
|
-
state = data.setdefault("installed", {})
|
|
632
|
-
status = "active"
|
|
633
|
-
message = "MCP가 활성화되었습니다."
|
|
634
|
-
if item["install_mode"] == "connector":
|
|
635
|
-
status = "needs_auth"
|
|
636
|
-
message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
|
|
637
|
-
elif item["install_mode"] == "pip":
|
|
638
|
-
packages = item.get("pip_packages") or []
|
|
639
|
-
for pkg in packages:
|
|
640
|
-
completed = subprocess.run(
|
|
641
|
-
[sys.executable, "-m", "pip", "install", "--upgrade", pkg],
|
|
642
|
-
capture_output=True, text=True, timeout=900, check=False,
|
|
643
|
-
)
|
|
644
|
-
if completed.returncode != 0:
|
|
645
|
-
raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
|
|
646
|
-
message = f"필수 패키지 설치 완료: {', '.join(packages)}"
|
|
647
|
-
elif item["install_mode"] == "pypi":
|
|
648
|
-
pkg = item.get("package", "")
|
|
649
|
-
version = item.get("package_version")
|
|
650
|
-
pkg_str = f"{pkg}=={version}" if version else pkg
|
|
651
|
-
completed = subprocess.run(
|
|
652
|
-
[sys.executable, "-m", "pip", "install", pkg_str],
|
|
653
|
-
capture_output=True, text=True, timeout=300, check=False,
|
|
654
|
-
)
|
|
655
|
-
if completed.returncode != 0:
|
|
656
|
-
raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
|
|
657
|
-
message = f"pip 패키지 설치 완료: {pkg_str}"
|
|
658
|
-
elif item["install_mode"] == "npm":
|
|
659
|
-
pkg = item.get("package", "")
|
|
660
|
-
version = item.get("package_version")
|
|
661
|
-
pkg_str = f"{pkg}@{version}" if version else pkg
|
|
662
|
-
completed = subprocess.run(
|
|
663
|
-
["npm", "install", "-g", pkg_str],
|
|
664
|
-
capture_output=True, text=True, timeout=300, check=False,
|
|
665
|
-
)
|
|
666
|
-
if completed.returncode != 0:
|
|
667
|
-
raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or f"{pkg} 설치 실패")
|
|
668
|
-
message = f"npm 패키지 설치 완료: {pkg_str}"
|
|
669
|
-
state[mcp_id] = {
|
|
670
|
-
"installed": True,
|
|
671
|
-
"status": status,
|
|
672
|
-
"authenticated": item["install_mode"] != "connector",
|
|
673
|
-
"updated_at": datetime.now().isoformat(),
|
|
674
|
-
}
|
|
675
|
-
save_mcp_installs(data)
|
|
676
|
-
public = mcp_public_item(item, state)
|
|
677
|
-
public["message"] = message
|
|
678
|
-
return public
|
|
679
558
|
|
|
680
559
|
_history_lock = threading.Lock()
|
|
681
560
|
|
|
@@ -23,6 +23,10 @@ DEFAULT_EMBEDDING_DIM = 384
|
|
|
23
23
|
EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def embedding_model_id(dim: int) -> str:
|
|
27
|
+
return f"lattice-local-hash-v1:{int(dim)}"
|
|
28
|
+
|
|
29
|
+
|
|
26
30
|
def _tokenize(text: str) -> List[str]:
|
|
27
31
|
raw = str(text or "").lower()
|
|
28
32
|
tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
|
|
@@ -57,6 +61,10 @@ class LocalEmbeddingModel:
|
|
|
57
61
|
dim: int = DEFAULT_EMBEDDING_DIM
|
|
58
62
|
model_id: str = EMBEDDING_MODEL_ID
|
|
59
63
|
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
|
|
66
|
+
object.__setattr__(self, "model_id", embedding_model_id(self.dim))
|
|
67
|
+
|
|
60
68
|
def embed(self, text: str) -> List[float]:
|
|
61
69
|
vector = [0.0] * self.dim
|
|
62
70
|
features = _tokenize(text)
|
|
@@ -6,9 +6,12 @@ Extracted from server.py to reduce module size.
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
9
12
|
from datetime import datetime, timedelta
|
|
10
13
|
from pathlib import Path
|
|
11
|
-
from typing import Dict, List, Optional
|
|
14
|
+
from typing import Callable, Dict, List, Optional
|
|
12
15
|
|
|
13
16
|
import httpx
|
|
14
17
|
from fastapi import HTTPException
|
|
@@ -789,3 +792,139 @@ async def install_skill(plugin: str, skill: str) -> Dict:
|
|
|
789
792
|
"license": entry["license"],
|
|
790
793
|
"author": entry["author"],
|
|
791
794
|
}
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def create_mcp_install_state(data_dir: Path) -> Dict[str, Callable]:
|
|
798
|
+
"""Return bound MCP state helpers for given data_dir. No global side effects."""
|
|
799
|
+
MCP_FILE = Path(data_dir) / "mcp_installs.json"
|
|
800
|
+
|
|
801
|
+
def load_mcp_installs() -> Dict:
|
|
802
|
+
if not os.path.exists(MCP_FILE):
|
|
803
|
+
return {"installed": {}, "updated_at": None}
|
|
804
|
+
try:
|
|
805
|
+
with open(MCP_FILE, "r", encoding="utf-8") as f:
|
|
806
|
+
data = json.load(f)
|
|
807
|
+
if "installed" not in data:
|
|
808
|
+
data["installed"] = {}
|
|
809
|
+
return data
|
|
810
|
+
except Exception as e:
|
|
811
|
+
logging.warning("load_mcp_installs failed: %s", e)
|
|
812
|
+
return {"installed": {}, "updated_at": None}
|
|
813
|
+
|
|
814
|
+
def save_mcp_installs(data: Dict) -> None:
|
|
815
|
+
data["updated_at"] = datetime.now().isoformat()
|
|
816
|
+
with open(MCP_FILE, "w", encoding="utf-8") as f:
|
|
817
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
818
|
+
|
|
819
|
+
def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
|
|
820
|
+
state = installed_state.get(item.get("id"), {}) or {}
|
|
821
|
+
installed = item.get("install_mode") in {"builtin", "bundled"} or bool(state.get("installed"))
|
|
822
|
+
connector_pending = item.get("install_mode") == "connector" and not state.get("authenticated")
|
|
823
|
+
authenticated = item.get("install_mode") != "connector" or bool(state.get("authenticated"))
|
|
824
|
+
return {
|
|
825
|
+
"id": item.get("id"),
|
|
826
|
+
"name": item.get("name"),
|
|
827
|
+
"category": item.get("category", ""),
|
|
828
|
+
"install_mode": item.get("install_mode"),
|
|
829
|
+
"description": item.get("description", ""),
|
|
830
|
+
"capabilities": item.get("capabilities", []),
|
|
831
|
+
"connector_url": item.get("connector_url"),
|
|
832
|
+
"external_url": item.get("external_url"),
|
|
833
|
+
"package": item.get("package"),
|
|
834
|
+
"homepage": item.get("homepage"),
|
|
835
|
+
"source": item.get("source", "local"),
|
|
836
|
+
"installed": installed,
|
|
837
|
+
"status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
|
|
838
|
+
"authenticated": authenticated,
|
|
839
|
+
"updated_at": state.get("updated_at"),
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
|
|
843
|
+
text = (query or "").lower()
|
|
844
|
+
installed = load_mcp_installs().get("installed", {})
|
|
845
|
+
registry = await _get_combined_registry()
|
|
846
|
+
scored: List[Dict] = []
|
|
847
|
+
for item in registry:
|
|
848
|
+
score = 0
|
|
849
|
+
hits: List[str] = []
|
|
850
|
+
for keyword in item.get("keywords", []):
|
|
851
|
+
if keyword.lower() in text:
|
|
852
|
+
score += 3 if len(keyword) > 2 else 1
|
|
853
|
+
hits.append(keyword)
|
|
854
|
+
if not hits and text:
|
|
855
|
+
desc_words = item.get("description", "").lower().split()
|
|
856
|
+
for word in text.split():
|
|
857
|
+
if len(word) > 2 and word in desc_words:
|
|
858
|
+
score += 1
|
|
859
|
+
hits.append(word)
|
|
860
|
+
if item.get("id") == "filesystem" and any(w in text for w in ["만들", "구현", "build", "deploy", "코드", "앱"]):
|
|
861
|
+
score += 2
|
|
862
|
+
if score:
|
|
863
|
+
public = mcp_public_item(item, installed)
|
|
864
|
+
public["score"] = score
|
|
865
|
+
public["matched_keywords"] = hits[:6]
|
|
866
|
+
scored.append(public)
|
|
867
|
+
if not scored:
|
|
868
|
+
fallback_ids = ["filesystem", "browser", "documents"]
|
|
869
|
+
scored = [
|
|
870
|
+
{**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
|
|
871
|
+
for item in registry if item.get("id") in fallback_ids
|
|
872
|
+
]
|
|
873
|
+
return sorted(scored, key=lambda x: x["score"], reverse=True)[:max(1, min(limit, 24))]
|
|
874
|
+
|
|
875
|
+
async def install_mcp(mcp_id: str) -> Dict:
|
|
876
|
+
registry = await _get_combined_registry()
|
|
877
|
+
item = next((entry for entry in registry if entry.get("id") == mcp_id), None)
|
|
878
|
+
if not item:
|
|
879
|
+
raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
|
|
880
|
+
data = load_mcp_installs()
|
|
881
|
+
state = data.setdefault("installed", {})
|
|
882
|
+
status = "active"
|
|
883
|
+
message = "MCP가 활성화되었습니다."
|
|
884
|
+
if item.get("install_mode") == "connector":
|
|
885
|
+
status = "needs_auth"
|
|
886
|
+
message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
|
|
887
|
+
elif item.get("install_mode") == "pip":
|
|
888
|
+
packages = item.get("pip_packages") or []
|
|
889
|
+
for pkg in packages:
|
|
890
|
+
completed = subprocess.run(
|
|
891
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", pkg],
|
|
892
|
+
capture_output=True, text=True, timeout=900, check=False,
|
|
893
|
+
)
|
|
894
|
+
if completed.returncode != 0:
|
|
895
|
+
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
896
|
+
message = f"필수 패키지 설치 완료: {', '.join(packages)}"
|
|
897
|
+
elif item.get("install_mode") == "pypi":
|
|
898
|
+
pkg = item.get("package", "")
|
|
899
|
+
version = item.get("package_version")
|
|
900
|
+
pkg_str = f"{pkg}=={version}" if version else pkg
|
|
901
|
+
completed = subprocess.run([sys.executable, "-m", "pip", "install", pkg_str], capture_output=True, text=True, timeout=300, check=False)
|
|
902
|
+
if completed.returncode != 0:
|
|
903
|
+
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
904
|
+
message = f"pip 패키지 설치 완료: {pkg_str}"
|
|
905
|
+
elif item.get("install_mode") == "npm":
|
|
906
|
+
pkg = item.get("package", "")
|
|
907
|
+
version = item.get("package_version")
|
|
908
|
+
pkg_str = f"{pkg}@{version}" if version else pkg
|
|
909
|
+
completed = subprocess.run(["npm", "install", "-g", pkg_str], capture_output=True, text=True, timeout=300, check=False)
|
|
910
|
+
if completed.returncode != 0:
|
|
911
|
+
raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
|
|
912
|
+
message = f"npm 패키지 설치 완료: {pkg_str}"
|
|
913
|
+
state[mcp_id] = {
|
|
914
|
+
"installed": True,
|
|
915
|
+
"status": status,
|
|
916
|
+
"authenticated": item.get("install_mode") != "connector",
|
|
917
|
+
"updated_at": datetime.now().isoformat(),
|
|
918
|
+
}
|
|
919
|
+
save_mcp_installs(data)
|
|
920
|
+
public = mcp_public_item(item, state)
|
|
921
|
+
public["message"] = message
|
|
922
|
+
return public
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
"load_mcp_installs": load_mcp_installs,
|
|
926
|
+
"save_mcp_installs": save_mcp_installs,
|
|
927
|
+
"mcp_public_item": mcp_public_item,
|
|
928
|
+
"recommend_mcps": recommend_mcps,
|
|
929
|
+
"install_mcp": install_mcp,
|
|
930
|
+
}
|
|
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
|
21
21
|
from lattice_brain.runtime.contracts import realtime_event_contract, run_record_contract, workflow_run_contract
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
WORKSPACE_OS_VERSION = "8.
|
|
24
|
+
WORKSPACE_OS_VERSION = "8.1.0"
|
|
25
25
|
|
|
26
26
|
# Workspace types separate single-user Personal workspaces from shared
|
|
27
27
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|