ltcai 10.3.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.
Files changed (143) hide show
  1. package/README.md +37 -36
  2. package/docs/CHANGELOG.md +77 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +59 -92
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +1 -1
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/contracts.py +2 -1
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/workflow.py +10 -7
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +2 -2
  38. package/latticeai/api/chat.py +19 -15
  39. package/latticeai/api/chat_agent_http.py +19 -7
  40. package/latticeai/api/chat_helpers.py +9 -8
  41. package/latticeai/api/chat_stream.py +2 -2
  42. package/latticeai/api/computer_use.py +2 -2
  43. package/latticeai/api/hooks.py +4 -2
  44. package/latticeai/api/local_files.py +8 -4
  45. package/latticeai/api/mcp.py +2 -2
  46. package/latticeai/api/models.py +5 -5
  47. package/latticeai/api/permissions.py +2 -2
  48. package/latticeai/api/search.py +5 -3
  49. package/latticeai/api/security_dashboard.py +3 -5
  50. package/latticeai/api/setup.py +3 -3
  51. package/latticeai/api/static_routes.py +7 -2
  52. package/latticeai/api/tools.py +9 -4
  53. package/latticeai/api/workspace.py +25 -22
  54. package/latticeai/app_factory.py +132 -1322
  55. package/latticeai/cli/entrypoint.py +1 -1
  56. package/latticeai/core/agent.py +28 -14
  57. package/latticeai/core/audit.py +5 -3
  58. package/latticeai/core/embedding_providers.py +19 -6
  59. package/latticeai/core/legacy_compatibility.py +1 -1
  60. package/latticeai/core/marketplace.py +2 -2
  61. package/latticeai/core/mcp_catalog.py +3 -1
  62. package/latticeai/core/model_compat.py +1 -1
  63. package/latticeai/core/plugins.py +1 -1
  64. package/latticeai/core/project_sessions.py +1 -1
  65. package/latticeai/core/run_explain.py +6 -4
  66. package/latticeai/core/workspace_os.py +0 -4
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_state.py +1 -1
  69. package/latticeai/core/workspace_snapshots.py +3 -3
  70. package/latticeai/integrations/telegram_bot.py +52 -3
  71. package/latticeai/models/router.py +61 -34
  72. package/latticeai/runtime/access_runtime.py +3 -1
  73. package/latticeai/runtime/build_phases.py +1417 -0
  74. package/latticeai/runtime/review_wiring.py +6 -1
  75. package/latticeai/runtime/runtime_context.py +276 -0
  76. package/latticeai/runtime/sso_config_runtime.py +1 -1
  77. package/latticeai/runtime/stages.py +8 -2
  78. package/latticeai/services/app_context.py +23 -0
  79. package/latticeai/services/architecture_readiness.py +1 -1
  80. package/latticeai/services/brain_intelligence.py +4 -2
  81. package/latticeai/services/cloud_streaming.py +5 -3
  82. package/latticeai/services/command_center.py +1 -1
  83. package/latticeai/services/evidence_actions.py +2 -1
  84. package/latticeai/services/hybrid_chat.py +1 -1
  85. package/latticeai/services/local_knowledge.py +25 -9
  86. package/latticeai/services/memory_service.py +8 -4
  87. package/latticeai/services/model_catalog.py +12 -12
  88. package/latticeai/services/model_engines.py +15 -17
  89. package/latticeai/services/model_loading.py +1 -1
  90. package/latticeai/services/model_runtime.py +31 -33
  91. package/latticeai/services/multimodal_streaming.py +10 -4
  92. package/latticeai/services/openai_compatible_adapter.py +4 -3
  93. package/latticeai/services/product_readiness.py +1 -1
  94. package/latticeai/services/run_executor.py +12 -3
  95. package/latticeai/services/search_service.py +1 -1
  96. package/latticeai/services/triggers.py +1 -1
  97. package/latticeai/setup/auto_setup.py +1 -1
  98. package/latticeai/setup/wizard.py +12 -3
  99. package/latticeai/tools/computer.py +3 -1
  100. package/latticeai/tools/documents.py +2 -2
  101. package/package.json +1 -1
  102. package/scripts/check_current_release_docs.mjs +1 -1
  103. package/src-tauri/Cargo.lock +1 -1
  104. package/src-tauri/Cargo.toml +1 -1
  105. package/src-tauri/tauri.conf.json +1 -1
  106. package/static/app/asset-manifest.json +36 -36
  107. package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
  108. package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
  109. package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
  110. package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
  111. package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
  112. package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
  113. package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
  114. package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
  115. package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
  116. package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
  117. package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
  118. package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
  119. package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
  120. package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
  121. package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
  122. package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
  123. package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
  124. package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
  125. package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
  126. package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
  127. package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
  128. package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
  129. package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
  130. package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
  131. package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
  132. package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
  133. package/static/app/assets/search-B8jsVTw_.js +1 -0
  134. package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
  135. package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
  136. package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
  137. package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
  138. package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
  139. package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
  140. package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
  141. package/static/app/index.html +3 -3
  142. package/static/sw.js +1 -1
  143. package/static/app/assets/search-CwEJWBEA.js +0 -1
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  os.environ.setdefault("MLX_VLM_DRAFT_KIND", "mtp")
20
20
 
