ltcai 2.1.0 → 2.2.1
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 +153 -609
- package/auto_setup.py +17 -17
- package/docs/CHANGELOG.md +83 -0
- package/docs/MULTI_AGENT_RUNTIME.md +4 -4
- package/docs/PLUGIN_SDK.md +7 -7
- package/docs/REALTIME_COLLABORATION.md +6 -6
- package/docs/V2_ARCHITECTURE.md +45 -25
- package/docs/WORKFLOW_DESIGNER.md +4 -4
- package/docs/architecture.md +127 -135
- package/docs/kg-schema.md +3 -3
- package/docs/public-deploy.md +2 -3
- package/docs/spec-vs-impl.md +13 -10
- package/knowledge_graph.py +2 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +8 -0
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/model_compat.py +7 -63
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/realtime.py +1 -1
- package/latticeai/core/workflow_engine.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +1 -1
- package/latticeai/services/model_catalog.py +105 -153
- package/latticeai/services/model_recommendation.py +28 -17
- package/latticeai/services/model_runtime.py +2 -2
- package/llm_router.py +80 -92
- package/ltcai_cli.py +2 -3
- package/package.json +8 -3
- package/static/account.html +3 -1
- package/static/activity.html +5 -2
- package/static/admin.html +5 -1
- package/static/agents.html +5 -2
- package/static/chat.html +12 -10
- package/static/css/responsive.css +597 -0
- package/static/css/tokens.css +224 -165
- package/static/graph.html +12 -2
- package/static/lattice-reference.css +366 -739
- package/static/platform.css +45 -16
- package/static/plugins.html +5 -2
- package/static/scripts/admin.js +33 -33
- package/static/scripts/chat.js +109 -42
- package/static/scripts/graph.js +169 -11
- package/static/scripts/ux.js +167 -0
- package/static/workflows.html +5 -2
- package/static/workspace.css +55 -19
- package/static/workspace.html +5 -2
- package/telegram_bot.py +1 -1
package/llm_router.py
CHANGED
|
@@ -29,16 +29,14 @@ executor = ThreadPoolExecutor(max_workers=1)
|
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
31
|
import mlx.core as mx
|
|
32
|
-
from mlx_lm import load as lm_load
|
|
33
32
|
from mlx_vlm import load as vlm_load
|
|
34
33
|
VLM_AVAILABLE = True
|
|
35
|
-
print("✅ MLX-VLM
|
|
34
|
+
print("✅ MLX-VLM is ready for multimodal models.")
|
|
36
35
|
except Exception as e:
|
|
37
36
|
mx = None
|
|
38
|
-
lm_load = None
|
|
39
37
|
vlm_load = None
|
|
40
38
|
VLM_AVAILABLE = False
|
|
41
|
-
print(f"⚠️ MLX
|
|
39
|
+
print(f"⚠️ MLX-VLM unavailable: {e}")
|
|
42
40
|
|
|
43
41
|
BRAND_NAME = "Lattice AI"
|
|
44
42
|
LEGACY_BRAND_PATTERNS = [
|
|
@@ -77,12 +75,12 @@ OPENAI_COMPATIBLE_PROVIDERS = {
|
|
|
77
75
|
"groq": {
|
|
78
76
|
"env_key": "GROQ_API_KEY",
|
|
79
77
|
"base_url": "https://api.groq.com/openai/v1",
|
|
80
|
-
"default_model": "llama-
|
|
78
|
+
"default_model": "meta-llama/llama-4-scout-17b-16e-instruct",
|
|
81
79
|
},
|
|
82
80
|
"together": {
|
|
83
81
|
"env_key": "TOGETHER_API_KEY",
|
|
84
82
|
"base_url": "https://api.together.xyz/v1",
|
|
85
|
-
"default_model": "
|
|
83
|
+
"default_model": "Qwen/Qwen3-VL-32B-Instruct",
|
|
86
84
|
},
|
|
87
85
|
"xai": {
|
|
88
86
|
"env_key": "XAI_API_KEY",
|
|
@@ -93,14 +91,14 @@ OPENAI_COMPATIBLE_PROVIDERS = {
|
|
|
93
91
|
"env_key": "OLLAMA_API_KEY",
|
|
94
92
|
"base_url_env": "OLLAMA_BASE_URL",
|
|
95
93
|
"base_url": "http://localhost:11434/v1",
|
|
96
|
-
"default_model": "
|
|
94
|
+
"default_model": "hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M",
|
|
97
95
|
"api_key_fallback": "ollama",
|
|
98
96
|
},
|
|
99
97
|
"vllm": {
|
|
100
98
|
"env_key": "VLLM_API_KEY",
|
|
101
99
|
"base_url_env": "VLLM_BASE_URL",
|
|
102
100
|
"base_url": "http://localhost:8000/v1",
|
|
103
|
-
"default_model": "
|
|
101
|
+
"default_model": "Qwen/Qwen3-VL-8B-Instruct",
|
|
104
102
|
"api_key_fallback": "vllm",
|
|
105
103
|
},
|
|
106
104
|
"lmstudio": {
|
|
@@ -137,20 +135,18 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
137
135
|
{"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6 via OpenRouter", "family": "Claude"},
|
|
138
136
|
{"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
|
|
139
137
|
{"id": "qwen/qwen3-vl-235b-a22b-instruct", "name": "Qwen3-VL 235B A22B via OpenRouter", "family": "Qwen"},
|
|
140
|
-
{"id": "
|
|
138
|
+
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
|
|
141
139
|
{"id": "x-ai/grok-2", "name": "Grok 2 via OpenRouter", "family": "Grok"},
|
|
142
|
-
{"id": "meta-llama/llama-
|
|
140
|
+
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
|
|
143
141
|
{"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
|
|
144
142
|
],
|
|
145
143
|
"groq": [
|
|
146
|
-
{"id": "
|
|
147
|
-
{"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B Instant", "family": "Llama"},
|
|
148
|
-
{"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B Versatile", "family": "Llama"},
|
|
144
|
+
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
149
145
|
],
|
|
150
146
|
"together": [
|
|
151
147
|
{"id": "Qwen/Qwen3-VL-32B-Instruct", "name": "Qwen3-VL 32B", "family": "Qwen"},
|
|
152
|
-
{"id": "
|
|
153
|
-
{"id": "
|
|
148
|
+
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B", "family": "Gemma"},
|
|
149
|
+
{"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
154
150
|
],
|
|
155
151
|
"xai": [
|
|
156
152
|
{"id": "grok-beta", "name": "Grok Beta", "family": "Grok"},
|
|
@@ -158,6 +154,40 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
158
154
|
],
|
|
159
155
|
}
|
|
160
156
|
|
|
157
|
+
MODEL_SOURCE_BY_FAMILY = {
|
|
158
|
+
"GPT": ("미국", "OpenAI"),
|
|
159
|
+
"Claude": ("미국", "Anthropic"),
|
|
160
|
+
"Qwen": ("중국", "Alibaba"),
|
|
161
|
+
"Llama": ("미국", "Meta"),
|
|
162
|
+
"Gemini": ("미국", "Google"),
|
|
163
|
+
"Grok": ("미국", "xAI"),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def source_metadata_for_model(provider: str, model: Dict[str, str], *, local_server: bool) -> Dict[str, str]:
|
|
168
|
+
family = str(model.get("family") or "")
|
|
169
|
+
country, company = MODEL_SOURCE_BY_FAMILY.get(family, ("미상", provider.title()))
|
|
170
|
+
if local_server:
|
|
171
|
+
execution_method = "내 컴퓨터에서만 실행"
|
|
172
|
+
internet_requirement = "모델을 다운로드할 때만 인터넷 필요; 실행 중에는 필요 없음"
|
|
173
|
+
else:
|
|
174
|
+
execution_method = "인터넷 연결 후 사용"
|
|
175
|
+
internet_requirement = "내 파일이 인터넷으로 전송될 수 있음"
|
|
176
|
+
return {
|
|
177
|
+
"source_country": country,
|
|
178
|
+
"source_company": company,
|
|
179
|
+
"execution_method": execution_method,
|
|
180
|
+
"internet_requirement": internet_requirement,
|
|
181
|
+
"model_name": model.get("name") or model.get("id") or "",
|
|
182
|
+
"source_display_order": [
|
|
183
|
+
"source_country",
|
|
184
|
+
"source_company",
|
|
185
|
+
"execution_method",
|
|
186
|
+
"internet_requirement",
|
|
187
|
+
"model_name",
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
|
|
161
191
|
@dataclass
|
|
162
192
|
class CloudModel:
|
|
163
193
|
provider: str
|
|
@@ -207,37 +237,29 @@ def _resolve_local_hf_model(model_id: str) -> str:
|
|
|
207
237
|
return model_id
|
|
208
238
|
|
|
209
239
|
def ensure_mlx_runtime() -> None:
|
|
210
|
-
global mx,
|
|
211
|
-
if mx is not None and
|
|
240
|
+
global mx, vlm_load, VLM_AVAILABLE
|
|
241
|
+
if mx is not None and vlm_load is not None:
|
|
212
242
|
return
|
|
213
243
|
try:
|
|
214
244
|
import mlx.core as mlx_core
|
|
215
|
-
from
|
|
245
|
+
from mlx_vlm import load as mlx_vlm_load
|
|
216
246
|
|
|
217
247
|
mx = mlx_core
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
from mlx_vlm import load as mlx_vlm_load
|
|
221
|
-
vlm_load = mlx_vlm_load
|
|
222
|
-
VLM_AVAILABLE = True
|
|
223
|
-
except Exception:
|
|
224
|
-
vlm_load = None
|
|
225
|
-
VLM_AVAILABLE = False
|
|
248
|
+
vlm_load = mlx_vlm_load
|
|
249
|
+
VLM_AVAILABLE = True
|
|
226
250
|
mx.set_default_device(mx.gpu)
|
|
227
251
|
except Exception as e:
|
|
228
|
-
raise RuntimeError(f"MLX runtime is not available after install: {e}") from e
|
|
252
|
+
raise RuntimeError(f"MLX-VLM runtime is not available after install: {e}") from e
|
|
229
253
|
|
|
230
254
|
def _mlx_sampler(temperature: float):
|
|
231
255
|
"""Build an MLX sampler callable for the given temperature.
|
|
232
256
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
``generate_step() got an unexpected keyword argument 'temp'``. Both libraries
|
|
237
|
-
accept ``sampler=`` and share make_sampler from mlx_lm.sample_utils.
|
|
257
|
+
Lattice v2.2 keeps local execution on MLX-VLM only. Returning ``None`` lets
|
|
258
|
+
MLX-VLM use its bundled default sampler without pulling another generation
|
|
259
|
+
package into the runtime contract.
|
|
238
260
|
"""
|
|
239
|
-
|
|
240
|
-
return
|
|
261
|
+
_ = temperature
|
|
262
|
+
return None
|
|
241
263
|
|
|
242
264
|
class LLMRouter:
|
|
243
265
|
def __init__(self):
|
|
@@ -331,8 +353,8 @@ class LLMRouter:
|
|
|
331
353
|
return self._load_cloud_model(provider, provider_model, api_key_override=api_key_override, owner=owner)
|
|
332
354
|
|
|
333
355
|
ensure_mlx_runtime()
|
|
334
|
-
if mx is None or
|
|
335
|
-
raise RuntimeError("MLX is not available in this process. Run on Apple Silicon with Metal access.")
|
|
356
|
+
if mx is None or vlm_load is None:
|
|
357
|
+
raise RuntimeError("MLX-VLM is not available in this process. Run on Apple Silicon with Metal access.")
|
|
336
358
|
|
|
337
359
|
cache_key = f"{model_id}_{draft_model_id}" if draft_model_id else model_id
|
|
338
360
|
if cache_key in self._cache:
|
|
@@ -348,24 +370,13 @@ class LLMRouter:
|
|
|
348
370
|
|
|
349
371
|
def _load():
|
|
350
372
|
mx.set_default_device(mx.gpu)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if is_gemma4 and VLM_AVAILABLE:
|
|
355
|
-
print(f"🔄 Loading Target (VLM Mode): {target_model_id}...")
|
|
356
|
-
model, tokenizer = vlm_load(target_model_id)
|
|
357
|
-
else:
|
|
358
|
-
print(f"🔄 Loading Target (LM Mode): {target_model_id}...")
|
|
359
|
-
model, tokenizer = lm_load(target_model_id)
|
|
360
|
-
|
|
361
|
-
# 2. Draft 로드 (Gemma 4는 항상 vlm_load 사용)
|
|
373
|
+
print(f"🔄 Loading Target (VLM Mode): {target_model_id}...")
|
|
374
|
+
model, tokenizer = vlm_load(target_model_id)
|
|
375
|
+
|
|
362
376
|
draft_model = None
|
|
363
377
|
if target_draft_model_id:
|
|
364
378
|
print(f"🔄 Loading Assistant (VLM Mode): {target_draft_model_id}...")
|
|
365
|
-
|
|
366
|
-
draft_model, _ = vlm_load(target_draft_model_id)
|
|
367
|
-
else:
|
|
368
|
-
draft_model, _ = lm_load(target_draft_model_id)
|
|
379
|
+
draft_model, _ = vlm_load(target_draft_model_id)
|
|
369
380
|
print(f"✅ Assistant Ready.")
|
|
370
381
|
|
|
371
382
|
return model, tokenizer, draft_model
|
|
@@ -418,14 +429,16 @@ class LLMRouter:
|
|
|
418
429
|
}]
|
|
419
430
|
for model in provider_models:
|
|
420
431
|
model_id = model["id"]
|
|
432
|
+
local_server = provider in local_server_providers
|
|
421
433
|
items.append({
|
|
422
434
|
"id": f"{provider}:{model_id}",
|
|
423
435
|
"name": model.get("name") or f"{provider.title()} · {model_id}",
|
|
424
436
|
"provider": provider,
|
|
425
437
|
"family": model.get("family"),
|
|
426
|
-
"tag": "local-server" if
|
|
438
|
+
"tag": "local-server" if local_server else "cloud",
|
|
427
439
|
"available": has_key,
|
|
428
440
|
"requires": config["env_key"] if not has_key else None,
|
|
441
|
+
**source_metadata_for_model(provider, model, local_server=local_server),
|
|
429
442
|
})
|
|
430
443
|
custom = os.getenv("LATTICEAI_CLOUD_MODELS") or ""
|
|
431
444
|
for raw in [item.strip() for item in custom.split(",") if item.strip()]:
|
|
@@ -439,6 +452,11 @@ class LLMRouter:
|
|
|
439
452
|
"tag": "cloud",
|
|
440
453
|
"available": bool(os.getenv(config["env_key"]) or config.get("api_key_fallback")),
|
|
441
454
|
"requires": None,
|
|
455
|
+
**source_metadata_for_model(
|
|
456
|
+
provider,
|
|
457
|
+
{"id": model, "name": f"{provider.title()} · {model}", "family": provider.title()},
|
|
458
|
+
local_server=provider in local_server_providers,
|
|
459
|
+
),
|
|
442
460
|
})
|
|
443
461
|
return items
|
|
444
462
|
|
|
@@ -511,25 +529,15 @@ class LLMRouter:
|
|
|
511
529
|
return await self._cloud_generate(cached, message, context, max_tokens, temperature)
|
|
512
530
|
|
|
513
531
|
model, tokenizer, draft_model = self._cache[self._current]
|
|
514
|
-
|
|
515
|
-
prompt = (
|
|
516
|
-
self._build_vlm_prompt(model, tokenizer, message, context, 1)
|
|
517
|
-
if image_data and is_gemma4 and VLM_AVAILABLE
|
|
518
|
-
else self._build_prompt(message, context, tokenizer)
|
|
519
|
-
)
|
|
532
|
+
prompt = self._build_vlm_prompt(model, tokenizer, message, context, 1 if image_data else 0)
|
|
520
533
|
|
|
521
534
|
loop = asyncio.get_event_loop()
|
|
522
535
|
|
|
523
536
|
def _gen():
|
|
524
537
|
import mlx.core as mx
|
|
525
538
|
mx.set_default_device(mx.gpu)
|
|
526
|
-
|
|
527
|
-
if
|
|
528
|
-
from mlx_vlm import generate as vlm_gen
|
|
529
|
-
return vlm_gen(model, tokenizer, prompt=prompt, image=self._prep_image(image_data), max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
530
|
-
else:
|
|
531
|
-
from mlx_lm import generate as lm_gen
|
|
532
|
-
return lm_gen(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
|
|
539
|
+
from mlx_vlm import generate as vlm_gen
|
|
540
|
+
return vlm_gen(model, tokenizer, prompt=prompt, image=self._prep_image(image_data) if image_data else None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
533
541
|
result = await loop.run_in_executor(executor, _gen)
|
|
534
542
|
# mlx-vlm might return a GenerationResult object; extract the text
|
|
535
543
|
if hasattr(result, "text"):
|
|
@@ -567,12 +575,7 @@ class LLMRouter:
|
|
|
567
575
|
return
|
|
568
576
|
|
|
569
577
|
model, tokenizer, draft_model = self._cache[self._current]
|
|
570
|
-
|
|
571
|
-
prompt = (
|
|
572
|
-
self._build_vlm_prompt(model, tokenizer, message, context, 1)
|
|
573
|
-
if image_data and is_gemma4 and VLM_AVAILABLE
|
|
574
|
-
else self._build_prompt(message, context, tokenizer)
|
|
575
|
-
)
|
|
578
|
+
prompt = self._build_vlm_prompt(model, tokenizer, message, context, 1 if image_data else 0)
|
|
576
579
|
loop = asyncio.get_event_loop()
|
|
577
580
|
queue = asyncio.Queue()
|
|
578
581
|
|
|
@@ -580,13 +583,8 @@ class LLMRouter:
|
|
|
580
583
|
import mlx.core as mx
|
|
581
584
|
mx.set_default_device(mx.gpu)
|
|
582
585
|
try:
|
|
583
|
-
|
|
584
|
-
if
|
|
585
|
-
from mlx_vlm import stream_generate as vlm_stream
|
|
586
|
-
gen = vlm_stream(model, tokenizer, prompt=prompt, image=self._prep_image(image_data), max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
587
|
-
else:
|
|
588
|
-
from mlx_lm import stream_generate as lm_stream
|
|
589
|
-
gen = lm_stream(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
|
|
586
|
+
from mlx_vlm import stream_generate as vlm_stream
|
|
587
|
+
gen = vlm_stream(model, tokenizer, prompt=prompt, image=self._prep_image(image_data) if image_data else None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
590
588
|
|
|
591
589
|
for chunk in gen:
|
|
592
590
|
text = chunk.text if hasattr(chunk, "text") else (chunk[0] if isinstance(chunk, tuple) else str(chunk))
|
|
@@ -675,13 +673,8 @@ class LLMRouter:
|
|
|
675
673
|
def _gen():
|
|
676
674
|
import mlx.core as mx
|
|
677
675
|
mx.set_default_device(mx.gpu)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
from mlx_vlm import generate as vlm_gen
|
|
681
|
-
return vlm_gen(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
682
|
-
else:
|
|
683
|
-
from mlx_lm import generate as lm_gen
|
|
684
|
-
return lm_gen(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
|
|
676
|
+
from mlx_vlm import generate as vlm_gen
|
|
677
|
+
return vlm_gen(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
685
678
|
result = await loop.run_in_executor(executor, _gen)
|
|
686
679
|
if hasattr(result, "text"):
|
|
687
680
|
return normalize_branding(result.text)
|
|
@@ -742,13 +735,8 @@ class LLMRouter:
|
|
|
742
735
|
import mlx.core as mx
|
|
743
736
|
mx.set_default_device(mx.gpu)
|
|
744
737
|
try:
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
from mlx_vlm import stream_generate as vlm_stream
|
|
748
|
-
gen = vlm_stream(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
749
|
-
else:
|
|
750
|
-
from mlx_lm import stream_generate as lm_stream
|
|
751
|
-
gen = lm_stream(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
|
|
738
|
+
from mlx_vlm import stream_generate as vlm_stream
|
|
739
|
+
gen = vlm_stream(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
|
|
752
740
|
for chunk in gen:
|
|
753
741
|
text = chunk.text if hasattr(chunk, "text") else (chunk[0] if isinstance(chunk, tuple) else str(chunk))
|
|
754
742
|
loop.call_soon_threadsafe(queue.put_nowait, text)
|
package/ltcai_cli.py
CHANGED
|
@@ -97,9 +97,8 @@ def doctor() -> int:
|
|
|
97
97
|
("FastAPI", _has_module("fastapi"), "required server dependency", True),
|
|
98
98
|
("Uvicorn", _has_module("uvicorn"), "required server dependency", True),
|
|
99
99
|
("OpenAI SDK", _has_module("openai"), "required for cloud providers", False),
|
|
100
|
-
("MLX", _has_module("mlx"), "required for Apple Silicon
|
|
101
|
-
("MLX-
|
|
102
|
-
("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma/VLM models", False),
|
|
100
|
+
("MLX", _has_module("mlx"), "required for Apple Silicon multimodal models", False),
|
|
101
|
+
("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma-4/VLM models", False),
|
|
103
102
|
("Ollama binary", shutil.which("ollama") is not None, "optional local-server engine", False),
|
|
104
103
|
]
|
|
105
104
|
data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -29,8 +29,13 @@
|
|
|
29
29
|
"capture:skills": "node scripts/capture/capture_skills.js",
|
|
30
30
|
"capture:enterprise": "node scripts/capture/capture_enterprise.js",
|
|
31
31
|
"capture:onboarding": "node scripts/capture/capture_onboarding.js",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"release:artifacts": "npm run build:python && npm pack && cd vscode-extension && npm run package:vsix",
|
|
33
|
+
"release:validate": "python3 scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
|
|
34
|
+
"publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
|
|
35
|
+
"publish:pypi": "npm run build:python && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
|
|
36
|
+
"publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
|
|
37
|
+
"publish:openvsx": "cd vscode-extension && npm run package:vsix && npm run publish:openvsx",
|
|
38
|
+
"publish:all": "npm run release:artifacts && npm run release:validate && npm publish ltcai-$npm_package_version.tgz --access public && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl && cd vscode-extension && npm run publish:vscode && npm run publish:openvsx"
|
|
34
39
|
},
|
|
35
40
|
"keywords": [
|
|
36
41
|
"ltcai",
|
package/static/account.html
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
<html lang="ko">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
6
6
|
<title>Lattice AI</title>
|
|
7
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
7
8
|
<link rel="manifest" href="/manifest.json">
|
|
8
9
|
<meta name="theme-color" content="#f3ecff">
|
|
9
10
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap">
|
|
15
16
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
16
17
|
<link rel="stylesheet" href="/static/lattice-reference.css?v=0.3.3">
|
|
18
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1">
|
|
17
19
|
</head>
|
|
18
20
|
<body class="lattice-ref-auth">
|
|
19
21
|
<div class="orb orb-1"></div>
|
package/static/activity.html
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Realtime Activity — Lattice AI</title>
|
|
7
|
-
<
|
|
7
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
8
|
+
<link rel="stylesheet" href="/static/css/tokens.css?v=2.2.1" />
|
|
9
|
+
<link rel="stylesheet" href="/static/platform.css?v=2.2.1" />
|
|
10
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1" />
|
|
8
11
|
</head>
|
|
9
12
|
<body>
|
|
10
13
|
<main>
|
package/static/admin.html
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
7
7
|
<title>Lattice AI Admin</title>
|
|
8
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
8
9
|
<link rel="manifest" href="/manifest.json">
|
|
9
10
|
<meta name="theme-color" content="#f3ecff">
|
|
10
11
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
@@ -15,10 +16,13 @@
|
|
|
15
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap">
|
|
16
17
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
17
18
|
<link rel="stylesheet" href="/static/lattice-reference.css?v=0.3.3">
|
|
19
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1">
|
|
18
20
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
19
21
|
</head>
|
|
20
22
|
|
|
21
23
|
<body class="lattice-ref-admin">
|
|
24
|
+
<div class="sidebar-overlay" onclick="closeAdminRail&&closeAdminRail()"></div>
|
|
25
|
+
<button class="admin-rail-toggle graph-nav-toggle" onclick="toggleAdminRail&&toggleAdminRail()" title="메뉴" aria-label="관리자 메뉴 열기"><i class="ti ti-menu-2"></i></button>
|
|
22
26
|
<aside class="reference-rail admin-rail">
|
|
23
27
|
<div class="rail-brand"><i class="ti ti-shield-lock"></i><strong>LATTICE AI</strong><span>Administrator</span></div>
|
|
24
28
|
<nav>
|
package/static/agents.html
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Multi-Agent Runtime — Lattice AI</title>
|
|
7
|
-
<
|
|
7
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
8
|
+
<link rel="stylesheet" href="/static/css/tokens.css?v=2.2.1" />
|
|
9
|
+
<link rel="stylesheet" href="/static/platform.css?v=2.2.1" />
|
|
10
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1" />
|
|
8
11
|
</head>
|
|
9
12
|
<body>
|
|
10
13
|
<main>
|
package/static/chat.html
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
7
7
|
<title>Lattice AI — All-in-One Multimodal Workspace</title>
|
|
8
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
8
9
|
|
|
9
10
|
<!-- PWA -->
|
|
10
11
|
<link rel="manifest" href="/manifest.json">
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
<!-- ── Setup Wizard Styles ──────────────────────────────────────────── -->
|
|
26
27
|
<link rel="stylesheet" href="/static/lattice-reference.css?v=0.3.3">
|
|
28
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1">
|
|
27
29
|
</head>
|
|
28
30
|
|
|
29
31
|
<body class="lattice-ref-chat">
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
<div class="app-layout">
|
|
40
|
-
<div class="sidebar-overlay" onclick="closeSidebar()"></div>
|
|
42
|
+
<div class="sidebar-overlay" onclick="closeSidebar();closeGraphNav&&closeGraphNav();closeAdminRail&&closeAdminRail()"></div>
|
|
41
43
|
<!-- Sidebar -->
|
|
42
44
|
<aside class="sidebar">
|
|
43
45
|
<div class="sidebar-header">
|
|
@@ -81,6 +83,7 @@
|
|
|
81
83
|
<div class="mode-segmented" id="mode-segmented" role="tablist" aria-label="작업 모드"></div>
|
|
82
84
|
</div>
|
|
83
85
|
<div class="header-pills">
|
|
86
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="테마 전환" aria-label="라이트/다크 테마 전환"><i class="ti ti-moon"></i><i class="ti ti-sun"></i></button>
|
|
84
87
|
<div class="lang-picker" id="header-lang-picker">
|
|
85
88
|
<button class="logout-btn" id="lang-btn" onclick="toggleLangMenu('header-lang-picker')" title="Language"><i class="ti ti-language"></i> Language</button>
|
|
86
89
|
<div class="lang-picker-menu" id="header-lang-picker-menu">
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
</div>
|
|
94
97
|
</header>
|
|
95
98
|
|
|
96
|
-
<div class="acct-modal-overlay" id="acct-modal-overlay">
|
|
99
|
+
<div class="acct-modal-overlay" id="acct-modal-overlay" onclick="if(event.target===this)closeAcctModal()">
|
|
97
100
|
<div class="acct-modal">
|
|
98
101
|
<div class="acct-tabs">
|
|
99
102
|
<button class="acct-tab active" id="tab-profile" onclick="switchAcctTab('profile')" data-i18n="tab_profile">프로필</button>
|
|
@@ -237,8 +240,7 @@
|
|
|
237
240
|
<button class="hdc-btn" onclick="openDataGraph()"><i class="ti ti-arrow-right"></i> 그래프 보기</button>
|
|
238
241
|
</div>
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
<div class="hdc-card hdc-setup" id="home-setup-card" style="display:none">
|
|
243
|
+
<div class="hdc-card hdc-setup" id="home-setup-card">
|
|
242
244
|
<div class="hdc-title"><i class="ti ti-settings-automation"></i> 자동 설정</div>
|
|
243
245
|
<div class="hdc-setup-count">
|
|
244
246
|
<span id="home-setup-num">—</span>
|
|
@@ -279,7 +281,7 @@
|
|
|
279
281
|
<div class="chat-capability-row" id="chat-capability-row">
|
|
280
282
|
<span>파일 생성</span>
|
|
281
283
|
<span>지식 정리</span>
|
|
282
|
-
<span
|
|
284
|
+
<span>내 컴퓨터에서 실행</span>
|
|
283
285
|
</div>
|
|
284
286
|
</div>
|
|
285
287
|
</section>
|
|
@@ -359,12 +361,12 @@
|
|
|
359
361
|
<button class="mode-card" id="mode-card-advanced" onclick="selectMode('advanced')">
|
|
360
362
|
<div class="mode-icon"><i class="ti ti-terminal-2"></i></div>
|
|
361
363
|
<h3 data-i18n="mode_advanced">고급 모드</h3>
|
|
362
|
-
<span data-i18n="mode_advanced_sub"
|
|
364
|
+
<span data-i18n="mode_advanced_sub">같은 기능을 더 자세한 설명으로 표시</span>
|
|
363
365
|
</button>
|
|
364
366
|
<button class="mode-card" id="mode-card-admin" onclick="selectMode('admin')">
|
|
365
367
|
<div class="mode-icon"><i class="ti ti-shield-lock"></i></div>
|
|
366
368
|
<h3 data-i18n="mode_admin">관리자 모드</h3>
|
|
367
|
-
<span data-i18n="mode_admin_sub"
|
|
369
|
+
<span data-i18n="mode_admin_sub">사용자, 정책, 감사 로그 관리</span>
|
|
368
370
|
</button>
|
|
369
371
|
</div>
|
|
370
372
|
</section>
|
|
@@ -375,7 +377,7 @@
|
|
|
375
377
|
<div class="model-panel-header">
|
|
376
378
|
<div>
|
|
377
379
|
<h2 data-i18n="model_switcher">모델 스위처</h2>
|
|
378
|
-
<p style="color: var(--muted); font-size: 12px; margin-top: 4px;" data-i18n="model_switcher_sub"
|
|
380
|
+
<p style="color: var(--muted); font-size: 12px; margin-top: 4px;" data-i18n="model_switcher_sub">제작 국가, 제작 회사, 실행 방식, 인터넷 사용 여부를 확인하고 모델을 선택합니다.</p>
|
|
379
381
|
</div>
|
|
380
382
|
<button class="admin-close" onclick="closeModelPanel()"><i class="ti ti-x"></i></button>
|
|
381
383
|
</div>
|
|
@@ -478,7 +480,7 @@
|
|
|
478
480
|
|
|
479
481
|
<!-- ── 파일 에디터 ── -->
|
|
480
482
|
<div id="file-editor-overlay" class="admin-overlay" style="display:none">
|
|
481
|
-
<section class="admin-panel" style="max-width:720px;height:
|
|
483
|
+
<section class="admin-panel" style="max-width:720px;height:min(85dvh, calc(100dvh - 24px));display:flex;flex-direction:column">
|
|
482
484
|
<div class="admin-header" style="flex-shrink:0">
|
|
483
485
|
<div style="min-width:0;flex:1">
|
|
484
486
|
<h2><i class="ti ti-file-pencil" style="color:var(--accent)"></i> 파일 편집</h2>
|