ltcai 7.9.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.
Files changed (107) hide show
  1. package/README.md +35 -49
  2. package/docs/CHANGELOG.md +35 -0
  3. package/docs/V3_FRONTEND.md +11 -5
  4. package/lattice_brain/__init__.py +1 -1
  5. package/lattice_brain/embeddings.py +9 -1
  6. package/lattice_brain/graph/projection.py +19 -5
  7. package/lattice_brain/graph/provenance.py +26 -4
  8. package/lattice_brain/graph/write_master.py +30 -3
  9. package/lattice_brain/runtime/multi_agent.py +1 -1
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/app_factory.py +13 -134
  12. package/latticeai/core/local_embeddings.py +8 -0
  13. package/latticeai/core/marketplace.py +1 -1
  14. package/latticeai/core/mcp_registry.py +140 -1
  15. package/latticeai/core/tool_registry.py +7 -0
  16. package/latticeai/core/workspace_os.py +1 -1
  17. package/latticeai/services/architecture_readiness.py +50 -6
  18. package/latticeai/services/model_runtime.py +36 -2
  19. package/latticeai/services/product_readiness.py +19 -17
  20. package/package.json +2 -2
  21. package/scripts/build_frontend_assets.mjs +11 -2
  22. package/src-tauri/Cargo.lock +1 -1
  23. package/src-tauri/Cargo.toml +1 -1
  24. package/src-tauri/tauri.conf.json +1 -1
  25. package/static/app/asset-manifest.json +11 -102
  26. package/static/app/assets/Act-BOO66G-c.js +1 -0
  27. package/static/app/assets/Brain-C6lEYiD7.js +321 -0
  28. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
  29. package/static/app/assets/Library-DK4FIp8a.js +1 -0
  30. package/static/app/assets/System-Bgs6Ql7x.js +1 -0
  31. package/static/app/assets/core-CwxXejkd.js +1 -2
  32. package/static/app/assets/index-Bvv79nre.js +16 -0
  33. package/static/app/assets/index-Dslqglia.css +2 -0
  34. package/static/app/assets/primitives-B70WOra0.js +1 -0
  35. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
  36. package/static/app/index.html +3 -10
  37. package/static/app/theme-boot.js +8 -0
  38. package/static/sw.js +1 -1
  39. package/frontend/index.html +0 -24
  40. package/frontend/openapi.json +0 -15425
  41. package/frontend/src/App.tsx +0 -243
  42. package/frontend/src/api/client.ts +0 -580
  43. package/frontend/src/api/openapi.ts +0 -17892
  44. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  45. package/frontend/src/components/FeedbackState.tsx +0 -45
  46. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  47. package/frontend/src/components/LivingBrain.tsx +0 -220
  48. package/frontend/src/components/ProductFlow.tsx +0 -171
  49. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  50. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  51. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  52. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  53. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  54. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  55. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  56. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  57. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  58. package/frontend/src/components/primitives.tsx +0 -392
  59. package/frontend/src/components/ui/badge.tsx +0 -27
  60. package/frontend/src/components/ui/button.tsx +0 -37
  61. package/frontend/src/components/ui/card.tsx +0 -22
  62. package/frontend/src/components/ui/input.tsx +0 -16
  63. package/frontend/src/components/ui/textarea.tsx +0 -16
  64. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  65. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  66. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  67. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  68. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  69. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  70. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  71. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  72. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  73. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  74. package/frontend/src/features/brain/brainData.ts +0 -246
  75. package/frontend/src/features/brain/graphLayout.ts +0 -37
  76. package/frontend/src/features/brain/types.ts +0 -150
  77. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  78. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  79. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  80. package/frontend/src/i18n.ts +0 -1303
  81. package/frontend/src/lib/utils.ts +0 -33
  82. package/frontend/src/main.tsx +0 -23
  83. package/frontend/src/pages/Act.tsx +0 -458
  84. package/frontend/src/pages/Ask.tsx +0 -14
  85. package/frontend/src/pages/Brain.tsx +0 -914
  86. package/frontend/src/pages/Capture.tsx +0 -258
  87. package/frontend/src/pages/Library.tsx +0 -486
  88. package/frontend/src/pages/System.tsx +0 -621
  89. package/frontend/src/routes.ts +0 -92
  90. package/frontend/src/store/appStore.ts +0 -94
  91. package/frontend/src/styles.css +0 -6579
  92. package/static/app/assets/Act-DOvf59ru.js +0 -2
  93. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  94. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  95. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  96. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  97. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  98. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  99. package/static/app/assets/System-D6t9jo9V.js +0 -2
  100. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  101. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  102. package/static/app/assets/index-C7g26IF6.css +0 -2
  103. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  104. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  105. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  106. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  107. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lattice AI