21
21
  from concurrent.futures import ThreadPoolExecutor
22
- from typing import AsyncIterator, Dict, List, Optional, Tuple
22
+ from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
23
23
 
24
24
  from PIL import Image
25
25
 
@@ -34,37 +34,44 @@ from latticeai.models.model_providers import ( # noqa: F401
34
34
  PROVIDER_MODEL_CATALOG,
35
35
  )
36
36
 
37
+ # Optional dependencies. Each is aliased on import and then re-exported as
38
+ # `Any`, so "installed" and "absent" are the same declared type and every
39
+ # call site keeps its historical name.
37
40
  try:
38
- from openai import AsyncOpenAI
41
+ from openai import AsyncOpenAI as _AsyncOpenAI
39
42
  except Exception:
40
- AsyncOpenAI = None
43
+ _AsyncOpenAI = None # type: ignore[assignment,misc]
44
+ AsyncOpenAI: Any = _AsyncOpenAI
41
45
 
42
46
  # 추론 전용 싱글 스레드 워커 (GPU 스트림 보호용)
43
47
  executor = ThreadPoolExecutor(max_workers=1)
44
48
 
45
49
  try:
46
- import mlx.core as mx
50
+ import mlx.core as _mx
47
51
  except Exception as e:
48
- mx = None
52
+ _mx = None # type: ignore[assignment]
49
53
  print(f"⚠️ MLX core unavailable: {e}")
54
+ mx: Any = _mx
50
55
 
51
56
  try:
52
- from mlx_vlm import load as vlm_load
57
+ from mlx_vlm import load as _vlm_load
53
58
  VLM_AVAILABLE = True
54
59
  print("✅ MLX-VLM is ready for multimodal models.")
55
60
  except Exception as e:
56
- vlm_load = None
61
+ _vlm_load = None # type: ignore[assignment]
57
62
  VLM_AVAILABLE = False
58
63
  print(f"⚠️ MLX-VLM unavailable: {e}")
64
+ vlm_load: Any = _vlm_load
59
65
 
60
66
  try:
61
- from mlx_lm import load as lm_load
67
+ from mlx_lm import load as _lm_load
62
68
  LM_AVAILABLE = True
63
69
  print("✅ MLX-LM is ready for text fallback models.")
64
70
  except Exception as e:
65
- lm_load = None
71
+ _lm_load = None # type: ignore[assignment]
66
72
  LM_AVAILABLE = False
67
73
  print(f"⚠️ MLX-LM unavailable: {e}")
74
+ lm_load: Any = _lm_load
68
75
 
69
76
  BRAND_NAME = "Lattice AI"
