ltcai 8.8.0 → 9.0.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 (111) hide show
  1. package/README.md +33 -37
  2. package/auto_setup.py +84 -70
  3. package/docs/CHANGELOG.md +113 -237
  4. package/docs/CODE_REVIEW_2026-07-06.md +764 -0
  5. package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
  6. package/docs/DEVELOPMENT.md +10 -10
  7. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  8. package/docs/ONBOARDING.md +2 -2
  9. package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
  10. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  11. package/docs/TRUST_MODEL.md +5 -1
  12. package/docs/WHY_LATTICE.md +4 -3
  13. package/docs/architecture.md +4 -0
  14. package/docs/kg-schema.md +1 -1
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/archive.py +4 -9
  17. package/lattice_brain/conversations.py +156 -21
  18. package/lattice_brain/embeddings.py +38 -2
  19. package/lattice_brain/graph/_kg_common.py +39 -496
  20. package/lattice_brain/graph/_kg_constants.py +243 -0
  21. package/lattice_brain/graph/_kg_fsutil.py +297 -0
  22. package/lattice_brain/graph/discovery.py +0 -948
  23. package/lattice_brain/graph/discovery_index.py +972 -0
  24. package/lattice_brain/graph/json_utils.py +25 -0
  25. package/lattice_brain/graph/retrieval.py +66 -597
  26. package/lattice_brain/graph/retrieval_docgen.py +210 -0
  27. package/lattice_brain/graph/retrieval_vector.py +460 -0
  28. package/lattice_brain/graph/runtime.py +16 -0
  29. package/lattice_brain/graph/store.py +6 -0
  30. package/lattice_brain/ingestion.py +68 -0
  31. package/lattice_brain/portability.py +1 -9
  32. package/lattice_brain/quality.py +98 -4
  33. package/lattice_brain/runtime/agent_runtime.py +166 -0
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/utils.py +28 -0
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/chat.py +368 -418
  38. package/latticeai/api/chat_helpers.py +227 -0
  39. package/latticeai/api/computer_use.py +149 -31
  40. package/latticeai/api/marketplace.py +11 -0
  41. package/latticeai/api/mcp.py +3 -2
  42. package/latticeai/api/models.py +4 -1
  43. package/latticeai/api/permissions.py +72 -33
  44. package/latticeai/api/setup.py +17 -2
  45. package/latticeai/api/tools.py +105 -62
  46. package/latticeai/app_factory.py +101 -296
  47. package/latticeai/core/agent.py +25 -7
  48. package/latticeai/core/io_utils.py +37 -0
  49. package/latticeai/core/legacy_compatibility.py +1 -1
  50. package/latticeai/core/local_embeddings.py +2 -4
  51. package/latticeai/core/marketplace.py +33 -2
  52. package/latticeai/core/mcp_catalog.py +450 -0
  53. package/latticeai/core/mcp_registry.py +2 -441
  54. package/latticeai/core/sessions.py +11 -3
  55. package/latticeai/core/tool_registry.py +15 -4
  56. package/latticeai/core/users.py +4 -9
  57. package/latticeai/core/workspace_os.py +1 -1
  58. package/latticeai/core/workspace_os_utils.py +3 -17
  59. package/latticeai/integrations/telegram_bot.py +7 -2
  60. package/latticeai/models/model_providers.py +111 -0
  61. package/latticeai/models/router.py +58 -136
  62. package/latticeai/runtime/audit_runtime.py +27 -16
  63. package/latticeai/runtime/automation_runtime.py +9 -0
  64. package/latticeai/runtime/bootstrap.py +1 -1
  65. package/latticeai/runtime/history_runtime.py +163 -0
  66. package/latticeai/runtime/namespace_runtime.py +173 -0
  67. package/latticeai/runtime/network_config_runtime.py +56 -0
  68. package/latticeai/runtime/sso_config_runtime.py +128 -0
  69. package/latticeai/runtime/user_key_runtime.py +106 -0
  70. package/latticeai/services/app_context.py +1 -0
  71. package/latticeai/services/architecture_readiness.py +2 -2
  72. package/latticeai/services/memory_service.py +213 -0
  73. package/latticeai/services/model_engines.py +79 -12
  74. package/latticeai/services/model_runtime.py +24 -4
  75. package/latticeai/services/platform_runtime.py +9 -1
  76. package/latticeai/services/process_audit.py +208 -0
  77. package/latticeai/services/product_readiness.py +11 -11
  78. package/latticeai/services/review_queue.py +64 -11
  79. package/latticeai/services/run_executor.py +21 -0
  80. package/latticeai/services/search_service.py +106 -30
  81. package/latticeai/services/setup_detection.py +80 -0
  82. package/latticeai/services/tool_dispatch.py +66 -0
  83. package/latticeai/services/workspace_service.py +15 -0
  84. package/package.json +1 -1
  85. package/scripts/check_i18n_literals.mjs +20 -8
  86. package/scripts/i18n_literal_allowlist.json +34 -0
  87. package/scripts/lint_frontend.mjs +6 -2
  88. package/setup_wizard.py +196 -74
  89. package/src-tauri/Cargo.lock +1 -1
  90. package/src-tauri/Cargo.toml +1 -1
  91. package/src-tauri/tauri.conf.json +1 -1
  92. package/static/app/asset-manifest.json +11 -11
  93. package/static/app/assets/{Act-C7K9wsO9.js → Act-21lIXx2E.js} +1 -1
  94. package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
  95. package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-BA7Z2Q1u.js} +1 -1
  96. package/static/app/assets/{Library-Cgj-EF50.js → Library-bFMtyni3.js} +1 -1
  97. package/static/app/assets/System-K6krGCqn.js +1 -0
  98. package/static/app/assets/index-C4R3ws30.js +17 -0
  99. package/static/app/assets/index-ChSeOB02.css +2 -0
  100. package/static/app/assets/primitives-sQU3it5I.js +1 -0
  101. package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-DK3Fd_lR.js} +1 -1
  102. package/static/app/index.html +2 -2
  103. package/static/css/tokens.css +4 -2
  104. package/static/sw.js +1 -1
  105. package/tools/local_files.py +6 -0
  106. package/latticeai/runtime/sso_runtime.py +0 -52
  107. package/static/app/assets/Brain-I1OSzxJu.js +0 -321
  108. package/static/app/assets/System-D1Lkei3I.js +0 -1
  109. package/static/app/assets/index--P0ksosz.js +0 -17
  110. package/static/app/assets/index-DCh5AoXt.css +0 -2
  111. package/static/app/assets/primitives-BLqaKk5g.js +0 -1
