ltcai 8.9.0 → 9.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
package/auto_setup.py CHANGED
@@ -1,908 +1,11 @@
1
- """
2
- Lattice AI — Zero-Config Auto Setup
3
- ===================================
1
+ """Compatibility shim for :mod:`latticeai.setup.auto_setup`."""
4
2
 
5
- 명세: ``lattice_ai_full_spec.pptx`` 슬라이드 16·17 (자동 환경 매트릭스 + 5단계 흐름)
6
-
7
- 5단계
8
- -----
9
- ① PROBE OS · CPU · GPU · RAM · 디스크 · 가속 SDK 감지
10
- ② RECOMMEND 사양 점수 → 최적 모델 / 런타임 / 양자화 자동 선택
11
- ③ INSTALL OS별 패키지 매니저 어댑터 호출 (winget · brew · apt · 스토어)
12
- ④ VERIFY 추론 토큰/초 측정, 첫 응답 지연, 메모리 누수 점검
13
- ⑤ PRESET 기본/고급 모드 분기 + 단축키·MCP·테마 적용
14
-
15
- 원칙
16
- ----
17
- - **표준 라이브러리 only.** 외부 패키지 import 는 모두 try/except 로 감싼다.
18
- - **변경하지 않는다, 추천만 한다.** INSTALL 단계는 *실행 명령어* 를 생성하고
19
- 돌려보낼 뿐, ``--apply`` 플래그 없이는 시스템을 건드리지 않는다.
20
- - **모든 출력은 JSON-직렬화 가능**해야 UI(설치 마법사 화면) 에서 그대로 표시 가능.
21
-
22
- 사용
23
- ----
24
- ```bash
25
- python3 auto_setup.py probe # 1단계만
26
- python3 auto_setup.py recommend # 1+2단계
27
- python3 auto_setup.py plan # 1+2+3 (설치 계획 출력)
28
- python3 auto_setup.py plan --apply --confirm-token <token>
29
- python3 auto_setup.py verify # 4단계 단독
30
- python3 auto_setup.py preset # 5단계
31
- python3 auto_setup.py all # 전체 흐름
32
- ```
33
- """
34
-
35
- from __future__ import annotations
36
-
37
- import argparse
38
- import json
39
- import os
40
- import platform
41
- import re
42
- import shutil
43
- import subprocess
44
- import sys
45
- import time
46
- from dataclasses import asdict, dataclass, field
47
- from pathlib import Path
48
- from typing import Any, Dict, List, Optional, Tuple
49
-
50
- from latticeai.services.process_audit import (
51
- CommandConfirmationError,
52
- append_process_audit_event,
53
- command_plan,
54
- command_plan_for_commands,
55
- require_command_confirmation,
56
- )
57
-
58
- __all__ = [
59
- "SystemProfile", "Recommendation", "InstallPlan",
60
- "probe", "recommend", "plan", "verify", "preset", "run_all",
61
- ]
62
-
63
-
64
- # ── 1. PROBE ────────────────────────────────────────────────────────────────
65
- @dataclass
66
- class GPUInfo:
67
- vendor: str = "unknown" # nvidia | amd | intel | apple | none
68
- model: str = ""
69
- vram_mb: int = 0
70
- sdk: List[str] = field(default_factory=list) # ['cuda', 'metal', 'mlx', ...]
71
-
72
-
73
- @dataclass
74
- class SystemProfile:
75
- os: str = "" # windows | darwin | linux | ios | android
76
- os_version: str = ""
77
- arch: str = "" # x86_64 | arm64 | …
78
- cpu_model: str = ""
79
- cpu_cores: int = 0
80
- cpu_logical_cores: int = 0
81
- cpu_instructions: List[str] = field(default_factory=list)
82
- ram_mb: int = 0
83
- disk_free_mb: int = 0
84
- gpu: GPUInfo = field(default_factory=GPUInfo)
85
- package_manager: Optional[str] = None # winget | brew | apt | dnf | pacman
86
- has_internet: bool = True
87
- python_version: str = ""
88
- is_wsl: bool = False
89
- wsl_version: str = ""
90
- cuda_available: bool = False
91
- cuda_version: str = ""
92
- tools: Dict[str, str] = field(default_factory=dict)
93
-
94
- def score(self) -> int:
95
- """LLM 적합도 점수 (0..100). RECOMMEND 의 입력."""
96
- s = 0
97
- s += min(self.cpu_cores * 2, 24)
98
- s += min(self.ram_mb // 1024 * 2, 40)
99
- s += min(self.gpu.vram_mb // 1024 * 4, 36)
100
- return min(s, 100)
101
-
102
- def to_json(self) -> Dict[str, Any]:
103
- d = asdict(self)
104
- d["score"] = self.score()
105
- return d
106
-
107
-
108
- def _read_text(path: str) -> str:
109
- try:
110
- return Path(path).read_text(encoding="utf-8", errors="replace")
111
- except Exception:
112
- return ""
113
-
114
-
115
- def _run(cmd: List[str], timeout: float = 4.0) -> str:
116
- try:
117
- out = subprocess.run(cmd, capture_output=True, text=True,
118
- timeout=timeout, check=False)
119
- return (out.stdout or "") + (out.stderr or "")
120
- except Exception:
121
- return ""
122
-
123
-
124
- def _windows_candidate_paths(binary: str) -> List[str]:
125
- local_appdata = os.environ.get("LOCALAPPDATA", "")
126
- program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
127
- program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
128
- candidates = {
129
- "ollama": [
130
- str(Path(local_appdata) / "Programs" / "Ollama" / "ollama.exe") if local_appdata else "",
131
- str(Path(program_files) / "Ollama" / "ollama.exe"),
132
- ],
133
- "lms": [
134
- str(Path(local_appdata) / "Programs" / "LM Studio" / "resources" / "app" / ".webpack" / "lms.exe") if local_appdata else "",
135
- str(Path(program_files) / "LM Studio" / "resources" / "app" / ".webpack" / "lms.exe"),
136
- ],
137
- "nvidia-smi": [
138
- str(Path(program_files) / "NVIDIA Corporation" / "NVSMI" / "nvidia-smi.exe"),
139
- str(Path(program_files_x86) / "NVIDIA Corporation" / "NVSMI" / "nvidia-smi.exe"),
140
- ],
141
- }
142
- return [item for item in candidates.get(binary, []) if item]
143
-
144
-
145
- def _which(binary: str) -> Optional[str]:
146
- found = shutil.which(binary)
147
- if found:
148
- return found
149
- if platform.system() == "Windows":
150
- for candidate in _windows_candidate_paths(binary):
151
- if Path(candidate).exists():
152
- return candidate
153
- return None
154
-
155
-
156
- def _parse_windows_video_controllers(raw: str) -> List[Dict[str, Any]]:
157
- controllers: List[Dict[str, Any]] = []
158
- if not raw:
159
- return controllers
160
- try:
161
- data = json.loads(raw)
162
- if isinstance(data, dict):
163
- data = [data]
164
- if isinstance(data, list):
165
- for item in data:
166
- name = str(item.get("Name") or "").strip()
167
- if not name:
168
- continue
169
- try:
170
- ram_mb = int(item.get("AdapterRAM") or 0) // (1024 * 1024)
171
- except Exception:
172
- ram_mb = 0
173
- controllers.append({"name": name, "vram_mb": ram_mb})
174
- if controllers:
175
- return controllers
176
- except Exception:
177
- pass
178
- current: Dict[str, Any] = {}
179
- for line in raw.splitlines():
180
- if line.startswith("Name="):
181
- if current:
182
- controllers.append(current)
183
- current = {"name": line.split("=", 1)[-1].strip(), "vram_mb": 0}
184
- elif line.startswith("AdapterRAM=") and current:
185
- try:
186
- current["vram_mb"] = int(line.split("=", 1)[-1].strip()) // (1024 * 1024)
187
- except ValueError:
188
- current["vram_mb"] = 0
189
- if current:
190
- controllers.append(current)
191
- return controllers
192
-
193
-
194
- def _detect_gpu(prof_os: str, arch: str) -> GPUInfo:
195
- """OS별 휴리스틱으로 GPU 감지. 외부 라이브러리 없이 가능한 만큼만."""
196
- gpu = GPUInfo()
197
-
198
- # NVIDIA
199
- nvidia_smi = _which("nvidia-smi")
200
- if nvidia_smi:
201
- info = _run([nvidia_smi, "--query-gpu=name,memory.total",
202
- "--format=csv,noheader,nounits"])
203
- if info.strip():
204
- first = info.strip().splitlines()[0]
205
- try:
206
- name, mem = [x.strip() for x in first.split(",", 1)]
207
- gpu.vendor = "nvidia"
208
- gpu.model = name
209
- gpu.vram_mb = int(float(mem))
210
- gpu.sdk.append("cuda")
211
- except ValueError:
212
- pass
213
-
214
- # Apple Silicon / Metal
215
- if prof_os == "darwin":
216
- sp = _run(["system_profiler", "SPDisplaysDataType"], timeout=6.0)
217
- if "Apple" in sp and arch == "arm64":
218
- gpu.vendor = "apple"
219
- for line in sp.splitlines():
220
- if "Chipset Model" in line:
221
- gpu.model = line.split(":", 1)[-1].strip()
222
- break
223
- # Apple Silicon 의 GPU 메모리는 통합 메모리 = RAM. 별도 표기 안 함.
224
- gpu.sdk.extend(["metal", "mlx" if _has_module("mlx") else ""])
225
- gpu.sdk = [s for s in gpu.sdk if s]
226
-
227
- # Windows
228
- if prof_os == "windows" and gpu.vendor == "unknown":
229
- shell = _which("powershell") or _which("pwsh")
230
- info = ""
231
- if shell:
232
- info = _run([
233
- shell, "-NoProfile", "-Command",
234
- "Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM | ConvertTo-Json -Compress",
235
- ], timeout=8.0)
236
- if not info:
237
- info = _run(["wmic", "path", "win32_VideoController", "get",
238
- "Name,AdapterRAM", "/format:list"])
239
- controllers = _parse_windows_video_controllers(info)
240
- if controllers:
241
- primary = max(controllers, key=lambda item: int(item.get("vram_mb") or 0))
242
- name = str(primary.get("name") or "")
243
- gpu.model = name
244
- gpu.vram_mb = int(primary.get("vram_mb") or 0)
245
- low = name.lower()
246
- if "nvidia" in low or "rtx" in low or "geforce" in low:
247
- gpu.vendor = "nvidia"; gpu.sdk.append("cuda")
248
- elif "amd" in low or "radeon" in low:
249
- gpu.vendor = "amd"; gpu.sdk.extend(["directml", "vulkan"])
250
- elif "intel" in low or "arc" in low or "iris" in low:
251
- gpu.vendor = "intel"; gpu.sdk.extend(["directml", "vulkan"])
252
-
253
- # Linux (lspci)
254
- if prof_os == "linux" and gpu.vendor == "unknown":
255
- info = _run(["lspci"], timeout=3.0).lower()
256
- if "nvidia" in info:
257
- gpu.vendor = "nvidia"; gpu.sdk.append("cuda")
258
- elif "amd/ati" in info or "advanced micro devices" in info:
259
- gpu.vendor = "amd"; gpu.sdk.extend(["rocm", "vulkan"])
260
- elif "intel corporation" in info and "vga" in info:
261
- gpu.vendor = "intel"; gpu.sdk.append("vulkan")
262
-
263
- return gpu
264
-
265
-
266
- def _detect_package_manager(prof_os: str) -> Optional[str]:
267
- if prof_os == "windows":
268
- return "winget" if _which("winget") else None
269
- if prof_os == "darwin":
270
- return "brew" if _which("brew") else None
271
- if prof_os == "linux":
272
- for pm in ("apt", "dnf", "pacman", "zypper", "apk"):
273
- if _which(pm):
274
- return pm
275
- return None
276
-
277
-
278
- def _detect_tools() -> Dict[str, str]:
279
- tools: Dict[str, str] = {}
280
- for binary in ("ollama", "lms", "nvidia-smi", "nvcc", "winget", "brew", "apt", "git", "node", "python", "python3"):
281
- found = _which(binary)
282
- if found:
283
- tools[binary] = found
284
- return tools
285
-
286
-
287
- def _detect_wsl(prof_os: str) -> Tuple[bool, str]:
288
- if prof_os != "linux":
289
- return False, ""
290
- raw = _read_text("/proc/version")
291
- is_wsl = "microsoft" in raw.lower() or "wsl" in raw.lower()
292
- version = "2" if "microsoft-standard" in raw.lower() or "wsl2" in raw.lower() else ("1" if is_wsl else "")
293
- return is_wsl, version
294
-
295
-
296
- def _detect_cuda() -> Tuple[bool, str]:
297
- nvidia_smi = _which("nvidia-smi")
298
- nvcc = _which("nvcc")
299
- version = ""
300
- if nvidia_smi:
301
- raw = _run([nvidia_smi, "--query-gpu=driver_version", "--format=csv,noheader"], timeout=4.0)
302
- version = raw.splitlines()[0].strip() if raw.splitlines() else ""
303
- if nvcc:
304
- raw = _run([nvcc, "--version"], timeout=4.0)
305
- m = re.search(r"release\s+([\d.]+)", raw)
306
- if m:
307
- version = m.group(1)
308
- return bool(nvidia_smi or nvcc), version
309
-
310
-
311
- def _detect_cpu_details(prof_os: str) -> Tuple[str, int, int, List[str]]:
312
- model = platform.processor() or ""
313
- physical = os.cpu_count() or 0
314
- logical = os.cpu_count() or 0
315
- flags: List[str] = []
316
- if prof_os == "darwin":
317
- model = _run(["sysctl", "-n", "machdep.cpu.brand_string"]).strip() or model
318
- try:
319
- physical = int((_run(["sysctl", "-n", "hw.physicalcpu"]).strip() or physical))
320
- logical = int((_run(["sysctl", "-n", "hw.logicalcpu"]).strip() or logical))
321
- except ValueError:
322
- pass
323
- flags = [item.lower() for item in _run(["sysctl", "-n", "machdep.cpu.features"]).split()]
324
- elif prof_os == "linux":
325
- text = _read_text("/proc/cpuinfo")
326
- for line in text.splitlines():
327
- if line.lower().startswith("model name") and not model:
328
- model = line.split(":", 1)[-1].strip()
329
- if line.lower().startswith(("flags", "features")) and not flags:
330
- flags = line.split(":", 1)[-1].strip().lower().split()
331
- elif prof_os == "windows":
332
- raw = _run(["wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors", "/format:list"])
333
- for line in raw.splitlines():
334
- key, _, value = line.partition("=")
335
- if key == "Name" and value.strip():
336
- model = value.strip()
337
- elif key == "NumberOfCores" and value.strip():
338
- try:
339
- physical = int(value.strip())
340
- except ValueError:
341
- pass
342
- elif key == "NumberOfLogicalProcessors" and value.strip():
343
- try:
344
- logical = int(value.strip())
345
- except ValueError:
346
- pass
347
- try:
348
- import ctypes
349
- kernel32 = ctypes.windll.kernel32
350
- feature_map = {6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand"}
351
- flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
352
- except Exception:
353
- pass
354
- interesting = {"avx", "avx2", "avx512f", "fma", "neon", "sse4_2", "sse", "sse2", "sse3", "rdrand"}
355
- return model, physical, logical, sorted({flag for flag in flags if flag in interesting})
356
-
357
-
358
- def _has_module(name: str) -> bool:
359
- try:
360
- __import__(name)
361
- return True
362
- except Exception:
363
- return False
364
-
365
-
366
- def probe() -> SystemProfile:
367
- """① PROBE — 외부 의존성 없이 가능한 만큼 환경을 감지한다."""
368
- prof = SystemProfile()
369
- prof.os = {"Darwin": "darwin", "Windows": "windows",
370
- "Linux": "linux"}.get(platform.system(), platform.system().lower())
371
- prof.os_version = platform.release()
372
- prof.arch = platform.machine().lower()
373
- cpu_model, cpu_cores, cpu_logical_cores, cpu_instructions = _detect_cpu_details(prof.os)
374
- prof.cpu_model = cpu_model
375
- prof.cpu_cores = cpu_cores
376
- prof.cpu_logical_cores = cpu_logical_cores
377
- prof.cpu_instructions = cpu_instructions
378
- prof.python_version = platform.python_version()
379
- prof.is_wsl, prof.wsl_version = _detect_wsl(prof.os)
380
- prof.cuda_available, prof.cuda_version = _detect_cuda()
381
- prof.tools = _detect_tools()
382
-
383
- # RAM
384
- try:
385
- if prof.os == "linux":
386
- for line in _read_text("/proc/meminfo").splitlines():
387
- if line.startswith("MemTotal:"):
388
- prof.ram_mb = int(line.split()[1]) // 1024
389
- break
390
- elif prof.os == "darwin":
391
- out = _run(["sysctl", "-n", "hw.memsize"])
392
- if out.strip():
393
- try:
394
- prof.ram_mb = int(out.strip()) // (1024 * 1024)
395
- except ValueError:
396
- prof.ram_mb = 0
397
- if not prof.ram_mb:
398
- profiler = _run(["system_profiler", "SPHardwareDataType"], timeout=8.0)
399
- m = re.search(r"Memory:\s+([\d.]+)\s*(TB|GB|MB)", profiler, re.IGNORECASE)
400
- if m:
401
- value = float(m.group(1))
402
- unit = m.group(2).lower()
403
- if unit == "tb":
404
- prof.ram_mb = int(value * 1024 * 1024)
405
- elif unit == "gb":
406
- prof.ram_mb = int(value * 1024)
407
- else:
408
- prof.ram_mb = int(value)
409
- if not prof.ram_mb:
410
- hostinfo = _run(["hostinfo"])
411
- m = re.search(r"Primary memory available:\s+([\d.]+)\s+gigabytes", hostinfo, re.IGNORECASE)
412
- if m:
413
- prof.ram_mb = int(float(m.group(1)) * 1024)
414
- elif prof.os == "windows":
415
- out = _run(["wmic", "ComputerSystem", "get", "TotalPhysicalMemory",
416
- "/format:list"])
417
- for line in out.splitlines():
418
- if line.startswith("TotalPhysicalMemory="):
419
- prof.ram_mb = int(line.split("=", 1)[-1].strip()) // (1024 * 1024)
420
- break
421
- except Exception:
422
- pass
423
-
424
- # Disk
425
- try:
426
- usage = shutil.disk_usage(Path.home())
427
- prof.disk_free_mb = usage.free // (1024 * 1024)
428
- except Exception:
429
- pass
430
-
431
- prof.gpu = _detect_gpu(prof.os, prof.arch)
432
- prof.package_manager = _detect_package_manager(prof.os)
433
- return prof
434
-
435
-
436
- # ── 2. RECOMMEND ────────────────────────────────────────────────────────────
437
- @dataclass
438
- class Recommendation:
439
- runtime: str # llama.cpp | mlx | vllm | mlc-llm | tflite
440
- backend: str # cuda | metal+mlx | directml | vulkan | rocm | cpu
441
- model_id: str # 추천 모델 (huggingface-like id)
442
- quantization: str # q4_K_M | q5_K_M | mxfp4 | f16
443
- rationale: List[str] # 왜 이걸 골랐는지 (UI에 표시)
444
- estimated_tokens_per_sec: Optional[float] = None
445
-
446
- def to_json(self) -> Dict[str, Any]:
447
- return asdict(self)
448
-
449
-
450
- # 모델 카탈로그. PPT 슬라이드 16 의 "추천 모델" 열과 동기화.
451
- _MODEL_CATALOG: List[Dict[str, Any]] = [
452
- # (min_ram_mb, min_vram_mb, model_id, quant, runtime_preference)
453
- # OS 오버헤드(~4-6 GB) + KV 캐시 여유를 감안한 보수적 RAM 임계값
454
- {"ram": 64 * 1024, "vram": 32 * 1024,
455
- "id": "mlx-community/gemma-4-31b-it-4bit", "q": "4bit", "multimodal": True},
456
- {"ram": 64 * 1024, "vram": 32 * 1024,
457
- "id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
458
- {"ram": 48 * 1024, "vram": 24 * 1024,
459
- "id": "mlx-community/gemma-4-31b-it-4bit", "q": "4bit", "multimodal": True},
460
- {"ram": 32 * 1024, "vram": 16 * 1024,
461
- "id": "mlx-community/gemma-4-26b-a4b-it-4bit", "q": "4bit", "multimodal": True},
462
- {"ram": 48 * 1024, "vram": 24 * 1024,
463
- "id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
464
- {"ram": 24 * 1024, "vram": 12 * 1024,
465
- "id": "mlx-community/Llama-4-Scout-17B-16E-Instruct-4bit", "q": "4bit", "multimodal": True},
466
- {"ram": 16 * 1024, "vram": 8 * 1024,
467
- "id": "mlx-community/gemma-4-12b-it-4bit", "q": "4bit", "multimodal": True},
468
- {"ram": 32 * 1024, "vram": 16 * 1024,
469
- "id": "Qwen/Qwen3-VL-8B-Instruct", "q": "q5_K_M", "multimodal": True},
470
- {"ram": 24 * 1024, "vram": 12 * 1024,
471
- "id": "Qwen/Qwen3-VL-8B-Instruct", "q": "q4_K_M", "multimodal": True},
472
- {"ram": 16 * 1024, "vram": 8 * 1024,
473
- "id": "Qwen/Qwen3-VL-8B-Instruct", "q": "q4_K_M", "multimodal": True},
474
- {"ram": 12 * 1024, "vram": 6 * 1024,
475
- "id": "Qwen/Qwen3-VL-4B-Instruct", "q": "q4_K_M", "multimodal": True},
476
- {"ram": 8 * 1024, "vram": 4 * 1024,
477
- "id": "Qwen/Qwen3-VL-4B-Instruct", "q": "q4_K_M", "multimodal": True},
478
- {"ram": 4 * 1024, "vram": 0,
479
- "id": "Qwen/Qwen3-VL-4B-Instruct", "q": "q4_K_M", "multimodal": True},
480
- ]
481
-
482
-
483
- def recommend(profile: SystemProfile) -> Recommendation:
484
- """② RECOMMEND — 프로파일을 보고 런타임/모델/양자화를 결정한다."""
485
- rationale: List[str] = []
486
-
487
- # backend / runtime
488
- if profile.os == "darwin" and profile.gpu.vendor == "apple":
489
- backend = "metal+mlx"
490
- runtime = "mlx" if _has_module("mlx_vlm") else "llama.cpp"
491
- rationale.append("Apple Silicon → Metal + MLX-VLM")
492
- elif profile.gpu.vendor == "nvidia" and profile.cuda_available and (profile.os == "linux" or profile.is_wsl):
493
- backend = "cuda"
494
- runtime = "vllm" if profile.gpu.vram_mb >= 12 * 1024 else "llama.cpp"
495
- rationale.append(f"NVIDIA GPU {profile.gpu.vram_mb} MB VRAM + CUDA → {runtime}")
496
- elif profile.gpu.vendor == "nvidia":
497
- backend = "cuda" if profile.cuda_available else "vulkan"
498
- runtime = "lmstudio" if profile.tools.get("lms") else ("ollama" if profile.tools.get("ollama") else "llama.cpp")
499
- rationale.append("Windows NVIDIA는 LM Studio/Ollama 우선, vLLM은 WSL/Linux 권장")
500
- elif profile.os == "windows" and profile.gpu.vendor in ("amd", "intel"):
501
- backend = "directml/vulkan"
502
- runtime = "lmstudio" if profile.tools.get("lms") else ("ollama" if profile.tools.get("ollama") else "llama.cpp")
503
- rationale.append("Windows + AMD/Intel GPU → DirectML/Vulkan")
504
- elif profile.os == "linux" and profile.gpu.vendor == "amd":
505
- backend = "rocm" if "rocm" in profile.gpu.sdk else "vulkan"
506
- runtime = "ollama" if profile.tools.get("ollama") else "llama.cpp"
507
- rationale.append("Linux + AMD GPU → ROCm/Vulkan")
508
- else:
509
- backend = "cpu"
510
- runtime = "ollama" if profile.tools.get("ollama") else "llama.cpp"
511
- instruction_hint = ", ".join(profile.cpu_instructions) or "명령어 미감지"
512
- rationale.append(f"GPU 가속이 없거나 미감지 → CPU 추론 ({profile.cpu_logical_cores or profile.cpu_cores} threads, {instruction_hint})")
513
-
514
- # model size by RAM/VRAM
515
- pick = _MODEL_CATALOG[-1] # 가장 작은 모델 기본값
516
- for entry in _MODEL_CATALOG:
517
- if profile.ram_mb >= entry["ram"] and (
518
- backend in {"cpu", "metal+mlx"} or profile.gpu.vram_mb >= entry["vram"]
519
- ):
520
- pick = entry
521
- break
522
- rationale.append(
523
- f"RAM {profile.ram_mb} MB · VRAM {profile.gpu.vram_mb} MB → {pick['id']}"
524
- )
525
- if pick.get("multimodal"):
526
- rationale.append("최신 멀티모달 모델을 우선 선택")
527
-
528
- # 양자화: VRAM 충분 → 더 정밀한 양자화로 업그레이드
529
- quant = pick["q"]
530
- if profile.gpu.vram_mb >= 24 * 1024:
531
- quant = "f16"
532
- rationale.append("VRAM ≥ 24 GB → f16 풀 정밀도")
533
-
534
- # 거친 tokens/sec 예측 (very rough)
535
- est_tps = None
536
- if backend == "cuda":
537
- est_tps = max(8.0, profile.gpu.vram_mb / 800)
538
- elif backend == "metal+mlx":
539
- est_tps = max(6.0, (profile.ram_mb // 1024) * 0.7)
540
- elif backend == "cpu":
541
- est_tps = max(1.5, profile.cpu_cores * 0.6)
542
-
543
- return Recommendation(
544
- runtime=runtime, backend=backend,
545
- model_id=pick["id"], quantization=quant,
546
- rationale=rationale, estimated_tokens_per_sec=est_tps,
547
- )
548
-
549
-
550
- # ── 3. INSTALL plan ─────────────────────────────────────────────────────────
551
- @dataclass
552
- class InstallStep:
553
- name: str
554
- why: str
555
- command: List[str]
556
- requires_admin: bool = False
557
-
558
- def to_json(self) -> Dict[str, Any]:
559
- return {
560
- "name": self.name,
561
- "why": self.why,
562
- "command": self.command,
563
- "requires_admin": self.requires_admin,
564
- "command_plan": command_plan(
565
- self.command,
566
- name=self.name,
567
- purpose="auto_setup_install",
568
- requires_admin=self.requires_admin,
569
- metadata={"why": self.why},
570
- ),
571
- }
572
-
573
-
574
- @dataclass
575
- class InstallPlan:
576
- package_manager: Optional[str]
577
- steps: List[InstallStep]
578
- notes: List[str] = field(default_factory=list)
579
-
580
- def to_json(self) -> Dict[str, Any]:
581
- plan_summary = command_plan_for_commands(
582
- [step.command for step in self.steps],
583
- name="auto_setup_plan",
584
- purpose="auto_setup_install",
585
- metadata={"package_manager": self.package_manager},
586
- )
587
- return {
588
- "package_manager": self.package_manager,
589
- "steps": [s.to_json() for s in self.steps],
590
- "notes": self.notes,
591
- "command_plan": plan_summary,
592
- "confirmation_token": plan_summary["confirmation_token"],
593
- }
594
-
595
-
596
- # 패키지 카탈로그: 핵심 의존성을 OS별 명령으로 매핑
597
- _PKG_MAP: Dict[str, Dict[str, Tuple[str, ...]]] = {
598
- # name : { pm : (cmd parts) }
599
- "python3.11+": {
600
- "winget": ("winget", "install", "-e", "--id", "Python.Python.3.11"),
601
- "brew": ("brew", "install", "python@3.11"),
602
- "apt": ("apt-get", "install", "-y", "python3.11"),
603
- "dnf": ("dnf", "install", "-y", "python3.11"),
604
- },
605
- "node20": {
606
- "winget": ("winget", "install", "-e", "--id", "OpenJS.NodeJS.LTS"),
607
- "brew": ("brew", "install", "node@20"),
608
- "apt": ("apt-get", "install", "-y", "nodejs"),
609
- "dnf": ("dnf", "install", "-y", "nodejs"),
610
- },
611
- "ollama": {
612
- "brew": ("brew", "install", "ollama"),
613
- "winget": ("winget", "install", "-e", "--id", "Ollama.Ollama"),
614
- "apt": ("sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh"),
615
- },
616
- "huggingface-cli": {
617
- "brew": ("pip3", "install", "--upgrade", "huggingface_hub"),
618
- "winget": ("pip", "install", "--upgrade", "huggingface_hub"),
619
- "apt": ("pip3", "install", "--upgrade", "huggingface_hub"),
620
- "dnf": ("pip3", "install", "--upgrade", "huggingface_hub"),
621
- },
622
- }
623
-
624
-
625
- def plan(profile: SystemProfile, rec: Recommendation) -> InstallPlan:
626
- """③ INSTALL — 추천을 만족시키는 *명령 계획* 을 만든다. 실행하지 않는다."""
627
- pm = profile.package_manager
628
- steps: List[InstallStep] = []
629
- notes: List[str] = []
630
-
631
- def need(name: str, why: str) -> None:
632
- cmd_tuple = _PKG_MAP.get(name, {}).get(pm or "")
633
- if cmd_tuple:
634
- steps.append(InstallStep(
635
- name=name, why=why,
636
- command=list(cmd_tuple),
637
- requires_admin=(cmd_tuple[0] in ("apt-get", "dnf", "pacman")),
638
- ))
639
- else:
640
- notes.append(f"패키지 매니저 어댑터 없음: {name} ({pm}) — 수동 설치 필요")
641
-
642
- if sys.version_info < (3, 11):
643
- need("python3.11+", "Lattice AI 서버는 Python 3.11 이상이 필요합니다.")
644
- if not _which("node"):
645
- need("node20", "VSCode 확장 / npm CLI 부트스트랩에 필요")
646
-
647
- # 런타임별 추가
648
- if rec.runtime == "mlx" and not _has_module("mlx_vlm"):
649
- steps.append(InstallStep(
650
- name="mlx-vlm", why="Apple Silicon 멀티모달 추론",
651
- command=["pip3", "install", "--upgrade", "mlx-vlm"],
652
- ))
653
- if rec.runtime in {"llama.cpp", "ollama"} and not _which("ollama"):
654
- need("ollama", "llama.cpp 가중치를 가장 쉽게 받는 경로")
655
- if rec.runtime == "lmstudio" and not _which("lms"):
656
- notes.append("LM Studio CLI(lms)를 찾지 못했습니다. https://lmstudio.ai/download 에서 설치하면 Windows/macOS/Linux 모델 다운로드와 GPU 백엔드를 자동 감지합니다.")
657
- if rec.runtime == "vllm" and not _has_module("vllm"):
658
- steps.append(InstallStep(
659
- name="vllm", why="NVIDIA CUDA/WSL/Linux 서버형 추론",
660
- command=["pip3", "install", "--upgrade", "vllm", "huggingface_hub"],
661
- ))
662
- if profile.gpu.vendor == "nvidia" and not profile.cuda_available:
663
- notes.append("NVIDIA GPU는 감지됐지만 CUDA/nvidia-smi를 찾지 못했습니다. Windows에서는 NVIDIA 드라이버와 CUDA Toolkit 설치 후 재검사를 권장합니다.")
664
- if profile.os == "windows" and profile.gpu.vendor == "nvidia" and not profile.is_wsl:
665
- notes.append("vLLM은 Windows native보다 WSL2/Linux에서 안정적입니다. Windows 데스크톱은 LM Studio 또는 Ollama GPU 경로를 먼저 권장합니다.")
666
-
667
- if not _which("huggingface-cli"):
668
- need("huggingface-cli", "추천 모델 가중치 다운로드용")
669
-
670
- # 모델 가중치 풀
671
- model_command = ["huggingface-cli", "download", rec.model_id, "--quiet"]
672
- if rec.runtime == "ollama":
673
- lower = rec.model_id.lower()
674
- if "gemma-4-31b" in lower:
675
- model_command = ["ollama", "pull", "hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M"]
676
- elif "gemma-4-12b" in lower:
677
- model_command = ["ollama", "pull", "hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M"]
678
- elif "llama-4-scout" in lower:
679
- model_command = ["ollama", "pull", "hf.co/ggml-org/Llama-4-Scout-17B-16E-Instruct-GGUF:Q4_K_M"]
680
- elif "qwen3-vl-8b" in lower:
681
- model_command = ["ollama", "pull", "qwen3-vl:8b"]
682
- elif "qwen3-vl-4b" in lower:
683
- model_command = ["ollama", "pull", "qwen3-vl:4b"]
684
- elif rec.runtime == "lmstudio":
685
- model_command = ["lms", "get", rec.model_id]
686
- steps.append(InstallStep(
687
- name=f"weights:{rec.model_id}",
688
- why="추론에 사용할 모델 가중치",
689
- command=model_command,
690
- ))
691
-
692
- return InstallPlan(package_manager=pm, steps=steps, notes=notes)
693
-
694
-
695
- def apply_plan(
696
- plan_obj: InstallPlan,
697
- *,
698
- confirm: bool = False,
699
- confirmation_token: Optional[str] = None,
700
- ) -> List[Dict[str, Any]]:
701
- """위험: 실제로 설치 명령을 실행한다. ``confirm`` and token are required."""
702
- if not confirm:
703
- raise RuntimeError("refuse to apply: pass confirm=True")
704
- expected = plan_obj.to_json().get("confirmation_token")
705
- if str(confirmation_token or "").strip() != expected:
706
- raise CommandConfirmationError("refuse to apply: confirmation token mismatch")
707
- results: List[Dict[str, Any]] = []
708
- for step in plan_obj.steps:
709
- step_plan = command_plan(
710
- step.command,
711
- name=step.name,
712
- purpose="auto_setup_install",
713
- requires_admin=step.requires_admin,
714
- metadata={"why": step.why},
715
- )
716
- try:
717
- require_command_confirmation(
718
- step.command,
719
- step_plan["confirmation_token"],
720
- purpose="auto_setup_install",
721
- )
722
- append_process_audit_event("installer_command", plan=step_plan, status="started")
723
- r = subprocess.run(step.command, capture_output=True, text=True,
724
- timeout=300, check=False)
725
- append_process_audit_event(
726
- "installer_command",
727
- plan=step_plan,
728
- status="finished",
729
- returncode=r.returncode,
730
- stdout=r.stdout,
731
- stderr=r.stderr,
732
- )
733
- results.append({
734
- "name": step.name,
735
- "command_hash": step_plan["command_hash"],
736
- "command_preview": step_plan["command_preview"],
737
- "returncode": r.returncode,
738
- "stdout_tail": (r.stdout or "")[-2000:],
739
- "stderr_tail": (r.stderr or "")[-2000:],
740
- })
741
- except Exception as exc:
742
- append_process_audit_event("installer_command", plan=step_plan, status="error", error=str(exc))
743
- results.append({"name": step.name, "command_hash": step_plan["command_hash"], "error": str(exc)})
744
- return results
745
-
746
-
747
- # ── 4. VERIFY ───────────────────────────────────────────────────────────────
748
- def verify(profile: SystemProfile, rec: Recommendation) -> Dict[str, Any]:
749
- """④ VERIFY — 가벼운 sanity check. 실제 LLM 추론 벤치는 별도 도구로.
750
- 여기서는 ‘설치된 것들이 import 되는가’ + ‘디스크/RAM 여유’ 정도만 본다."""
751
- checks: List[Dict[str, Any]] = []
752
-
753
- def add(label: str, ok: bool, detail: str = "") -> None:
754
- checks.append({"label": label, "ok": ok, "detail": detail})
755
-
756
- add("Python 3.11+", sys.version_info >= (3, 11), platform.python_version())
757
- add("RAM ≥ 4 GB", profile.ram_mb >= 4 * 1024, f"{profile.ram_mb} MB")
758
- add("디스크 여유 ≥ 8 GB", profile.disk_free_mb >= 8 * 1024,
759
- f"{profile.disk_free_mb} MB free")
760
-
761
- if rec.runtime == "mlx":
762
- add("mlx_vlm import", _has_module("mlx_vlm"), "Apple Silicon 멀티모달 런타임")
763
- if rec.runtime in {"llama.cpp", "ollama"}:
764
- add("ollama binary", _which("ollama") is not None,
765
- _which("ollama") or "not found")
766
- if rec.runtime == "lmstudio":
767
- add("LM Studio CLI", _which("lms") is not None, _which("lms") or "not found")
768
- if rec.backend == "cuda":
769
- add("CUDA/nvidia-smi", profile.cuda_available, profile.cuda_version or "not found")
770
-
771
- # CPU/메모리 잠깐 측정
772
- t0 = time.perf_counter()
773
- _ = sum(i * i for i in range(200_000))
774
- cpu_ms = (time.perf_counter() - t0) * 1000
775
- add("CPU latency sample", cpu_ms < 200, f"{cpu_ms:.1f} ms / 200k ops")
776
-
777
- return {
778
- "checks": checks,
779
- "all_pass": all(c["ok"] for c in checks),
780
- }
781
-
782
-
783
- # ── 5. PRESET ───────────────────────────────────────────────────────────────
784
- def preset(profile: SystemProfile, rec: Recommendation) -> Dict[str, Any]:
785
- """⑤ PRESET — UX 분기 + 단축키 + 테마 + MCP 도구 기본값.
786
-
787
- PPT 슬라이드 3 (모드 선택) · 17 (PRESET) 명세를 따른다.
788
- """
789
- # 기본 모드 vs 고급 모드 자동 선택 휴리스틱
790
- advanced = (
791
- profile.gpu.vendor in ("nvidia", "apple") or
792
- profile.ram_mb >= 24 * 1024 or
793
- "code" in (profile.cpu_model or "").lower() # 개발자 머신 추정
794
- )
795
- mode = "advanced" if advanced else "basic"
796
-
797
- # 단축키는 OS별 자연 컨벤션 따름
798
- mod = "Cmd" if profile.os == "darwin" else "Ctrl"
799
- shortcuts = {
800
- "newChat": f"{mod}+N",
801
- "toggleSidebar": f"{mod}+B",
802
- "openGraph": f"{mod}+G",
803
- "search": f"{mod}+K",
804
- "toggleMode": f"{mod}+Shift+M",
805
- "submit": "Enter",
806
- "newline": "Shift+Enter",
807
- }
808
-
809
- # 기본 MCP 도구 (PPT 슬라이드 11 의 기본 5종)
810
- mcp_defaults = [
811
- {"id": "filesystem", "scope": "local", "enabled": True},
812
- {"id": "web-search", "scope": "remote", "enabled": True},
813
- {"id": "code-execute", "scope": "local", "enabled": True},
814
- {"id": "browser-automation","scope": "remote","enabled": False},
815
- {"id": "database", "scope": "remote", "enabled": False},
816
- ]
817
-
818
- # 테마: OS 다크 모드 추종이 기본
819
- theme = {"mode": "auto", # auto | light | dark
820
- "accent": "#6E4AE6", # PPT 슬라이드 19 토큰
821
- "density": "comfortable" if mode == "basic" else "compact"}
822
-
823
- # 다국어: OS locale 기반 추정
824
- locale = os.environ.get("LANG", os.environ.get("LC_ALL", "ko_KR"))
825
- lang = "ko" if locale.lower().startswith("ko") else (
826
- "ja" if locale.lower().startswith("ja") else "en"
827
- )
828
-
829
- return {
830
- "mode": mode,
831
- "model": {"id": rec.model_id, "runtime": rec.runtime,
832
- "backend": rec.backend, "quantization": rec.quantization},
833
- "shortcuts": shortcuts,
834
- "mcp": mcp_defaults,
835
- "theme": theme,
836
- "language": lang,
837
- "tips": (
838
- ["기본 모드는 카드형 액션과 큰 입력창 위주. 언제든 '고급 모드' 로 전환할 수 있어요."]
839
- if mode == "basic"
840
- else ["고급 모드: 사이드바·디테일 패널·파이프라인 도구가 모두 활성화됩니다."]
841
- ),
842
- }
843
-
844
-
845
- # ── orchestrator ────────────────────────────────────────────────────────────
846
- def run_all(*, apply_install: bool = False, confirmation_token: Optional[str] = None) -> Dict[str, Any]:
847
- p = probe()
848
- r = recommend(p)
849
- pl = plan(p, r)
850
- install_results = None
851
- if apply_install:
852
- install_results = apply_plan(pl, confirm=True, confirmation_token=confirmation_token)
853
- v = verify(p, r)
854
- ps = preset(p, r)
855
- return {
856
- "probe": p.to_json(),
857
- "recommend": r.to_json(),
858
- "plan": pl.to_json(),
859
- "install": install_results,
860
- "verify": v,
861
- "preset": ps,
862
- }
863
-
864
-
865
- # ── CLI ────────────────────────────────────────────────────────────────────
866
- def _main() -> int:
867
- parser = argparse.ArgumentParser(prog="auto_setup",
868
- description="Lattice AI zero-config setup")
869
- sub = parser.add_subparsers(dest="cmd", required=True)
870
- sub.add_parser("probe")
871
- sub.add_parser("recommend")
872
- sp_plan = sub.add_parser("plan")
873
- sp_plan.add_argument("--apply", action="store_true",
874
- help="actually run the install commands (DANGER)")
875
- sp_plan.add_argument("--confirm-token", default=None,
876
- help="confirmation token from the dry-run plan output")
877
- sub.add_parser("verify")
878
- sub.add_parser("preset")
879
- sub.add_parser("all")
880
- args = parser.parse_args()
881
-
882
- if args.cmd == "probe":
883
- print(json.dumps(probe().to_json(), indent=2, ensure_ascii=False)); return 0
884
- if args.cmd == "recommend":
885
- p = probe(); r = recommend(p)
886
- print(json.dumps({"probe": p.to_json(), "recommend": r.to_json()},
887
- indent=2, ensure_ascii=False))
888
- return 0
889
- if args.cmd == "plan":
890
- p = probe(); r = recommend(p); pl = plan(p, r)
891
- out: Dict[str, Any] = {"plan": pl.to_json()}
892
- if args.apply:
893
- out["install"] = apply_plan(pl, confirm=True, confirmation_token=args.confirm_token)
894
- print(json.dumps(out, indent=2, ensure_ascii=False)); return 0
895
- if args.cmd == "verify":
896
- p = probe(); r = recommend(p)
897
- print(json.dumps(verify(p, r), indent=2, ensure_ascii=False)); return 0
898
- if args.cmd == "preset":
899
- p = probe(); r = recommend(p)
900
- print(json.dumps(preset(p, r), indent=2, ensure_ascii=False)); return 0
901
- if args.cmd == "all":
902
- print(json.dumps(run_all(apply_install=False), indent=2, ensure_ascii=False))
903
- return 0
904
- return 2
3
+ from latticeai.setup import auto_setup as _impl
905
4
 
906
5
 
907
6
  if __name__ == "__main__":
908
- raise SystemExit(_main())
7
+ raise SystemExit(_impl._main())
8
+ else:
9
+ import sys
10
+
11
+ sys.modules[__name__] = _impl