70
77
  LEGACY_BRAND_PATTERNS = [
@@ -111,7 +118,11 @@ def normalize_branding(text: Optional[str]) -> str:
111
118
  return normalized
112
119
 
113
120
 
114
- def source_metadata_for_model(provider: str, model: Dict[str, str], *, local_server: bool) -> Dict[str, str]:
121
+ # Returns a display payload whose `source_display_order` value is a list,
122
+ # so the value type is Any rather than str.
123
+ def source_metadata_for_model(
124
+ provider: str, model: Dict[str, Any], *, local_server: bool
125
+ ) -> Dict[str, Any]:
115
126
  family = str(model.get("family") or "")
116
127
  country, company = MODEL_SOURCE_BY_FAMILY.get(family, ("미상", provider.title()))
117
128
  if local_server:
@@ -139,7 +150,7 @@ def source_metadata_for_model(provider: str, model: Dict[str, str], *, local_ser
139
150
  class CloudModel:
140
151
  provider: str
141
152
  model: str
142
- client: object
153
+ client: Any # AsyncOpenAI when the optional dependency is installed
143
154
  cache_key: str
144
155
 
145
156
  def parse_model_ref(model_id: str) -> tuple[str, str]:
@@ -272,7 +283,9 @@ def _mlx_sampler(temperature: float):
272
283
 
273
284
  class LLMRouter:
274
285
  def __init__(self):
275
- self._cache: Dict[str, Tuple] = {}
286
+ # A local entry is (model, tokenizer, draft_model, loader_kind); a
287
+ # cloud entry is a CloudModel. `_unpack_local_cache` splits them.
288
+ self._cache: Dict[str, Any] = {}
276
289
  self._current: Optional[str] = None
277
290
  self._last_used: Dict[str, float] = {}
278
291
  self._max_local_models = max(1, int(os.getenv("LATTICEAI_MAX_LOCAL_MODELS", "1")))
@@ -366,8 +379,8 @@ class LLMRouter:
366
379
  async def load_model(
367
380
  self,
368
381
  model_id: str,
369
- adapter_path: str = None,
370
- draft_model_id: str = None,
382
+ adapter_path: Optional[str] = None,
383
+ draft_model_id: Optional[str] = None,
371
384
  api_key_override: Optional[str] = None,
372
385
  owner: Optional[str] = None,
373
386
  ) -> str:
@@ -449,21 +462,27 @@ class LLMRouter:
449
462
 
450
463
  base_url = os.getenv(config.get("base_url_env", "")) if config.get("base_url_env") else None
451
464
  base_url = base_url or config.get("base_url")
452
- client_kwargs = {"api_key": api_key}
453
- if base_url:
454
- client_kwargs["base_url"] = base_url
465
+ # base_url is passed only when configured: an explicit None is not
466
+ # the same as omitting the argument.
467
+ client = (
468
+ AsyncOpenAI(api_key=api_key, base_url=base_url)
469
+ if base_url
470
+ else AsyncOpenAI(api_key=api_key)
471
+ )
455
472
 
456
473
  cache_owner = owner or "global"
457
474
  cache_key = f"{provider}:{model}::{cache_owner}"
458
475
  with self._lock:
459
- self._cache[cache_key] = CloudModel(provider=provider, model=model, client=AsyncOpenAI(**client_kwargs), cache_key=cache_key)
476
+ self._cache[cache_key] = CloudModel(
477
+ provider=provider, model=model, client=client, cache_key=cache_key
478
+ )
460
479
  self._current = cache_key
461
480
  self._touch(cache_key)
462
481
  return f"Cloud provider ready: {cache_key}"
463
482
 
464
483
  def detected_cloud_models(self) -> List[Dict[str, str]]:
465
484
  local_server_providers = {"ollama", "vllm", "lmstudio", "llamacpp"}
466
- items = []
485
+ items: List[Dict[str, Any]] = []
467
486
  for provider, config in OPENAI_COMPATIBLE_PROVIDERS.items():
468
487
  has_key = bool(os.getenv(config["env_key"]) or config.get("api_key_fallback"))
469
488
  provider_models = PROVIDER_MODEL_CATALOG.get(provider) or [{
@@ -486,19 +505,23 @@ class LLMRouter:
486
505
  })
487
506
  custom = os.getenv("LATTICEAI_CLOUD_MODELS") or ""
488
507
  for raw in [item.strip() for item in custom.split(",") if item.strip()]:
489
- provider, model = parse_model_ref(raw)
508
+ provider, custom_model = parse_model_ref(raw)
490
509
  if provider != "local_mlx" and provider in OPENAI_COMPATIBLE_PROVIDERS:
491
510
  config = OPENAI_COMPATIBLE_PROVIDERS[provider]
492
511
  items.append({
493
- "id": f"{provider}:{model}",
494
- "name": f"{provider.title()} · {model}",
512
+ "id": f"{provider}:{custom_model}",
513
+ "name": f"{provider.title()} · {custom_model}",
495
514
  "provider": provider,
496
515
  "tag": "cloud",
497
516
  "available": bool(os.getenv(config["env_key"]) or config.get("api_key_fallback")),
498
517
  "requires": None,
499
518
  **source_metadata_for_model(
500
519
  provider,
501
- {"id": model, "name": f"{provider.title()} · {model}", "family": provider.title()},
520
+ {
521
+ "id": custom_model,
522
+ "name": f"{provider.title()} · {custom_model}",
523
+ "family": provider.title(),
524
+ },
502
525
  local_server=provider in local_server_providers,
503
526
  ),
504
527
  })
@@ -551,7 +574,7 @@ class LLMRouter:
551
574
  print(f"⚠️ VLM chat template fallback: {e}")
552
575
  return self._build_prompt(message, context, processor)
553
576
 
554
- def _unpack_local_cache(self, cached: Tuple) -> Tuple[object, object, object, str]:
577
+ def _unpack_local_cache(self, cached: Any) -> Tuple[Any, Any, Any, str]:
555
578
  model, tokenizer, draft_model = cached[:3]
556
579
  loader_kind = str(cached[3]) if len(cached) > 3 else "mlx_vlm"
557
580
  return model, tokenizer, draft_model, loader_kind
@@ -622,8 +645,9 @@ class LLMRouter:
622
645
  loop = asyncio.get_event_loop()
623
646
 
624
647
  def _gen():
625
- import mlx.core as mx
626
- mx.set_default_device(mx.gpu)
648
+ import mlx.core as mx # type: ignore[no-redef]
649
+
650
+ mx.set_default_device(mx.gpu) # type: ignore[arg-type]
627
651
  if use_vlm:
628
652
  from mlx_vlm import generate as vlm_gen
629
653
  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")
@@ -679,11 +703,12 @@ class LLMRouter:
679
703
  else self._build_prompt(message, context, tokenizer)
680
704
  )
681
705
  loop = asyncio.get_event_loop()
682
- queue = asyncio.Queue()
706
+ queue: "asyncio.Queue[Any]" = asyncio.Queue()
683
707
 
684
708
  def _stream():
685
- import mlx.core as mx
686
- mx.set_default_device(mx.gpu)
709
+ import mlx.core as mx # type: ignore[no-redef]
710
+
711
+ mx.set_default_device(mx.gpu) # type: ignore[arg-type]
687
712
  try:
688
713
  if use_vlm:
689
714
  from mlx_vlm import stream_generate as vlm_stream
@@ -806,8 +831,9 @@ class LLMRouter:
806
831
 
807
832
  loop = asyncio.get_event_loop()
808
833
  def _gen():
809
- import mlx.core as mx
810
- mx.set_default_device(mx.gpu)
834
+ import mlx.core as mx # type: ignore[no-redef]
835
+
836
+ mx.set_default_device(mx.gpu) # type: ignore[arg-type]
811
837
  if loader_kind == "mlx_vlm":
812
838
  from mlx_vlm import generate as vlm_gen
813
839
  return vlm_gen(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
@@ -885,11 +911,12 @@ class LLMRouter:
885
911
  prompt = f"<|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n{message}<|im_end|>\n<|im_start|>assistant\n"
886
912
 
887
913
  loop = asyncio.get_event_loop()
888
- queue = asyncio.Queue()
914
+ queue: "asyncio.Queue[Any]" = asyncio.Queue()
889
915
 
890
916
  def _stream():
891
- import mlx.core as mx
892
- mx.set_default_device(mx.gpu)
917
+ import mlx.core as mx # type: ignore[no-redef]
918
+
919
+ mx.set_default_device(mx.gpu) # type: ignore[arg-type]
893
920
  try:
894
921
  if loader_kind == "mlx_vlm":
895
922
  from mlx_vlm import stream_generate as vlm_stream
@@ -15,7 +15,9 @@ def build_access_runtime(
15
15
  request_type: Any,
16
16
  load_users: Callable[[], Dict],
17
17
  get_session_email: Callable[[str], Optional[str]],
18
- user_id_for_email: Callable[[Dict, str], str],
18
+ # Optional in and out: an anonymous request has no email, and an email
19
+ # with no account has no id.
20
+ user_id_for_email: Callable[[Dict, Optional[str]], Optional[str]],
19
21
  ) -> Dict[str, Any]:
20
22
  """Build user/admin access helpers without changing legacy call signatures."""
21
23