2
2
 
3
- **Lattice AI 7.9 is the local-first Digital Brain product with cleaner runtime boundaries. It keeps your knowledge durable across any AI model, with Brain Chat Home ready for immediate use and agent execution hardened behind explicit runtime ports.**
3
+ **Lattice AI 8.0 is the local-first Digital Brain platform with hardened runtime architecture. It keeps your knowledge durable across any AI model, with AgentRuntime, ToolRegistry, central Config, and Knowledge Graph stability tracked by explicit release contracts.**
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 a clearer 7.9 agent-runtime boundary.
14
+ It is the finished private AI memory layer wrapped in a Living Brain experience — now with the 8.0 runtime architecture contract behind it.
15
15
 
16
16
  [![PyPI Version](https://img.shields.io/pypi/v/ltcai?label=PyPI)](https://pypi.org/project/ltcai/)
17
17
  [![npm Version](https://img.shields.io/npm/v/ltcai?label=npm)](https://www.npmjs.com/package/ltcai)
@@ -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 7.9 line keeps the
68
- same first-run flow and rebuilds the Brain Chat Home layout so chat and the
69
- composer are the first visible action.
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
- ![Login](output/release/v7.8.0/screenshots/01-login.png)
81
+ ![Login](output/release/v8.1.0/screenshots/01-login.png)
82
82
 
83
- ### 3. Environment Analysis
84
-
85
- See what kind of local AI experience this computer can support before choosing a
86
- model.
87
-
88
- ![Environment Analysis](output/release/v7.8.0/screenshots/02-environment-analysis.png)
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
- ![Recommended Models](output/release/v7.8.0/screenshots/03-recommended-models.png)
88
+ ![Recommended Models](output/release/v8.1.0/screenshots/02-recommended-models.png)
96
89
 
97
- ### 5. Install And Load
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
- ![Install and Load](output/release/v7.8.0/screenshots/04-install-load-progress.png)
95
+ ![Install and Load](output/release/v8.1.0/screenshots/03-install-load-progress.png)
103
96
 
104
- ### 6. Brain Chat
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
- ![Brain Chat Home](output/release/v7.8.0/screenshots/05-brain-chat-home.png)
103
+ ![Brain Chat Home](output/release/v8.1.0/screenshots/04-brain-chat-home.png)
111
104
 
112
- ### 7. Review Center
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
- ![Review Center](output/release/v7.8.0/screenshots/13-review-center.png)
110
+ ![Review Center](output/release/v8.1.0/screenshots/12-review-center.png)
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
- ![v7.8.0 Living Brain walkthrough](output/release/v7.8.0/gifs/v7.8.0-living-brain-walkthrough.gif)
126
+ ![v8.1.0 Living Brain walkthrough](output/release/v8.1.0/gifs/v8.1.0-living-brain-walkthrough.gif)
134
127
 
135
128
  Screenshot index and capture notes:
136
- [output/release/v7.8.0/SCREENSHOT_INDEX.md](output/release/v7.8.0/SCREENSHOT_INDEX.md)
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,36 +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 **7.9.0 — Agent Runtime Boundary Hardening**:
214
-
215
- - The single-agent state machine is now named `SingleAgentRuntime`, while the
216
- historical `AgentRuntime` import remains as a compatibility alias.
217
- - The product AgentRuntime facade in `lattice_brain.runtime.agent_runtime`
218
- remains the unambiguous `/agents` execution and observability boundary.
219
- - Agent rollback is now an injected runtime port wired through
220
- `ToolDispatchService`, so the core state machine no longer shells out directly.
221
- - Both runtime surfaces expose a `runtime-boundary/v1` descriptor so tests and
222
- API clients can identify the owning execution boundary without relying on
223
- ambiguous class names.
224
- - A shared `RuntimeBoundaryProtocol` now fixes the common inspection surface
225
- while letting each runtime keep its own execution methods.
226
- - Product readiness is machine-checkable through `product_readiness()`, which
227
- composes the architecture contract with exact artifact/docs/workflow evidence.
228
- - CI and release workflows now run frontend lint/typecheck/build gates plus the
229
- 7.9 product readiness score before a tag can be treated as release-safe.
230
-
231
- Expected artifacts for 7.9.0 release must use exact filenames:
232
-
233
- - `dist/ltcai-7.9.0-py3-none-any.whl`
234
- - `dist/ltcai-7.9.0.tar.gz`
235
- - `ltcai-7.9.0.tgz`
236
- - `dist/ltcai-7.9.0.vsix`
237
- - `src-tauri/target/release/bundle/dmg/Lattice AI_7.9.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`
238
222
 
239
223
  Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
240
224
 
241
225
  See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
242
- strategic roadmap slices applied through 7.9.0 and the follow-up tracks.
226
+ strategic roadmap slices applied through 8.1.0 and the follow-up tracks.
243
227
 
244
228
  ## Known Limitations
245
229
 
@@ -256,6 +240,8 @@ strategic roadmap slices applied through 7.9.0 and the follow-up tracks.
256
240
 
257
241
  | Version | Theme |
258
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 |
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 |
259
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 |
260
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 |
261
247
  | 7.7.0 | Complete Product Polish: command-center Brain Home, repaired Review Center evidence, exact 7.7 docs/artifacts, product readiness gate, and stronger CI/release checks |
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ 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
+
23
+ ## [8.0.0] - 2026-06-24
24
+
25
+ ### Changed
26
+ - Added `lattice-architecture-contract/v1` to make the AgentRuntime,
27
+ ToolRegistry, Config, server decomposition, and Knowledge Graph stabilization
28
+ boundaries explicit and testable for the major architecture line.
29
+ - Added `tool-registry-contract/v1` to the ToolRegistry manifest, including
30
+ dispatch, policy, and permission ownership.
31
+ - Updated architecture and product readiness targets to 8.0.0.
32
+ - Synchronized package/runtime/static/Tauri metadata to 8.0.0.
33
+ - Updated current-release docs and exact artifact names to 8.0.0 while
34
+ preserving historical 7.x entries.
35
+
36
+ ### Fixed
37
+ - Made logical Knowledge Graph `replace` imports transactional so malformed
38
+ imports roll back without clearing the existing graph.
39
+ - Locked v2 read-equivalence coverage for `list_documents`, `get_node`,
40
+ `relationship_search`, and `traverse`.
41
+ - Preserved colliding legacy edge labels during logical import/backfill while
42
+ keeping native write-door synonym dedupe canonical.
43
+
9
44
  ## [7.9.0] - 2026-06-23
10
45
 
11
46
  ### 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`) drives the nav rail, the
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/v3/asset-manifest.json`; the build
54
- step writes hashed CSS/JS siblings and `/app` reads the manifest instead of
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` (extended to cover `static/v3/**` via `scripts/lint_v3.mjs`).
137
- - `npm run build:assets` (writes hashed v3 assets and `asset-manifest.json`).
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.
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "7.9.0"
29
+ __version__ = "8.1.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -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"]
@@ -331,17 +331,31 @@ class KnowledgeGraphProjectionMixin:
331
331
  edge_id: Optional[str] = None,
332
332
  created_at: Optional[str] = None,
333
333
  strict: bool = False,
334
+ legacy_type: Optional[str] = None,
334
335
  ) -> None:
335
336
  if KGStoreV2 is None:
336
337
  if strict:
337
338
  raise RuntimeError("Knowledge Graph v2 schema is unavailable")
338
339
  return
339
- eid = (
340
- edge_id or f"edge:{_sha256_text(f'{from_node}|{edge_type}|{to_node}')[:24]}"
341
- )
340
+ explicit_legacy_type = legacy_type is not None
341
+ leg_type = legacy_type if explicit_legacy_type else edge_type
342
+ # Native canonical writes (and write-door dedupes) use legacy_type=''
343
+ # so (source,target,type) is the effective key.
344
+ # Import paths can pass distinct legacy_type to keep colliding legacy
345
+ # labels as separate rows (lossless for old data).
346
+ if leg_type and EdgeType is not None:
347
+ try:
348
+ if EdgeType.from_legacy(leg_type).value == leg_type:
349
+ leg_type = ""
350
+ except ValueError:
351
+ pass
342
352
  norm_type = (
343
353
  EdgeType.from_legacy(edge_type).value if EdgeType is not None else edge_type
344
354
  )
355
+ if explicit_legacy_type and leg_type:
356
+ eid = f"edge:{_sha256_text(f'{from_node}|{norm_type}|{to_node}|{leg_type}')[:24]}"
357
+ else:
358
+ eid = edge_id or f"edge:{_sha256_text(f'{from_node}|{norm_type}|{to_node}')[:24]}"
345
359
  meta_str = metadata_json if metadata_json is not None else "{}"
346
360
  confidence = float(_safe_loads(meta_str).get("confidence", 1.0))
347
361
  try:
@@ -360,7 +374,7 @@ class KnowledgeGraphProjectionMixin:
360
374
  from_node,
361
375
  to_node,
362
376
  norm_type,
363
- edge_type,
377
+ leg_type,
364
378
  float(weight),
365
379
  confidence,
366
380
  meta_str,
@@ -371,7 +385,7 @@ class KnowledgeGraphProjectionMixin:
371
385
  # (the UNIQUE upsert + weight=max alone would erase recurrence).
372
386
  row = conn.execute(
373
387
  "SELECT id FROM edges_v2 WHERE source=? AND target=? AND type=? AND legacy_type=?",
374
- (from_node, to_node, norm_type, edge_type),
388
+ (from_node, to_node, norm_type, leg_type),
375
389
  ).fetchone()
376
390
  if row is not None:
377
391
  conn.execute(
@@ -292,10 +292,24 @@ class KnowledgeGraphProvenanceMixin:
292
292
  plan["dry_run"] = True
293
293
  return plan
294
294
 
295
- if mode == "replace":
296
- self.clear_all()
297
-
298
295
  with self._connect() as conn:
296
+ if mode == "replace":
297
+ # Keep replacement imports transactional. The old clear_all()
298
+ # path committed before the import started, so a malformed
299
+ # artifact could leave a cleared or partially rebuilt graph.
300
+ # These deletes roll back with the rest of the import.
301
+ for table in (
302
+ "local_file_index",
303
+ "knowledge_sources",
304
+ "chunks",
305
+ "edges",
306
+ "nodes",
307
+ "vector_embeddings",
308
+ ):
309
+ conn.execute(f"DELETE FROM {table}")
310
+ if KGStoreV2 is not None:
311
+ conn.execute("DELETE FROM edges_v2")
312
+ conn.execute("DELETE FROM nodes_v2")
299
313
  for n in nodes:
300
314
  self._upsert_node(
301
315
  conn,
@@ -315,13 +329,21 @@ class KnowledgeGraphProvenanceMixin:
315
329
  metadata=_safe_loads(c.get("metadata_json")),
316
330
  )
317
331
  for e in edges:
332
+ e_meta = _safe_loads(e.get("metadata_json")) or {}
333
+ leg_label = e_meta.get("legacy_label")
334
+ if not leg_label:
335
+ orig = e.get("type") or ""
336
+ if orig:
337
+ # preserve whatever label came from export (legacy or canon)
338
+ leg_label = orig
318
339
  self._upsert_edge(
319
340
  conn,
320
341
  e["from_node"],
321
342
  e["to_node"],
322
343
  e["type"],
323
344
  weight=float(e.get("weight") or 1.0),
324
- metadata=_safe_loads(e.get("metadata_json")),
345
+ metadata=e_meta,
346
+ legacy_label=leg_label,
325
347
  )
326
348
  for s in sources:
327
349
  conn.execute(
@@ -73,20 +73,46 @@ class KnowledgeGraphWriteMixin:
73
73
  edge_type: str,
74
74
  weight: float = 1.0,
75
75
  metadata: Optional[Dict[str, Any]] = None,
76
+ *,
77
+ legacy_label: Optional[str] = None,
76
78
  ) -> str:
77
79
  # v4 write door: every new edge stores the canonical EdgeType value —
78
80
  # free-string types (e.g. '포함함', '언급함') are normalized here, so no
79
81
  # caller can mint new legacy taxonomy. The original label survives in
80
82
  # metadata.legacy_label for traceability.
83
+ #
84
+ # legacy_type in edges_v2:
85
+ # - normal write door: synonyms always dedupe to ONE row with legacy_type=''
86
+ # - import (passes legacy_label=) : preserves distinct legacy labels as
87
+ # separate v2 rows (lossless collision for old artifacts / backfill)
88
+ passed_for_legacy = legacy_label or edge_type
81
89
  if EdgeType is not None:
82
90
  canonical = EdgeType.from_legacy(edge_type).value
83
- if canonical != edge_type:
91
+ if canonical != edge_type or (legacy_label and legacy_label != canonical):
84
92
  metadata = dict(metadata or {})
85
- metadata.setdefault("legacy_label", edge_type)
93
+ ll = legacy_label or edge_type
94
+ if ll != canonical:
95
+ metadata.setdefault("legacy_label", ll)
86
96
  edge_type = canonical
87
97
  edge_id = f"edge:{_sha256_text(f'{from_node}|{edge_type}|{to_node}')[:24]}"
88
98
  now = _now()
89
99
  meta_json = _json(metadata) # canonical string shared with the projection
100
+ v2_legacy = ""
101
+ if passed_for_legacy and passed_for_legacy != edge_type:
102
+ if legacy_label is not None:
103
+ # explicit legacy_label from import path forces distinct legacy_type
104
+ # rows in v2 for collision preservation
105
+ v2_legacy = passed_for_legacy
106
+ else:
107
+ # normal write-door dedupes even for synonym labels (legacy_type='')
108
+ v2_legacy = ""
109
+ # v2 may use distinct eid when preserving legacy collisions (different from
110
+ # the canon-based edge_id used for legacy edges table which always dedupes)
111
+ v2_eid = (
112
+ f"edge:{_sha256_text(f'{from_node}|{edge_type}|{to_node}|{v2_legacy}')[:24]}"
113
+ if v2_legacy
114
+ else edge_id
115
+ )
90
116
  self._v2_project_edge(
91
117
  conn,
92
118
  from_node,
@@ -94,9 +120,10 @@ class KnowledgeGraphWriteMixin:
94
120
  edge_type,
95
121
  float(weight),
96
122
  meta_json,
97
- edge_id=edge_id,
123
+ edge_id=v2_eid,
98
124
  created_at=now,
99
125
  strict=True,
126
+ legacy_type=v2_legacy,
100
127
  )
101
128
  conn.execute(
102
129
  """
@@ -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 = "7.9.0"
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")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "7.9.0"
3
+ __version__ = "8.1.0"
@@ -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
- _get_combined_registry,
199
- _fetch_skills_marketplace, install_skill, SKILLS_DIR,
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