ltcai 10.2.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -44
- package/docs/CHANGELOG.md +125 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MYPY_BACKLOG.md +65 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +33 -6
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/context.py +23 -3
- package/lattice_brain/graph/_kg_common.py +164 -9
- package/lattice_brain/graph/_kg_contract.py +223 -0
- package/lattice_brain/graph/_kg_fsutil.py +2 -2
- package/lattice_brain/graph/curator.py +4 -4
- package/lattice_brain/graph/discovery.py +12 -1
- package/lattice_brain/graph/discovery_index.py +12 -1
- package/lattice_brain/graph/documents.py +17 -2
- package/lattice_brain/graph/ingest.py +12 -1
- package/lattice_brain/graph/proactive.py +9 -5
- package/lattice_brain/graph/projection.py +12 -1
- package/lattice_brain/graph/provenance.py +12 -1
- package/lattice_brain/graph/retrieval.py +12 -1
- package/lattice_brain/graph/retrieval_docgen.py +12 -1
- package/lattice_brain/graph/retrieval_reads.py +12 -1
- package/lattice_brain/graph/retrieval_vector.py +12 -1
- package/lattice_brain/graph/schema.py +6 -2
- package/lattice_brain/graph/write_master.py +12 -1
- package/lattice_brain/ingestion.py +1 -1
- package/lattice_brain/quality.py +5 -5
- package/lattice_brain/runtime/agent_runtime.py +7 -2
- package/lattice_brain/runtime/contracts.py +2 -1
- package/lattice_brain/runtime/hooks.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/workflow.py +10 -7
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +2 -2
- package/latticeai/api/chat.py +19 -15
- package/latticeai/api/chat_agent_http.py +19 -7
- package/latticeai/api/chat_helpers.py +9 -8
- package/latticeai/api/chat_stream.py +2 -2
- package/latticeai/api/computer_use.py +2 -2
- package/latticeai/api/hooks.py +4 -2
- package/latticeai/api/local_files.py +8 -4
- package/latticeai/api/mcp.py +2 -2
- package/latticeai/api/models.py +5 -5
- package/latticeai/api/permissions.py +2 -2
- package/latticeai/api/search.py +5 -3
- package/latticeai/api/security_dashboard.py +3 -5
- package/latticeai/api/setup.py +3 -3
- package/latticeai/api/static_routes.py +7 -2
- package/latticeai/api/tools.py +9 -4
- package/latticeai/api/workspace.py +25 -22
- package/latticeai/app_factory.py +132 -1346
- package/latticeai/cli/entrypoint.py +1 -1
- package/latticeai/core/agent.py +28 -14
- package/latticeai/core/audit.py +5 -3
- package/latticeai/core/embedding_providers.py +19 -6
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/mcp_catalog.py +3 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/project_sessions.py +1 -1
- package/latticeai/core/run_explain.py +6 -4
- package/latticeai/core/workspace_os.py +0 -4
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_state.py +1 -1
- package/latticeai/core/workspace_snapshots.py +3 -3
- package/latticeai/integrations/telegram_bot.py +52 -3
- package/latticeai/models/router.py +61 -34
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/build_phases.py +1417 -0
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/review_wiring.py +6 -1
- package/latticeai/runtime/runtime_context.py +276 -0
- package/latticeai/runtime/sso_config_runtime.py +1 -1
- package/latticeai/runtime/stages.py +8 -2
- package/latticeai/services/app_context.py +23 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +4 -2
- package/latticeai/services/cloud_streaming.py +5 -3
- package/latticeai/services/command_center.py +1 -1
- package/latticeai/services/evidence_actions.py +2 -1
- package/latticeai/services/hybrid_chat.py +1 -1
- package/latticeai/services/local_knowledge.py +25 -9
- package/latticeai/services/memory_service.py +8 -4
- package/latticeai/services/model_catalog.py +12 -12
- package/latticeai/services/model_engines.py +15 -17
- package/latticeai/services/model_loading.py +1 -1
- package/latticeai/services/model_runtime.py +31 -33
- package/latticeai/services/multimodal_streaming.py +10 -4
- package/latticeai/services/openai_compatible_adapter.py +4 -3
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +12 -3
- package/latticeai/services/search_service.py +1 -1
- package/latticeai/services/triggers.py +1 -1
- package/latticeai/setup/auto_setup.py +1 -1
- package/latticeai/setup/wizard.py +12 -3
- package/latticeai/tools/computer.py +3 -1
- package/latticeai/tools/documents.py +2 -2
- package/package.json +4 -2
- package/scripts/check_current_release_docs.mjs +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-CbdGD-2i.js → Act-BK__Y5VE.js} +1 -1
- package/static/app/assets/{AdminConsole-LgCkXpnf.js → AdminConsole-ZrTyN8_F.js} +1 -1
- package/static/app/assets/{Brain-CoPGJI1L.js → Brain-CYHDXpPB.js} +1 -1
- package/static/app/assets/{BrainHome-gVnaxSwp.js → BrainHome-bbJulb5g.js} +1 -1
- package/static/app/assets/{BrainSignals-ChxAYHtj.js → BrainSignals-CesSPtUP.js} +1 -1
- package/static/app/assets/{Capture-DBtgkHZg.js → Capture-p5upXmz2.js} +1 -1
- package/static/app/assets/{CommandPalette-Ovtv5I0Y.js → CommandPalette-0zkyC0RY.js} +1 -1
- package/static/app/assets/Library-Dt-vABI2.js +1 -0
- package/static/app/assets/{LivingBrain-CNz-6NSd.js → LivingBrain-Bz-h6LBH.js} +1 -1
- package/static/app/assets/{ProductFlow-ePX-Y73J.js → ProductFlow-DAZ7DJCE.js} +1 -1
- package/static/app/assets/{ReviewCard-C4mpvkwH.js → ReviewCard-Bv0WGbY6.js} +1 -1
- package/static/app/assets/{System-BvWNK1zc.js → System-BhrJQSD_.js} +1 -1
- package/static/app/assets/{activity-CiauPV_3.js → activity-AolpZJCH.js} +1 -1
- package/static/app/assets/{bot-Dwct-Q1x.js → bot-CsVsT2nd.js} +1 -1
- package/static/app/assets/{brain-BKDW1F0T.js → brain-vuppco5y.js} +1 -1
- package/static/app/assets/{button-CCB9Kuw7.js → button-1BP1PJZ3.js} +1 -1
- package/static/app/assets/{circle-pause-DNa8WHC5.js → circle-pause-5paeCOtq.js} +1 -1
- package/static/app/assets/{circle-play-8jmxn5mf.js → circle-play-By8mFYRV.js} +1 -1
- package/static/app/assets/{cpu-mxvF3V1I.js → cpu-DvUQxdKe.js} +1 -1
- package/static/app/assets/{download--SmCcx0p.js → download-CFqYG90a.js} +1 -1
- package/static/app/assets/{folder-open-D4Wy5roo.js → folder-open-CVSnp-Dp.js} +1 -1
- package/static/app/assets/{hard-drive-BwQcSPlS.js → hard-drive-Ckr4Vd6-.js} +1 -1
- package/static/app/assets/{index-DDV2YZwM.js → index--0zKF5Ow.js} +3 -3
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{input-DFhSjmLS.js → input-D-JKquXU.js} +1 -1
- package/static/app/assets/{network-Bts7VO94.js → network-DB88FZ2T.js} +1 -1
- package/static/app/assets/{primitives-BuSMEJY8.js → primitives-D8U9OaIo.js} +1 -1
- package/static/app/assets/search-B8jsVTw_.js +1 -0
- package/static/app/assets/{shield-alert-BfTO6X_G.js → shield-alert-DnsvO79P.js} +1 -1
- package/static/app/assets/{textarea-BjctW1oh.js → textarea-4RchwbTa.js} +1 -1
- package/static/app/assets/{useFocusTrap-CE43-LrA.js → useFocusTrap-CFJjy7cS.js} +1 -1
- package/static/app/assets/{useQuery-Bv3ejLL5.js → useQuery-3Reu-A4a.js} +1 -1
- package/static/app/assets/{users-CsNqLZAj.js → users-BTxYksxa.js} +1 -1
- package/static/app/assets/{utils-KFFdVG_t.js → utils-D02Vf-fw.js} +1 -1
- package/static/app/assets/{workspace-wdCvdyPF.js → workspace-CSJNgX-C.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-CBia2Fvm.js +0 -1
- package/static/app/assets/index-CQWdDU3z.css +0 -2
- package/static/app/assets/search-DZzxhWaQ.js +0 -1
|
@@ -236,7 +236,7 @@ class MemoryService:
|
|
|
236
236
|
elif kg_index:
|
|
237
237
|
vector_total = kg_index.get("indexed") or kg_index.get("ready")
|
|
238
238
|
|
|
239
|
-
sources = [
|
|
239
|
+
sources: List[Dict[str, Any]] = [
|
|
240
240
|
{
|
|
241
241
|
"id": "workspace", "type": "workspace", "label": "Workspace Memory",
|
|
242
242
|
"count": len(ws_mem), "size_bytes": ws_bytes if ws_mem else 0,
|
|
@@ -272,7 +272,7 @@ class MemoryService:
|
|
|
272
272
|
"detail": "Local embedding vector index." if kg_index else "Vector index unavailable.",
|
|
273
273
|
},
|
|
274
274
|
]
|
|
275
|
-
total_items = sum((s["count"] or 0) for s in sources)
|
|
275
|
+
total_items = sum(int(s["count"] or 0) for s in sources)
|
|
276
276
|
total_bytes = ws_bytes + kg_bytes + conv_bytes
|
|
277
277
|
healthy = sum(1 for s in sources if s["health"] == "ok")
|
|
278
278
|
overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
|
|
@@ -747,7 +747,11 @@ class MemoryService:
|
|
|
747
747
|
graph_count = int(sources.get("graph", {}).get("count") or 0)
|
|
748
748
|
vector_count = int(sources.get("vector", {}).get("count") or 0)
|
|
749
749
|
query = recall_query.strip() or self._latest_recall_query(user_email=user_email, workspace_id=workspace_id)
|
|
750
|
-
recall
|
|
750
|
+
recall: Dict[str, Any] = (
|
|
751
|
+
self.recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit)
|
|
752
|
+
if query
|
|
753
|
+
else {"query": "", "results": [], "count": 0, "source": "live"}
|
|
754
|
+
)
|
|
751
755
|
recall_items = [
|
|
752
756
|
{
|
|
753
757
|
"id": item.get("id"),
|
|
@@ -760,7 +764,7 @@ class MemoryService:
|
|
|
760
764
|
"matched_terms": item.get("matched_terms") or [],
|
|
761
765
|
"confidence": item.get("confidence") or "low",
|
|
762
766
|
}
|
|
763
|
-
for item in recall.get("results", [])[: max(1, min(limit, 8))]
|
|
767
|
+
for item in list(recall.get("results", []))[: max(1, min(limit, 8))]
|
|
764
768
|
]
|
|
765
769
|
durable_items = workspace_count + conversation_count + graph_count
|
|
766
770
|
# Capability and proof are deliberately separated. The brain is
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
15
|
import sys
|
|
16
|
-
from typing import Dict, List, Optional
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
18
|
from latticeai.services.model_capability_registry import (
|
|
19
19
|
LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
|
|
@@ -34,7 +34,7 @@ from latticeai.services.model_capability_registry import (
|
|
|
34
34
|
get_verified_models as _get_verified_models,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
ENGINE_INSTALLERS = {
|
|
37
|
+
ENGINE_INSTALLERS: Dict[str, Dict[str, Any]] = {
|
|
38
38
|
"local_mlx": {
|
|
39
39
|
"command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm>=0.6.3", "mlx-lm", "huggingface_hub[cli]"],
|
|
40
40
|
"label": "Install MLX runtime",
|
|
@@ -89,10 +89,10 @@ ENGINE_INSTALLERS = {
|
|
|
89
89
|
# user-facing ENGINE_MODEL_CATALOG below is then narrowed to the aggressive 5.2.0
|
|
90
90
|
# policy (latest family generations only, no text-only/legacy weights) and the
|
|
91
91
|
# engine-specific ids/sizes are normalised. See `_finalize_engine_catalog`.
|
|
92
|
-
_RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str,
|
|
92
|
+
_RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = _build_engine_model_catalog()
|
|
93
93
|
# Filled in at module end once the blocklist, alias map and family-version filter
|
|
94
94
|
# are all defined; declared here so the public name exists for static readers.
|
|
95
|
-
ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str,
|
|
95
|
+
ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = {}
|
|
96
96
|
|
|
97
97
|
# Historical aliases preserved (used by _recommended_with_engine_options and resolution).
|
|
98
98
|
# These can be enriched later from registry if needed; kept verbatim for safety.
|
|
@@ -173,7 +173,7 @@ def _version_tuple(raw: str) -> tuple[int, ...]:
|
|
|
173
173
|
return tuple(int(part) for part in raw.split(".") if part.isdigit())
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def _model_family_version(model: Dict[str,
|
|
176
|
+
def _model_family_version(model: Dict[str, Any]) -> Optional[tuple[str, tuple[int, ...]]]:
|
|
177
177
|
text = " ".join(str(model.get(key) or "") for key in ("family", "name", "id"))
|
|
178
178
|
for family, pattern in _VERSIONED_MODEL_PATTERNS:
|
|
179
179
|
match = pattern.search(text)
|
|
@@ -184,9 +184,9 @@ def _model_family_version(model: Dict[str, object]) -> Optional[tuple[str, tuple
|
|
|
184
184
|
return None
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def filter_lower_family_versions(models: List[Dict[str,
|
|
187
|
+
def filter_lower_family_versions(models: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
188
188
|
max_versions: Dict[str, tuple[int, ...]] = {}
|
|
189
|
-
detected: List[tuple[Dict[str,
|
|
189
|
+
detected: List[tuple[Dict[str, Any], Optional[tuple[str, tuple[int, ...]]]]] = []
|
|
190
190
|
for model in models:
|
|
191
191
|
version_info = _model_family_version(model)
|
|
192
192
|
detected.append((model, version_info))
|
|
@@ -214,12 +214,12 @@ _BLOCKED_CATALOG_FRAGMENTS = (
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def _is_blocked_catalog_id(model: Dict[str,
|
|
217
|
+
def _is_blocked_catalog_id(model: Dict[str, Any]) -> bool:
|
|
218
218
|
ident = str(model.get("id") or "").lower()
|
|
219
219
|
return any(fragment in ident for fragment in _BLOCKED_CATALOG_FRAGMENTS)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def _normalize_engine_entry(engine: str, model: Dict[str,
|
|
222
|
+
def _normalize_engine_entry(engine: str, model: Dict[str, Any]) -> Dict[str, Any]:
|
|
223
223
|
"""Apply historical engine-specific id + size conventions to a raw entry.
|
|
224
224
|
|
|
225
225
|
* Non-MLX engines resolve to their canonical packaged id via
|
|
@@ -243,9 +243,9 @@ def _normalize_engine_entry(engine: str, model: Dict[str, object]) -> Dict[str,
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
def _finalize_engine_catalog(
|
|
246
|
-
raw: Dict[str, List[Dict[str,
|
|
247
|
-
) -> Dict[str, List[Dict[str,
|
|
248
|
-
final: Dict[str, List[Dict[str,
|
|
246
|
+
raw: Dict[str, List[Dict[str, Any]]],
|
|
247
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
248
|
+
final: Dict[str, List[Dict[str, Any]]] = {}
|
|
249
249
|
for engine, models in raw.items():
|
|
250
250
|
kept = [
|
|
251
251
|
_normalize_engine_entry(engine, m)
|
|
@@ -33,7 +33,7 @@ from latticeai.services.process_audit import (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def _progress_payload(*args, **kwargs) -> Dict[str,
|
|
36
|
+
def _progress_payload(*args, **kwargs) -> Dict[str, Any]:
|
|
37
37
|
try:
|
|
38
38
|
from latticeai.services.model_runtime import model_download_progress_payload
|
|
39
39
|
except Exception:
|
|
@@ -63,7 +63,7 @@ def windows_binary_candidates(binary: str) -> List[Path]:
|
|
|
63
63
|
local_appdata = os.environ.get("LOCALAPPDATA", "")
|
|
64
64
|
program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
|
|
65
65
|
program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
|
66
|
-
candidates = {
|
|
66
|
+
candidates: Dict[str, List[Optional[Path]]] = {
|
|
67
67
|
"ollama": [
|
|
68
68
|
Path(local_appdata) / "Programs" / "Ollama" / "ollama.exe" if local_appdata else None,
|
|
69
69
|
Path(program_files) / "Ollama" / "ollama.exe",
|
|
@@ -111,7 +111,7 @@ def _json_request(
|
|
|
111
111
|
headers: Optional[Dict[str, str]] = None,
|
|
112
112
|
payload: Optional[Dict[str, Any]] = None,
|
|
113
113
|
timeout: float = 10.0,
|
|
114
|
-
) -> Dict[str,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
115
|
data = None
|
|
116
116
|
req_headers = dict(headers or {})
|
|
117
117
|
if payload is not None:
|
|
@@ -211,8 +211,6 @@ def get_openai_compatible_server_models(provider: str) -> List[str]:
|
|
|
211
211
|
if provider == "lmstudio":
|
|
212
212
|
models = []
|
|
213
213
|
for item in get_lmstudio_models():
|
|
214
|
-
if not isinstance(item, dict):
|
|
215
|
-
continue
|
|
216
214
|
key = str(item.get("key") or "").strip()
|
|
217
215
|
loaded_instances = item.get("loaded_instances") or []
|
|
218
216
|
if loaded_instances:
|
|
@@ -360,7 +358,7 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
360
358
|
raise ModelRuntimeError(status_code=500, detail="llama.cpp 서버가 모델을 자동 로드하지 못했습니다.")
|
|
361
359
|
|
|
362
360
|
|
|
363
|
-
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str,
|
|
361
|
+
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str, Any]:
|
|
364
362
|
ollama = local_binary("ollama")
|
|
365
363
|
if not ollama:
|
|
366
364
|
raise ModelRuntimeError(status_code=400, detail="Ollama가 설치되지 않았습니다.")
|
|
@@ -447,7 +445,7 @@ def get_ollama_pulled_models() -> set:
|
|
|
447
445
|
return set()
|
|
448
446
|
|
|
449
447
|
|
|
450
|
-
def engine_support_status(engine: str) -> Dict[str,
|
|
448
|
+
def engine_support_status(engine: str) -> Dict[str, Any]:
|
|
451
449
|
if engine != "vllm":
|
|
452
450
|
return {"supported": True, "reason": None}
|
|
453
451
|
is_apple_silicon = sys.platform == "darwin" and platform.machine() == "arm64"
|
|
@@ -470,7 +468,7 @@ def _engine_install_command(
|
|
|
470
468
|
if engine not in ENGINE_INSTALLERS:
|
|
471
469
|
raise ModelRuntimeError(status_code=400, detail="지원하지 않는 엔진입니다.")
|
|
472
470
|
installer = ENGINE_INSTALLERS[engine]
|
|
473
|
-
required_binary = installer.get("requires_binary")
|
|
471
|
+
required_binary = str(installer.get("requires_binary") or "")
|
|
474
472
|
if required_binary and shutil.which(required_binary) is None:
|
|
475
473
|
raise ModelRuntimeError(status_code=400, detail=f"{required_binary}가 설치되어 있지 않아 자동 설치할 수 없습니다.")
|
|
476
474
|
command = list(installer["command"])
|
|
@@ -528,16 +526,16 @@ def install_engine(
|
|
|
528
526
|
},
|
|
529
527
|
) from exc
|
|
530
528
|
|
|
531
|
-
run_kwargs = {
|
|
532
|
-
"cwd": cwd,
|
|
533
|
-
"capture_output": True,
|
|
534
|
-
"text": True,
|
|
535
|
-
"timeout": 900,
|
|
536
|
-
"check": False,
|
|
537
|
-
}
|
|
538
529
|
try:
|
|
539
530
|
append_process_audit_event("engine_install", plan=plan, status="started")
|
|
540
|
-
completed = subprocess.run(
|
|
531
|
+
completed = subprocess.run(
|
|
532
|
+
command,
|
|
533
|
+
cwd=cwd,
|
|
534
|
+
capture_output=True,
|
|
535
|
+
text=True,
|
|
536
|
+
timeout=900,
|
|
537
|
+
check=False,
|
|
538
|
+
)
|
|
541
539
|
except subprocess.TimeoutExpired:
|
|
542
540
|
append_process_audit_event("engine_install", plan=plan, status="timeout")
|
|
543
541
|
raise ModelRuntimeError(status_code=408, detail="엔진 설치 시간이 초과되었습니다.")
|
|
@@ -603,7 +601,7 @@ async def _smoke_test_loaded_model(
|
|
|
603
601
|
*,
|
|
604
602
|
api_key_override: Optional[str] = None,
|
|
605
603
|
model_router: Any = None,
|
|
606
|
-
) -> Dict[str,
|
|
604
|
+
) -> Dict[str, Any]:
|
|
607
605
|
"""로드 직후 짧은 채팅 테스트를 돌려 ready_to_chat 여부를 판정한다.
|
|
608
606
|
|
|
609
607
|
Cloud models are skipped to avoid cost.
|
|
@@ -231,7 +231,7 @@ async def prepare_and_load_model_stream(
|
|
|
231
231
|
if compatibility.get("supported") is False:
|
|
232
232
|
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
233
233
|
|
|
234
|
-
work_queue: "queue.Queue[Dict[str,
|
|
234
|
+
work_queue: "queue.Queue[Dict[str, Any]]" = queue.Queue()
|
|
235
235
|
work_result: Dict[str, object] = {}
|
|
236
236
|
|
|
237
237
|
def emit_progress(payload: Dict[str, object]) -> None:
|
|
@@ -269,10 +269,10 @@ def _json_request(
|
|
|
269
269
|
url: str,
|
|
270
270
|
*,
|
|
271
271
|
method: str = "GET",
|
|
272
|
-
payload: Optional[Dict[str,
|
|
272
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
273
273
|
headers: Optional[Dict[str, str]] = None,
|
|
274
274
|
timeout: float = 10.0,
|
|
275
|
-
) -> Dict[str,
|
|
275
|
+
) -> Dict[str, Any]:
|
|
276
276
|
data = None
|
|
277
277
|
req_headers = dict(headers or {})
|
|
278
278
|
if payload is not None:
|
|
@@ -299,12 +299,12 @@ def ensure_lmstudio_server() -> None:
|
|
|
299
299
|
return _ensure_lmstudio_server()
|
|
300
300
|
|
|
301
301
|
|
|
302
|
-
_LMSTUDIO_MODELS_CACHE: List[Dict[str,
|
|
302
|
+
_LMSTUDIO_MODELS_CACHE: List[Dict[str, Any]] = []
|
|
303
303
|
_LMSTUDIO_MODELS_CACHE_TS: float = 0.0
|
|
304
304
|
_LMSTUDIO_MODELS_CACHE_TTL: float = 10.0
|
|
305
305
|
|
|
306
306
|
|
|
307
|
-
def get_lmstudio_models(*, force: bool = False) -> List[Dict[str,
|
|
307
|
+
def get_lmstudio_models(*, force: bool = False) -> List[Dict[str, Any]]:
|
|
308
308
|
global _LMSTUDIO_MODELS_CACHE, _LMSTUDIO_MODELS_CACHE_TS
|
|
309
309
|
if not force and time.monotonic() - _LMSTUDIO_MODELS_CACHE_TS < _LMSTUDIO_MODELS_CACHE_TTL:
|
|
310
310
|
return _LMSTUDIO_MODELS_CACHE
|
|
@@ -335,15 +335,13 @@ def _lmstudio_candidate_keys(model_name: str) -> List[str]:
|
|
|
335
335
|
return list(dict.fromkeys(candidates))
|
|
336
336
|
|
|
337
337
|
|
|
338
|
-
def _find_lmstudio_model_key(model_name: str, models: List[Dict[str,
|
|
338
|
+
def _find_lmstudio_model_key(model_name: str, models: List[Dict[str, Any]]) -> Optional[str]:
|
|
339
339
|
if not models:
|
|
340
340
|
return None
|
|
341
341
|
candidate_keys = _lmstudio_candidate_keys(model_name)
|
|
342
342
|
exact = []
|
|
343
343
|
fuzzy = []
|
|
344
344
|
for item in models:
|
|
345
|
-
if not isinstance(item, dict):
|
|
346
|
-
continue
|
|
347
345
|
key = str(item.get("key") or "").strip()
|
|
348
346
|
display_name = str(item.get("display_name") or "").strip()
|
|
349
347
|
haystacks = [key.lower(), display_name.lower()]
|
|
@@ -352,10 +350,10 @@ def _find_lmstudio_model_key(model_name: str, models: List[Dict[str, object]]) -
|
|
|
352
350
|
continue
|
|
353
351
|
if any(token and token in hay for token in candidate_keys for hay in haystacks):
|
|
354
352
|
fuzzy.append(key)
|
|
355
|
-
return (exact or fuzzy
|
|
353
|
+
return next(iter(exact or fuzzy), None)
|
|
356
354
|
|
|
357
355
|
|
|
358
|
-
def ensure_lmstudio_model(model_name: str) -> Dict[str,
|
|
356
|
+
def ensure_lmstudio_model(model_name: str) -> Dict[str, Any]:
|
|
359
357
|
ensure_lmstudio_server()
|
|
360
358
|
auth_header = {"Authorization": f"Bearer {os.getenv('LMSTUDIO_API_KEY') or 'lmstudio'}"}
|
|
361
359
|
models = get_lmstudio_models()
|
|
@@ -430,7 +428,7 @@ def ensure_lmstudio_model(model_name: str) -> Dict[str, object]:
|
|
|
430
428
|
"cached": False,
|
|
431
429
|
}
|
|
432
430
|
|
|
433
|
-
def engine_support_status(engine: str) -> Dict[str,
|
|
431
|
+
def engine_support_status(engine: str) -> Dict[str, Any]:
|
|
434
432
|
return _engine_support_status(engine)
|
|
435
433
|
|
|
436
434
|
def hf_model_ready(repo_id: str, provider: str = "local_mlx") -> bool:
|
|
@@ -467,8 +465,8 @@ def model_download_progress_payload(
|
|
|
467
465
|
eta_seconds: Optional[float] = None,
|
|
468
466
|
file: Optional[str] = None,
|
|
469
467
|
indeterminate: bool = False,
|
|
470
|
-
) -> Dict[str,
|
|
471
|
-
payload: Dict[str,
|
|
468
|
+
) -> Dict[str, Any]:
|
|
469
|
+
payload: Dict[str, Any] = {
|
|
472
470
|
"stage": stage,
|
|
473
471
|
"message": message,
|
|
474
472
|
"indeterminate": indeterminate,
|
|
@@ -496,7 +494,7 @@ def estimate_eta_seconds(started_at: float, percent: Optional[float]) -> Optiona
|
|
|
496
494
|
return elapsed * (100.0 - percent) / percent
|
|
497
495
|
|
|
498
496
|
|
|
499
|
-
def hf_repo_files_with_sizes(repo_id: str) -> List[Dict[str,
|
|
497
|
+
def hf_repo_files_with_sizes(repo_id: str) -> List[Dict[str, Any]]:
|
|
500
498
|
from huggingface_hub import HfApi
|
|
501
499
|
|
|
502
500
|
api = HfApi()
|
|
@@ -522,7 +520,7 @@ def download_hf_model(
|
|
|
522
520
|
repo_id: str,
|
|
523
521
|
provider: str = "local_mlx",
|
|
524
522
|
progress_emit=None,
|
|
525
|
-
) -> Dict[str,
|
|
523
|
+
) -> Dict[str, Any]:
|
|
526
524
|
if importlib.util.find_spec("huggingface_hub") is None:
|
|
527
525
|
raise ModelRuntimeError(status_code=400, detail="huggingface_hub가 없습니다. 먼저 MLX runtime 설치를 진행해 주세요.")
|
|
528
526
|
|
|
@@ -695,7 +693,7 @@ def download_hf_model(
|
|
|
695
693
|
return {"model": repo_id, "path": str(target_dir), "cached": False}
|
|
696
694
|
|
|
697
695
|
|
|
698
|
-
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str,
|
|
696
|
+
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str, Any]:
|
|
699
697
|
return _pull_ollama_model_with_progress(model_name, progress_emit)
|
|
700
698
|
|
|
701
699
|
|
|
@@ -727,7 +725,7 @@ def _safe_engine_install_plan(
|
|
|
727
725
|
engine: str,
|
|
728
726
|
*,
|
|
729
727
|
base_dir: Path,
|
|
730
|
-
) -> Optional[Dict[str,
|
|
728
|
+
) -> Optional[Dict[str, Any]]:
|
|
731
729
|
try:
|
|
732
730
|
return _engine_install_plan(engine, base_dir=base_dir)
|
|
733
731
|
except Exception:
|
|
@@ -760,7 +758,7 @@ def engine_status(
|
|
|
760
758
|
r = state.router
|
|
761
759
|
verify_cache = cloud_verify_cache or {}
|
|
762
760
|
cloud_models = r.detected_cloud_models() if r else []
|
|
763
|
-
cloud_by_provider = {}
|
|
761
|
+
cloud_by_provider: Dict[str, List[Dict[str, Any]]] = {}
|
|
764
762
|
for model in cloud_models:
|
|
765
763
|
cloud_by_provider.setdefault(model["provider"], []).append(model)
|
|
766
764
|
|
|
@@ -787,10 +785,8 @@ def engine_status(
|
|
|
787
785
|
|
|
788
786
|
lmstudio_models = []
|
|
789
787
|
downloaded_lmstudio = get_lmstudio_models()
|
|
790
|
-
downloaded_by_key = {}
|
|
788
|
+
downloaded_by_key: Dict[str, Dict[str, Any]] = {}
|
|
791
789
|
for item in downloaded_lmstudio:
|
|
792
|
-
if not isinstance(item, dict):
|
|
793
|
-
continue
|
|
794
790
|
key = str(item.get("key") or "").strip()
|
|
795
791
|
if not key:
|
|
796
792
|
continue
|
|
@@ -823,7 +819,7 @@ def engine_status(
|
|
|
823
819
|
llamacpp_models.append({**m, "pulled": hf_model_ready(repo_id, "llamacpp")})
|
|
824
820
|
llamacpp_models = filter_lower_family_versions(llamacpp_models)
|
|
825
821
|
|
|
826
|
-
local_server_specs = [
|
|
822
|
+
local_server_specs: List[Dict[str, Any]] = [
|
|
827
823
|
{
|
|
828
824
|
"id": "vllm",
|
|
829
825
|
"name": "vLLM",
|
|
@@ -906,7 +902,7 @@ def engine_status(
|
|
|
906
902
|
env_key = next((item.get("requires") for item in cloud_by_provider.get(provider, []) if item.get("requires")), None)
|
|
907
903
|
provider_models = []
|
|
908
904
|
for model in cloud_by_provider.get(provider, []):
|
|
909
|
-
cache = verify_cache.get(model.get("id"))
|
|
905
|
+
cache = verify_cache.get(str(model.get("id") or ""))
|
|
910
906
|
provider_models.append({
|
|
911
907
|
**model,
|
|
912
908
|
"verified": cache.get("ok") if cache else None,
|
|
@@ -1009,7 +1005,7 @@ def normalize_local_model_request(model_id: str, engine: Optional[str] = None) -
|
|
|
1009
1005
|
return model_id
|
|
1010
1006
|
|
|
1011
1007
|
|
|
1012
|
-
def ensure_engine_ready(engine: str, *, state: ModelRuntimeState) -> Dict[str,
|
|
1008
|
+
def ensure_engine_ready(engine: str, *, state: ModelRuntimeState) -> Dict[str, Any]:
|
|
1013
1009
|
engine = "local_mlx" if engine == "mlx" else engine
|
|
1014
1010
|
if engine not in ENGINE_INSTALLERS and engine not in OPENAI_COMPATIBLE_PROVIDERS:
|
|
1015
1011
|
raise ModelRuntimeError(status_code=400, detail=f"지원하지 않는 엔진입니다: {engine}")
|
|
@@ -1065,7 +1061,7 @@ async def _smoke_test_loaded_model(
|
|
|
1065
1061
|
*,
|
|
1066
1062
|
api_key_override: Optional[str] = None,
|
|
1067
1063
|
state: ModelRuntimeState,
|
|
1068
|
-
) -> Dict[str,
|
|
1064
|
+
) -> Dict[str, Any]:
|
|
1069
1065
|
# Delegated to model_engines for server decomp
|
|
1070
1066
|
from .model_engines import _smoke_test_loaded_model as _impl_smoke
|
|
1071
1067
|
return await _impl_smoke(
|
|
@@ -1085,7 +1081,7 @@ async def prepare_and_load_model(
|
|
|
1085
1081
|
allow_download: bool = False,
|
|
1086
1082
|
*,
|
|
1087
1083
|
state: ModelRuntimeState,
|
|
1088
|
-
) -> Dict[str,
|
|
1084
|
+
) -> Dict[str, Any]:
|
|
1089
1085
|
from .model_loading import prepare_and_load_model as _impl
|
|
1090
1086
|
|
|
1091
1087
|
return await _impl(
|
|
@@ -1100,7 +1096,7 @@ async def prepare_and_load_model(
|
|
|
1100
1096
|
)
|
|
1101
1097
|
|
|
1102
1098
|
|
|
1103
|
-
def sse_event(event: str, data: Dict[str,
|
|
1099
|
+
def sse_event(event: str, data: Dict[str, Any]) -> str:
|
|
1104
1100
|
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
|
1105
1101
|
|
|
1106
1102
|
|
|
@@ -1128,7 +1124,7 @@ async def prepare_and_load_model_stream(
|
|
|
1128
1124
|
|
|
1129
1125
|
CLOUD_VERIFY_TTL_SECONDS = 600
|
|
1130
1126
|
|
|
1131
|
-
async def _probe_cloud_model(model_ref: str) -> Dict[str,
|
|
1127
|
+
async def _probe_cloud_model(model_ref: str) -> Dict[str, Any]:
|
|
1132
1128
|
provider, model_name = parse_model_ref(model_ref)
|
|
1133
1129
|
config = OPENAI_COMPATIBLE_PROVIDERS.get(provider)
|
|
1134
1130
|
if not config:
|
|
@@ -1140,12 +1136,14 @@ async def _probe_cloud_model(model_ref: str) -> Dict[str, object]:
|
|
|
1140
1136
|
|
|
1141
1137
|
base_url = os.getenv(config.get("base_url_env", "")) if config.get("base_url_env") else None
|
|
1142
1138
|
base_url = base_url or config.get("base_url")
|
|
1143
|
-
client_kwargs = {"api_key": api_key}
|
|
1144
|
-
if base_url:
|
|
1145
|
-
client_kwargs["base_url"] = base_url
|
|
1146
|
-
|
|
1147
1139
|
try:
|
|
1148
|
-
|
|
1140
|
+
# base_url is passed only when configured: an explicit None is not
|
|
1141
|
+
# the same as omitting the argument.
|
|
1142
|
+
client = (
|
|
1143
|
+
AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
1144
|
+
if base_url
|
|
1145
|
+
else AsyncOpenAI(api_key=api_key)
|
|
1146
|
+
)
|
|
1149
1147
|
await asyncio.wait_for(
|
|
1150
1148
|
client.chat.completions.create(
|
|
1151
1149
|
model=model_name,
|
|
@@ -1246,7 +1244,7 @@ class ModelRuntimeService:
|
|
|
1246
1244
|
adapter_path: Optional[str] = None,
|
|
1247
1245
|
draft_model_id: Optional[str] = None,
|
|
1248
1246
|
allow_download: bool = False,
|
|
1249
|
-
) -> Dict[str,
|
|
1247
|
+
) -> Dict[str, Any]:
|
|
1250
1248
|
return await prepare_and_load_model(
|
|
1251
1249
|
model_id,
|
|
1252
1250
|
request,
|
|
@@ -45,14 +45,20 @@ class MultimodalTurnResult:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class MultimodalAdapter(Protocol):
|
|
48
|
-
async def
|
|
48
|
+
# See CloudLLMAdapter.stream: an async generator is declared with `def`.
|
|
49
|
+
def stream_media(
|
|
49
50
|
self,
|
|
50
51
|
*,
|
|
51
52
|
prompt: str,
|
|
52
53
|
context: str,
|
|
53
54
|
model: Optional[str] = None,
|
|
54
|
-
) -> AsyncIterator[
|
|
55
|
-
"""Yield progress events, then a final event with media_urls.
|
|
55
|
+
) -> AsyncIterator[Any]:
|
|
56
|
+
"""Yield progress events, then a final event with media_urls.
|
|
57
|
+
|
|
58
|
+
Typed as ``Any`` rather than ``Dict[str, Any]`` because adapters are
|
|
59
|
+
supplied by callers: the bridge validates each event's shape rather
|
|
60
|
+
than trusting the annotation.
|
|
61
|
+
"""
|
|
56
62
|
...
|
|
57
63
|
|
|
58
64
|
|
|
@@ -112,7 +118,7 @@ class MultimodalStreamingBridge:
|
|
|
112
118
|
answer_text="\n".join(notes),
|
|
113
119
|
sent_node_ids=list(minimal.node_ids),
|
|
114
120
|
provider=getattr(self._adapter, "provider_name", "multimodal"),
|
|
115
|
-
model=model or getattr(self._adapter, "default_model", ""),
|
|
121
|
+
model=str(model or getattr(self._adapter, "default_model", "")),
|
|
116
122
|
)
|
|
117
123
|
|
|
118
124
|
|
|
@@ -51,10 +51,11 @@ class OpenAICompatibleAdapter:
|
|
|
51
51
|
raise RuntimeError(
|
|
52
52
|
"openai package is required for the cloud streaming adapter"
|
|
53
53
|
) from exc
|
|
54
|
-
|
|
54
|
+
# base_url is only passed when configured: AsyncOpenAI treats an
|
|
55
|
+
# explicit None differently from an omitted argument.
|
|
55
56
|
if self.base_url:
|
|
56
|
-
|
|
57
|
-
return AsyncOpenAI(
|
|
57
|
+
return AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
|
58
|
+
return AsyncOpenAI(api_key=self.api_key)
|
|
58
59
|
|
|
59
60
|
async def stream(
|
|
60
61
|
self,
|
|
@@ -29,6 +29,15 @@ class _RunHandle:
|
|
|
29
29
|
cancel_requested: bool = False
|
|
30
30
|
started: bool = False
|
|
31
31
|
|
|
32
|
+
def is_cancelled(self) -> bool:
|
|
33
|
+
"""Read the flag through a call.
|
|
34
|
+
|
|
35
|
+
`cancel_requested` is set by a *different* task (``cancel``), so a
|
|
36
|
+
reader must not be narrowed by an earlier check in its own frame:
|
|
37
|
+
the value can change between the guard and the poll.
|
|
38
|
+
"""
|
|
39
|
+
return self.cancel_requested
|
|
40
|
+
|
|
32
41
|
|
|
33
42
|
class RunExecutor:
|
|
34
43
|
"""Async task manager for persisted agent/workflow executions."""
|
|
@@ -119,9 +128,9 @@ class RunExecutor:
|
|
|
119
128
|
roles=roles,
|
|
120
129
|
inputs=inputs,
|
|
121
130
|
max_retries=max_retries,
|
|
122
|
-
cancel_requested=
|
|
131
|
+
cancel_requested=handle.is_cancelled,
|
|
123
132
|
)
|
|
124
|
-
if handle.
|
|
133
|
+
if handle.is_cancelled() and (payload.get("run") or {}).get("status") != "cancelled":
|
|
125
134
|
self._cancel_agent_record(run_id, handle.scope, "cancelled after the final result was persisted")
|
|
126
135
|
else:
|
|
127
136
|
self._results[run_id] = payload
|
|
@@ -232,7 +241,7 @@ class RunExecutor:
|
|
|
232
241
|
started_at=run.get("started_at") or _now(),
|
|
233
242
|
)
|
|
234
243
|
result = await asyncio.to_thread(self._execute_workflow_sync, workflow, user_email, handle.scope, inputs)
|
|
235
|
-
if handle.
|
|
244
|
+
if handle.is_cancelled():
|
|
236
245
|
self._cancel_workflow_record(run_id, handle.scope, "cancelled after the current synchronous step completed")
|
|
237
246
|
return
|
|
238
247
|
pause = (
|
|
@@ -330,7 +330,7 @@ class SearchService:
|
|
|
330
330
|
by_id[node_id] = current
|
|
331
331
|
current["score"] = max(float(current["score"]), score)
|
|
332
332
|
current["source_scores"]["graph"] = max(float(current["source_scores"]["graph"]), score)
|
|
333
|
-
context = {"reason": reason}
|
|
333
|
+
context: Dict[str, Any] = {"reason": reason}
|
|
334
334
|
if edge:
|
|
335
335
|
context["relationship"] = {
|
|
336
336
|
"id": edge.get("id"),
|
|
@@ -71,7 +71,7 @@ class TriggerService:
|
|
|
71
71
|
try:
|
|
72
72
|
self._tz = ZoneInfo(self._tz_name)
|
|
73
73
|
except Exception:
|
|
74
|
-
self._tz = ZoneInfo("UTC")
|
|
74
|
+
self._tz = ZoneInfo("UTC")
|
|
75
75
|
|
|
76
76
|
# ── durable state ──────────────────────────────────────────────────────
|
|
77
77
|
def _load_state(self) -> Dict[str, Any]:
|
|
@@ -298,7 +298,7 @@ def _detect_cpu_details(prof_os: str) -> Tuple[str, int, int, List[str]]:
|
|
|
298
298
|
quiet()
|
|
299
299
|
try:
|
|
300
300
|
import ctypes
|
|
301
|
-
kernel32 = ctypes.windll.kernel32
|
|
301
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
|
|
302
302
|
feature_map = {6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand"}
|
|
303
303
|
flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
|
|
304
304
|
except Exception:
|
|
@@ -352,7 +352,7 @@ def _detect_cpu() -> Dict[str, Any]:
|
|
|
352
352
|
quiet()
|
|
353
353
|
try:
|
|
354
354
|
import ctypes
|
|
355
|
-
kernel32 = ctypes.windll.kernel32
|
|
355
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
|
|
356
356
|
feature_map = {
|
|
357
357
|
6: "sse",
|
|
358
358
|
10: "sse2",
|
|
@@ -836,9 +836,18 @@ def get_recommendations(env: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
836
836
|
if cid == "homebrew":
|
|
837
837
|
action = {"type": "url", "url": OFFICIAL_DOWNLOADS["homebrew"], "binary": "brew"}
|
|
838
838
|
elif tools.get("brew") and brew_pkg:
|
|
839
|
-
action = {
|
|
839
|
+
action = {
|
|
840
|
+
"type": "brew",
|
|
841
|
+
"package": brew_pkg,
|
|
842
|
+
"binary": binary or "",
|
|
843
|
+
"official_url": OFFICIAL_DOWNLOADS.get(cid, ""),
|
|
844
|
+
}
|
|
840
845
|
else:
|
|
841
|
-
action = {
|
|
846
|
+
action = {
|
|
847
|
+
"type": "url",
|
|
848
|
+
"url": OFFICIAL_DOWNLOADS.get(cid, ""),
|
|
849
|
+
"binary": binary or "",
|
|
850
|
+
}
|
|
842
851
|
components.append({
|
|
843
852
|
"id": f"component_{cid}", "name": name,
|
|
844
853
|
"subtitle": subtitle, "status": "available",
|
|
@@ -17,7 +17,9 @@ _PLATFORM = platform.system()
|
|
|
17
17
|
|
|
18
18
|
# ── Computer Use ──────────────────────────────────────────────────────────────
|
|
19
19
|
_CU_AVAILABLE = False
|
|
20
|
-
|
|
20
|
+
# Optional dependency: None until _init_computer_use() imports it, and the
|
|
21
|
+
# _CU_AVAILABLE flag is what every call site checks.
|
|
22
|
+
_pyautogui: Any = None
|
|
21
23
|
|
|
22
24
|
def _init_computer_use():
|
|
23
25
|
global _CU_AVAILABLE, _pyautogui
|
|
@@ -51,7 +51,7 @@ def create_docx(title: str, body, filename: str = "document.docx") -> Dict[str,
|
|
|
51
51
|
text = block.strip()
|
|
52
52
|
if text:
|
|
53
53
|
document.add_paragraph(text)
|
|
54
|
-
document.save(target)
|
|
54
|
+
document.save(str(target))
|
|
55
55
|
return {"path": _relative(target), "bytes": target.stat().st_size}
|
|
56
56
|
|
|
57
57
|
|
|
@@ -107,7 +107,7 @@ def create_pptx(title: str, slides: List[Dict[str, Any]], filename: str = "prese
|
|
|
107
107
|
paragraph.text = str(bullet)
|
|
108
108
|
paragraph.level = 0
|
|
109
109
|
|
|
110
|
-
presentation.save(target)
|
|
110
|
+
presentation.save(str(target))
|
|
111
111
|
return {"path": _relative(target), "slides": len(presentation.slides), "bytes": target.stat().st_size}
|
|
112
112
|
|
|
113
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.4.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"test:vscode-extension": "cd vscode-extension && npm run build && cd .. && node --test tests/vscode-extension.test.cjs",
|
|
62
62
|
"check:i18n-namespaces": "node scripts/check_i18n_namespace_coverage.mjs",
|
|
63
63
|
"typecheck:python": "node scripts/run_python.mjs -m mypy",
|
|
64
|
-
"test:coverage": "node scripts/run_python.mjs -m pytest tests/ -q --cov --cov-report=term:skip-covered"
|
|
64
|
+
"test:coverage": "node scripts/run_python.mjs -m pytest tests/ -q --cov --cov-report=term:skip-covered",
|
|
65
|
+
"test:frontend:coverage": "vitest run --coverage"
|
|
65
66
|
},
|
|
66
67
|
"keywords": [
|
|
67
68
|
"ltcai",
|
|
@@ -137,6 +138,7 @@
|
|
|
137
138
|
"@types/react": "^19.2.17",
|
|
138
139
|
"@types/react-dom": "^19.2.3",
|
|
139
140
|
"@vitejs/plugin-react": "^6.0.2",
|
|
141
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
140
142
|
"autoprefixer": "^10.5.0",
|
|
141
143
|
"class-variance-authority": "^0.7.1",
|
|
142
144
|
"clsx": "^2.1.1",
|