@@ -34,502 +34,43 @@ except Exception: # pragma: no cover - v2 schema is optional at import time
34
34
  _exec_script = None # type: ignore[assignment]
35
35
 
36
36
  from ..embeddings import LocalEmbeddingModel
37
+ from .json_utils import _json, _safe_loads
38
+ from .runtime import get_llm_router, set_llm_router
37
39
 
38
40
  # Default read source for the graph queries: v2 reconstruction views.
39
41
  # Override with LATTICEAI_KG_READ_V2=0 to fall back to the legacy tables.
40
42
  _READ_FROM_V2_DEFAULT = os.getenv("LATTICEAI_KG_READ_V2", "1") != "0"
41
43
 
42
- # Bump when the v2 projection layout changes (columns, normalization rules).
43
- # On init, a stale projection is dropped and rebuilt from the authoritative
44
- # legacy tables safe because nodes_v2/edges_v2 only ever hold a derived view.
45
- # v4: summary nullable + verbatim (byte-faithful) projection of legacy values.
46
- _PROJECTION_VERSION = 4
47
- _KG_DB_FORMAT_VERSION = 4
48
- _KG_DB_FORMAT_KEY = "db_format_version"
49
- _V2_WRITE_MASTER_KEY = "v2_write_mastered_at"
50
-
51
- _llm_router_ref = None
52
-
53
-
54
- def set_llm_router(router_instance):
55
- global _llm_router_ref
56
- _llm_router_ref = router_instance
57
-
58
-
59
- GRAPH_SCHEMA_VERSION = 1
60
-
61
- LOCAL_TEXT_EXTENSIONS = {".txt", ".md"}
62
- LOCAL_CODE_EXTENSIONS = {
63
- ".py",
64
- ".js",
65
- ".ts",
66
- ".tsx",
67
- ".jsx",
68
- ".html",
69
- ".css",
70
- ".json",
71
- ".yaml",
72
- ".yml",
73
- ".xml",
74
- ".sql",
75
- ".sh",
76
- ".zsh",
77
- ".toml",
78
- ".ini",
79
- }
80
- LOCAL_DOCUMENT_EXTENSIONS = {".pdf", ".docx"}
81
- LOCAL_SPREADSHEET_EXTENSIONS = {".xlsx", ".csv"}
82
- LOCAL_SLIDE_EXTENSIONS = {".pptx"}
83
- LOCAL_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
84
- LOCAL_SUPPORTED_EXTENSIONS = (
85
- LOCAL_TEXT_EXTENSIONS
86
- | LOCAL_CODE_EXTENSIONS
87
- | LOCAL_DOCUMENT_EXTENSIONS
88
- | LOCAL_SPREADSHEET_EXTENSIONS
89
- | LOCAL_SLIDE_EXTENSIONS
90
- | LOCAL_IMAGE_EXTENSIONS
91
- )
92
-
93
- LOCAL_SIZE_LIMITS = {
94
- "text": 4_000_000,
95
- "code": 4_000_000,
96
- "pdf": 50_000_000,
97
- "document": 50_000_000,
98
- "spreadsheet": 50_000_000,
99
- "slide_deck": 50_000_000,
100
- "image": 100_000_000,
101
- }
102
-
103
- COMMON_EXCLUDED_DIRS = {
104
- ".git",
105
- "node_modules",
106
- ".venv",
107
- "venv",
108
- "env",
109
- "__pycache__",
110
- ".pytest_cache",
111
- ".mypy_cache",
112
- ".ruff_cache",
113
- ".next",
114
- ".nuxt",
115
- ".turbo",
116
- "dist",
117
- "build",
118
- "target",
119
- "out",
120
- "coverage",
121
- ".cache",
122
- ".config",
123
- ".ssh",
124
- ".gnupg",
125
- ".docker",
126
- ".kube",
127
- ".aws",
128
- ".azure",
129
- ".npm",
130
- ".pnpm-store",
131
- ".yarn",
132
- ".bun",
133
- ".cargo",
134
- ".rustup",
135
- ".pyenv",
136
- ".conda",
137
- ".local",
138
- ".claude",
139
- ".codex",
140
- ".cursor",
141
- ".copilot",
142
- ".antigravity",
143
- ".antigravity-ide",
144
- }
145
-
146
- COMMON_EXCLUDED_FILE_NAMES = {
147
- ".env",
148
- ".env.local",
149
- ".env.production",
150
- ".env.development",
151
- "id_rsa",
152
- "id_ed25519",
153
- "authorized_keys",
154
- "known_hosts",
155
- "credentials.json",
156
- "service-account.json",
157
- "token.json",
158
- "secrets.json",
159
- "cookies",
160
- "login data",
161
- "history",
162
- "web data",
163
- ".ds_store",
164
- "thumbs.db",
165
- }
166
- COMMON_EXCLUDED_FILE_SUFFIXES = {
167
- ".pem",
168
- ".key",
169
- ".p12",
170
- ".pfx",
171
- ".kdbx",
172
- ".wallet",
173
- ".sqlite",
174
- ".db",
175
- ".exe",
176
- ".dll",
177
- ".sys",
178
- ".msi",
179
- ".dmg",
180
- ".pkg",
181
- ".app",
182
- ".zip",
183
- ".tar",
184
- ".gz",
185
- ".7z",
186
- ".rar",
187
- ".mp4",
188
- ".mov",
189
- ".mp3",
190
- ".wav",
191
- ".tmp",
192
- ".bak",
193
- ".lock",
194
- }
195
- SENSITIVE_PATH_KEYWORDS = {
196
- "secret",
197
- "secrets",
198
- "token",
199
- "password",
200
- "passwd",
201
- "credential",
202
- "credentials",
203
- "private",
204
- "key",
205
- "wallet",
206
- "recovery",
207
- "seed",
208
- "mnemonic",
209
- "cookie",
210
- "session",
211
- "auth",
212
- "oauth",
213
- "certificate",
214
- "cert",
215
- "api_key",
216
- "apikey",
217
- }
218
-
219
- MACOS_EXCLUDED_PREFIXES = (
220
- "/System",
221
- "/Library",
222
- "/Applications",
223
- "/private",
224
- "/tmp",
225
- "/var",
226
- )
227
- WINDOWS_EXCLUDED_NAMES = {
228
- "windows",
229
- "program files",
230
- "program files (x86)",
231
- "programdata",
232
- "appdata",
233
- "$recycle.bin",
234
- "system volume information",
235
- "recovery",
236
- "perflogs",
237
- "intel",
238
- "amd",
239
- "nvidia",
240
- }
241
- LINUX_EXCLUDED_PREFIXES = (
242
- "/bin",
243
- "/boot",
244
- "/dev",
245
- "/etc",
246
- "/lib",
247
- "/lib64",
248
- "/proc",
249
- "/root",
250
- "/run",
251
- "/sbin",
252
- "/sys",
253
- "/tmp",
254
- "/usr",
255
- "/var",
256
- "/snap",
257
- "/lost+found",
44
+ # Static constants (projection/format versions, local-ingestion classification
45
+ # tables, OS exclusion lists) live in ._kg_constants; re-exported here so every
46
+ # existing ``from ._kg_common import <CONST>`` site is unaffected.
47
+ from ._kg_constants import ( # noqa: E402
48
+ _KG_DB_FORMAT_KEY,
49
+ _KG_DB_FORMAT_VERSION,
50
+ _PROJECTION_VERSION,
51
+ _V2_WRITE_MASTER_KEY,
52
+ COMMON_EXCLUDED_DIRS,
53
+ COMMON_EXCLUDED_FILE_NAMES,
54
+ COMMON_EXCLUDED_FILE_SUFFIXES,
55
+ GRAPH_SCHEMA_VERSION,
56
+ LINUX_EXCLUDED_PREFIXES,
57
+ LOCAL_CODE_EXTENSIONS,
58
+ LOCAL_DOCUMENT_EXTENSIONS,
59
+ LOCAL_IMAGE_EXTENSIONS,
60
+ LOCAL_SIZE_LIMITS,
61
+ LOCAL_SLIDE_EXTENSIONS,
62
+ LOCAL_SPREADSHEET_EXTENSIONS,
63
+ LOCAL_SUPPORTED_EXTENSIONS,
64
+ LOCAL_TEXT_EXTENSIONS,
65
+ MACOS_EXCLUDED_PREFIXES,
66
+ SENSITIVE_PATH_KEYWORDS,
67
+ WINDOWS_EXCLUDED_NAMES,
258
68
  )
259
69
 
260
70
 
261
- def _now() -> str:
262
- return datetime.now().isoformat()
263
-
264
-
265
- def _parse_iso(raw: Optional[str]) -> Optional[datetime]:
266
- if not raw:
267
- return None
268
- try:
269
- return datetime.fromisoformat(str(raw))
270
- except (TypeError, ValueError):
271
- return None
272
-
273
-
274
- def _recency_score(
275
- updated_at: Optional[str],
276
- *,
277
- now: Optional[datetime] = None,
278
- half_life_days: float = 14.0,
279
- ) -> float:
280
- stamp = _parse_iso(updated_at)
281
- if not stamp:
282
- return 0.0
283
- now = now or datetime.now()
284
- age_days = max(0.0, (now - stamp).total_seconds() / 86400.0)
285
- decay = math.log(2) / max(0.1, half_life_days)
286
- return math.exp(-decay * age_days)
287
-
288
-
289
- def _json(data: Optional[Dict[str, Any]]) -> str:
290
- return json.dumps(data or {}, ensure_ascii=False, sort_keys=True)
291
-
292
-
293
- def _safe_loads(raw: Optional[str]) -> Dict[str, Any]:
294
- """Tolerantly parse a metadata_json column — returns {} on corrupt rows."""
295
- if not raw:
296
- return {}
297
- try:
298
- value = json.loads(raw)
299
- return value if isinstance(value, dict) else {}
300
- except (json.JSONDecodeError, TypeError) as e:
301
- logging.warning(
302
- "knowledge_graph: corrupt metadata_json (%s) — using empty dict", e
303
- )
304
- return {}
305
-
306
-
307
- def _slug(text: str, max_len: int = 96) -> str:
308
- value = re.sub(r"\s+", " ", str(text or "")).strip().lower()
309
- value = re.sub(r"[^0-9a-zA-Z가-힣._:@/-]+", "-", value).strip("-")
310
- return (value or "untitled")[:max_len]
311
-
312
-
313
- def _sha256_bytes(data: bytes) -> str:
314
- return hashlib.sha256(data).hexdigest()
315
-
316
-
317
- def _sha256_text(text: str) -> str:
318
- return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
319
-
320
-
321
- def _safe_iso_from_stat_mtime(mtime: float) -> str:
322
- try:
323
- return datetime.fromtimestamp(float(mtime)).isoformat()
324
- except (TypeError, ValueError, OSError):
325
- return ""
326
-
327
-
328
- def _path_fingerprint(path: Path) -> str:
329
- return _sha256_text(str(path.expanduser().resolve()))[:24]
330
-
331
-
332
- def _is_relative_to(path: Path, base: Path) -> bool:
333
- try:
334
- path.relative_to(base)
335
- return True
336
- except ValueError:
337
- return False
338
-
339
-
340
- def _path_parts_lower(path: Path) -> List[str]:
341
- return [
342
- part.lower()
343
- for part in path.parts
344
- if part and part not in {os.sep, path.anchor}
345
- ]
346
-
347
-
348
- def _current_os_type() -> str:
349
- system = platform.system().lower()
350
- if system.startswith("darwin"):
351
- return "macos"
352
- if system.startswith("windows"):
353
- return "windows"
354
- if system.startswith("linux"):
355
- return "linux"
356
- return system or "unknown"
357
-
358
-
359
- def _drive_id_for_path(path: Path) -> str:
360
- resolved = path.expanduser().resolve()
361
- if resolved.drive:
362
- return resolved.drive.upper()
363
- parts = resolved.parts
364
- if len(parts) >= 3 and parts[1] == "Volumes":
365
- return f"/Volumes/{parts[2]}"
366
- if len(parts) >= 3 and parts[1] == "media":
367
- return f"/media/{parts[2]}"
368
- if len(parts) >= 3 and parts[1] == "mnt":
369
- return f"/mnt/{parts[2]}"
370
- return resolved.anchor or "/"
371
-
372
-
373
- def _file_category(ext: str) -> str:
374
- ext = (ext or "").lower()
375
- if ext in LOCAL_CODE_EXTENSIONS:
376
- return "code"
377
- if ext in LOCAL_TEXT_EXTENSIONS:
378
- return "text"
379
- if ext == ".pdf":
380
- return "pdf"
381
- if ext in LOCAL_DOCUMENT_EXTENSIONS:
382
- return "document"
383
- if ext in LOCAL_SPREADSHEET_EXTENSIONS:
384
- return "spreadsheet"
385
- if ext in LOCAL_SLIDE_EXTENSIONS:
386
- return "slide_deck"
387
- if ext in LOCAL_IMAGE_EXTENSIONS:
388
- return "image"
389
- return "unsupported"
390
-
391
-
392
- def _node_type_for_category(category: str) -> str:
393
- return {
394
- "code": "CodeFile",
395
- "spreadsheet": "Spreadsheet",
396
- "slide_deck": "SlideDeck",
397
- "image": "Image",
398
- "unsupported": "File",
399
- }.get(category, "Document")
400
-
401
-
402
- def _parser_type_for_category(category: str, ext: str) -> str:
403
- if category in {"text", "code"}:
404
- return "plain_text"
405
- if category == "spreadsheet" and ext == ".csv":
406
- return "csv_text"
407
- if category == "image":
408
- return "image_ocr"
409
- return ext.lstrip(".") or category
410
-
411
-
412
- def _size_limit_for_category(category: str) -> int:
413
- return LOCAL_SIZE_LIMITS.get(category, LOCAL_SIZE_LIMITS["document"])
414
-
415
-
416
- def _is_hidden_path(path: Path, root: Optional[Path] = None) -> bool:
417
- parts: Iterable[str]
418
- if root is not None:
419
- try:
420
- parts = path.relative_to(root).parts
421
- except ValueError:
422
- parts = path.parts
423
- else:
424
- parts = path.parts
425
- return any(part.startswith(".") and part not in {".", ".."} for part in parts)
426
-
427
-
428
- def _excluded_directory_reason(
429
- path: Path, *, root: Optional[Path] = None, os_type: Optional[str] = None
430
- ) -> Optional[str]:
431
- os_type = os_type or _current_os_type()
432
- name = path.name.lower()
433
- if name in COMMON_EXCLUDED_DIRS:
434
- return "excluded_folder"
435
- if _is_hidden_path(path, root):
436
- return "hidden_folder"
437
- parts = _path_parts_lower(path)
438
- if os_type == "windows" and any(part in WINDOWS_EXCLUDED_NAMES for part in parts):
439
- return "system_folder"
440
- normalized = path.as_posix()
441
- root_normalized = root.as_posix() if root else ""
442
-
443
- def _prefix_blocks(prefixes: Tuple[str, ...]) -> bool:
444
- for prefix in prefixes:
445
- path_under_prefix = normalized == prefix or normalized.startswith(
446
- f"{prefix}/"
447
- )
448
- root_under_prefix = bool(root_normalized) and (
449
- root_normalized == prefix or root_normalized.startswith(f"{prefix}/")
450
- )
451
- if path_under_prefix and not root_under_prefix:
452
- return True
453
- return False
454
-
455
- if os_type == "macos":
456
- home_library = Path.home() / "Library"
457
- try:
458
- root_is_library = bool(root) and _is_relative_to(
459
- root.expanduser().resolve(), home_library.expanduser().resolve()
460
- )
461
- if (
462
- _is_relative_to(
463
- path.expanduser().resolve(), home_library.expanduser().resolve()
464
- )
465
- and not root_is_library
466
- ):
467
- return "user_library"
468
- except OSError:
469
- pass
470
- if _prefix_blocks(MACOS_EXCLUDED_PREFIXES):
471
- return "system_folder"
472
- if os_type == "linux":
473
- if _prefix_blocks(LINUX_EXCLUDED_PREFIXES):
474
- return "system_folder"
475
- return None
476
-
477
-
478
- def _sensitive_file_reason(path: Path, *, root: Optional[Path] = None) -> Optional[str]:
479
- name = path.name.lower()
480
- suffix = path.suffix.lower()
481
- if name in COMMON_EXCLUDED_FILE_NAMES or suffix in COMMON_EXCLUDED_FILE_SUFFIXES:
482
- return "sensitive_or_excluded_file"
483
- try:
484
- rel_text = (
485
- path.relative_to(root).as_posix().lower()
486
- if root
487
- else path.as_posix().lower()
488
- )
489
- except ValueError:
490
- rel_text = path.as_posix().lower()
491
- tokens = re.split(r"[^0-9a-zA-Z_가-힣]+", rel_text)
492
- if any(token in SENSITIVE_PATH_KEYWORDS for token in tokens):
493
- return "sensitive_name"
494
- return None
495
-
496
-
497
- def _root_warning(path: Path, os_type: str) -> Optional[str]:
498
- resolved = path.expanduser().resolve()
499
- home = Path.home().expanduser().resolve()
500
- if os_type == "macos" and resolved == home:
501
- return "홈 전체에는 설정/숨김 폴더가 포함될 수 있습니다. 문서, 데스크탑, 다운로드, 프로젝트 폴더부터 추가하는 것을 권장합니다."
502
- if os_type == "linux" and resolved.as_posix() == "/":
503
- return "루트 디렉터리에는 시스템 파일이 포함되어 있습니다. 일반 사용자 폴더나 마운트된 데이터 폴더를 권장합니다."
504
- if os_type == "windows" and str(resolved).rstrip("\\/").upper() in {"C:", "C:\\"}:
505
- return "C드라이브에는 Windows 시스템 파일과 앱 설정 파일이 포함되어 있습니다. 하위 폴더를 선택하는 것을 권장합니다."
506
- return None
507
-
508
-
509
- def _sample_file(
510
- path: Path, root: Path, status: str, reason: str = ""
511
- ) -> Dict[str, Any]:
512
- try:
513
- rel = path.relative_to(root).as_posix()
514
- except ValueError:
515
- rel = path.name
516
- try:
517
- stat = path.stat()
518
- size = stat.st_size if path.is_file() else None
519
- modified_at = _safe_iso_from_stat_mtime(stat.st_mtime)
520
- except OSError:
521
- size = None
522
- modified_at = ""
523
- return {
524
- "path": str(path),
525
- "relative_path": rel,
526
- "name": path.name,
527
- "extension": path.suffix.lower(),
528
- "status": status,
529
- "reason": reason,
530
- "size_bytes": size,
531
- "modified_at": modified_at,
532
- }
71
+ # Pure fs/path/hash/classification helpers → ._kg_fsutil (re-exported so the
72
+ # computed __all__ below still forwards them to the graph mixins).
73
+ from ._kg_fsutil import * # noqa: E402,F401,F403
533
74
 
534
75
 
535
76
  def _clean_text(text: str) -> str:
@@ -586,9 +127,10 @@ ENABLE_LLM_EXTRACTION = os.getenv("LATTICEAI_LLM_EXTRACTION", "true").lower() in
586
127
 
587
128
 
588
129
  def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
589
- if not ENABLE_LLM_EXTRACTION or not _llm_router_ref:
130
+ router = get_llm_router()
131
+ if not ENABLE_LLM_EXTRACTION or not router:
590
132
  return None
591
- if not _llm_router_ref.current_model_id:
133
+ if not router.current_model_id:
592
134
  return None
593
135
  prompt = _LLM_EXTRACT_CONCEPT_PROMPT.format(text=text[:3000], limit=limit)
594
136
  try:
@@ -599,12 +141,12 @@ def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
599
141
  with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
600
142
  future = pool.submit(
601
143
  asyncio.run,
602
- _llm_router_ref.generate(prompt, max_tokens=1024, temperature=0.1),
144
+ router.generate(prompt, max_tokens=1024, temperature=0.1),
603
145
  )
604
146
  raw = future.result(timeout=30)
605
147
  else:
606
148
  raw = asyncio.run(
607
- _llm_router_ref.generate(prompt, max_tokens=1024, temperature=0.1)
149
+ router.generate(prompt, max_tokens=1024, temperature=0.1)
608
150
  )
609
151
  raw = raw.strip()
610
152
  if raw.startswith("```"):
@@ -627,9 +169,10 @@ def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
627
169
  def _llm_extract_triples(
628
170
  text: str, concepts: List[str], limit: int = 20
629
171
  ) -> Optional[List[Dict[str, str]]]:
630
- if not ENABLE_LLM_EXTRACTION or not _llm_router_ref:
172
+ router = get_llm_router()
173
+ if not ENABLE_LLM_EXTRACTION or not router:
631
174
  return None
632
- if not _llm_router_ref.current_model_id:
175
+ if not router.current_model_id:
633
176
  return None
634
177
  prompt = _LLM_EXTRACT_TRIPLE_PROMPT.format(
635
178
  text=text[:3000],
@@ -644,12 +187,12 @@ def _llm_extract_triples(
644
187
  with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
645
188
  future = pool.submit(
646
189
  asyncio.run,
647
- _llm_router_ref.generate(prompt, max_tokens=2048, temperature=0.1),
190
+ router.generate(prompt, max_tokens=2048, temperature=0.1),
648
191
  )
649
192
  raw = future.result(timeout=30)
650
193
  else:
651
194
  raw = asyncio.run(
652
- _llm_router_ref.generate(prompt, max_tokens=2048, temperature=0.1)
195
+ router.generate(prompt, max_tokens=2048, temperature=0.1)
653
196
  )
654
197
  raw = raw.strip()
655
198
  if raw.startswith("```"):