ltcai 8.0.0 → 8.2.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 (183) hide show
  1. package/README.md +33 -72
  2. package/docs/CHANGELOG.md +52 -874
  3. package/docs/DEVELOPMENT.md +26 -3
  4. package/docs/LEGACY_COMPATIBILITY.md +3 -1
  5. package/docs/TRUST_MODEL.md +26 -26
  6. package/docs/WHY_LATTICE.md +28 -25
  7. package/docs/kg-schema.md +6 -4
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/embeddings.py +9 -1
  10. package/lattice_brain/graph/schema.py +6 -0
  11. package/lattice_brain/runtime/multi_agent.py +1 -1
  12. package/latticeai/__init__.py +1 -1
  13. package/latticeai/api/computer_use.py +2 -0
  14. package/latticeai/api/memory.py +13 -0
  15. package/latticeai/app_factory.py +43 -162
  16. package/latticeai/brain/__init__.py +1 -1
  17. package/latticeai/core/config.py +10 -0
  18. package/latticeai/core/local_embeddings.py +8 -0
  19. package/latticeai/core/marketplace.py +1 -1
  20. package/latticeai/core/mcp_registry.py +140 -1
  21. package/latticeai/core/tool_registry.py +10 -0
  22. package/latticeai/core/workspace_graph_trace.py +132 -0
  23. package/latticeai/core/workspace_memory.py +75 -0
  24. package/latticeai/core/workspace_os.py +135 -1283
  25. package/latticeai/core/workspace_os_utils.py +132 -0
  26. package/latticeai/core/workspace_permissions.py +99 -0
  27. package/latticeai/core/workspace_plugins.py +97 -0
  28. package/latticeai/core/workspace_runs.py +612 -0
  29. package/latticeai/core/workspace_skills.py +114 -0
  30. package/latticeai/core/workspace_snapshots.py +195 -0
  31. package/latticeai/core/workspace_timeline.py +107 -0
  32. package/latticeai/runtime/audit_runtime.py +64 -0
  33. package/latticeai/runtime/config_runtime.py +4 -0
  34. package/latticeai/runtime/sso_runtime.py +52 -0
  35. package/latticeai/services/architecture_readiness.py +6 -5
  36. package/latticeai/services/memory_service.py +215 -7
  37. package/latticeai/services/model_capability_registry.py +68 -0
  38. package/latticeai/services/model_engines.py +603 -0
  39. package/latticeai/services/model_loading.py +482 -0
  40. package/latticeai/services/model_recommendation.py +5 -0
  41. package/latticeai/services/model_runtime.py +146 -837
  42. package/latticeai/services/product_readiness.py +19 -18
  43. package/package.json +2 -2
  44. package/scripts/build_frontend_assets.mjs +11 -2
  45. package/src-tauri/Cargo.lock +1 -1
  46. package/src-tauri/Cargo.toml +1 -1
  47. package/src-tauri/tauri.conf.json +1 -1
  48. package/static/app/asset-manifest.json +11 -102
  49. package/static/app/assets/Act-D9jIknFd.js +1 -0
  50. package/static/app/assets/Brain-CFOtWbPN.js +321 -0
  51. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
  52. package/static/app/assets/Library-C5Q2yWee.js +1 -0
  53. package/static/app/assets/System-BLbjdr1_.js +1 -0
  54. package/static/app/assets/core-CwxXejkd.js +1 -2
  55. package/static/app/assets/index-BqammyNu.js +16 -0
  56. package/static/app/assets/index-ty1iGgZu.css +2 -0
  57. package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
  58. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
  59. package/static/app/index.html +3 -10
  60. package/static/app/theme-boot.js +8 -0
  61. package/static/sw.js +1 -1
  62. package/tools/__init__.py +2 -1
  63. package/tools/computer.py +21 -0
  64. package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
  65. package/docs/HANDOVER_v3.6.0.md +0 -46
  66. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
  67. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
  68. package/docs/V2_ARCHITECTURE.md +0 -561
  69. package/docs/V3_2_AUDIT.md +0 -82
  70. package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
  71. package/docs/V3_FRONTEND.md +0 -140
  72. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
  73. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
  74. package/docs/V4_1_VALIDATION_REPORT.md +0 -47
  75. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
  76. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
  77. package/docs/V4_2_VALIDATION_REPORT.md +0 -89
  78. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
  79. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
  80. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
  81. package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
  82. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
  83. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
  84. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
  85. package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
  86. package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
  87. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
  88. package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
  89. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
  90. package/docs/V4_3_VALIDATION_REPORT.md +0 -58
  91. package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
  92. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
  93. package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
  94. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
  95. package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
  96. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
  97. package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
  98. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
  99. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
  100. package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
  101. package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
  102. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
  103. package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
  104. package/docs/V4_5_1_UX_REPORT.md +0 -45
  105. package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
  106. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
  107. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
  108. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
  109. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
  110. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
  111. package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
  112. package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
  113. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
  114. package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
  115. package/frontend/index.html +0 -24
  116. package/frontend/openapi.json +0 -15425
  117. package/frontend/src/App.tsx +0 -243
  118. package/frontend/src/api/client.ts +0 -580
  119. package/frontend/src/api/openapi.ts +0 -17892
  120. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  121. package/frontend/src/components/FeedbackState.tsx +0 -45
  122. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  123. package/frontend/src/components/LivingBrain.tsx +0 -220
  124. package/frontend/src/components/ProductFlow.tsx +0 -171
  125. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  126. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  127. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  128. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  129. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  130. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  131. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  132. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  133. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  134. package/frontend/src/components/primitives.tsx +0 -392
  135. package/frontend/src/components/ui/badge.tsx +0 -27
  136. package/frontend/src/components/ui/button.tsx +0 -37
  137. package/frontend/src/components/ui/card.tsx +0 -22
  138. package/frontend/src/components/ui/input.tsx +0 -16
  139. package/frontend/src/components/ui/textarea.tsx +0 -16
  140. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  141. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  142. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  143. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  144. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  145. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  146. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  147. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  148. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  149. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  150. package/frontend/src/features/brain/brainData.ts +0 -246
  151. package/frontend/src/features/brain/graphLayout.ts +0 -37
  152. package/frontend/src/features/brain/types.ts +0 -150
  153. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  154. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  155. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  156. package/frontend/src/i18n.ts +0 -1303
  157. package/frontend/src/lib/utils.ts +0 -33
  158. package/frontend/src/main.tsx +0 -23
  159. package/frontend/src/pages/Act.tsx +0 -458
  160. package/frontend/src/pages/Ask.tsx +0 -14
  161. package/frontend/src/pages/Brain.tsx +0 -914
  162. package/frontend/src/pages/Capture.tsx +0 -258
  163. package/frontend/src/pages/Library.tsx +0 -486
  164. package/frontend/src/pages/System.tsx +0 -621
  165. package/frontend/src/routes.ts +0 -92
  166. package/frontend/src/store/appStore.ts +0 -94
  167. package/frontend/src/styles.css +0 -6579
  168. package/static/app/assets/Act-DOvf59ru.js +0 -2
  169. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  170. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  171. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  172. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  173. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  174. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  175. package/static/app/assets/System-D6t9jo9V.js +0 -2
  176. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  177. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  178. package/static/app/assets/index-C7g26IF6.css +0 -2
  179. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  180. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  181. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  182. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  183. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -22,7 +22,7 @@ warnings.warn(
22
22
  "latticeai.brain is deprecated; import lattice_brain instead",
23
23
  DeprecationWarning,
24
24
  stacklevel=2,
25
- )
25
+ ) # one-time per import site in practice; shims remain for compat surface
26
26
 
27
27
  __all__ = [
28
28
  "AssembledContext",
@@ -26,6 +26,8 @@ from typing import List, Mapping, Optional
26
26
  from latticeai.core.security import host_is_loopback
27
27
 
28
28
 
29
+ __all__ = ["Config", "_value", "_str", "_bool", "_int"]
30
+
29
31
  def _value(env: Mapping[str, str], key: str, default: str = "") -> str:
30
32
  """Mirror the legacy ``env_value``: ``getenv(key) or default or ""`` (no strip)."""
31
33
  return env.get(key) or default or ""
@@ -68,6 +70,7 @@ class Config:
68
70
  host: str
69
71
  port: int
70
72
  network_exposed: bool
73
+ timezone: str
71
74
 
72
75
  # ── feature flags ───────────────────────────────────────────────
73
76
  enable_telegram: bool
@@ -93,6 +96,9 @@ class Config:
93
96
  local_model: str
94
97
  local_draft_model: str
95
98
  auto_read_chat_paths: bool
99
+ max_local_models: int
100
+ allow_model_downloads: bool
101
+ model_download_timeout: int
96
102
 
97
103
  # ── embeddings (retrieval vector signal) ────────────────────────
98
104
  embedding_provider: str
@@ -165,6 +171,7 @@ class Config:
165
171
  host=host,
166
172
  port=port,
167
173
  network_exposed=network_exposed,
174
+ timezone=_value(env, "LATTICE_TZ", "UTC") or "UTC",
168
175
  enable_telegram=_bool(env, "LATTICEAI_ENABLE_TELEGRAM", default=False),
169
176
  enable_graph=_bool(env, "LATTICEAI_ENABLE_GRAPH", default=True),
170
177
  autoload_models=_bool(env, "LATTICEAI_AUTOLOAD_MODELS", default=is_public),
@@ -184,6 +191,9 @@ class Config:
184
191
  local_model=local_model,
185
192
  local_draft_model=_value(env, "LATTICEAI_LOCAL_DRAFT_MODEL", ""),
186
193
  auto_read_chat_paths=_bool(env, "LATTICEAI_AUTO_READ_CHAT_PATHS", default=False),
194
+ max_local_models=_int(env, "LATTICEAI_MAX_LOCAL_MODELS", 1),
195
+ allow_model_downloads=_bool(env, "LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False),
196
+ model_download_timeout=_int(env, "LATTICEAI_MODEL_DOWNLOAD_TIMEOUT", 300),
187
197
  embedding_provider=_value(env, "LATTICEAI_EMBEDDING_PROVIDER", "hash").strip().lower(),
188
198
  embedding_profile=_value(env, "LATTICEAI_EMBEDDING_PROFILE", "").strip().lower(),
189
199
  embedding_model=_value(env, "LATTICEAI_EMBEDDING_MODEL", ""),
@@ -23,6 +23,10 @@ DEFAULT_EMBEDDING_DIM = 384
23
23
  EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
24
24
 
25
25
 
26
+ def embedding_model_id(dim: int) -> str:
27
+ return f"lattice-local-hash-v1:{int(dim)}"
28
+
29
+
26
30
  def _tokenize(text: str) -> List[str]:
27
31
  raw = str(text or "").lower()
28
32
  tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
@@ -57,6 +61,10 @@ class LocalEmbeddingModel:
57
61
  dim: int = DEFAULT_EMBEDDING_DIM
58
62
  model_id: str = EMBEDDING_MODEL_ID
59
63
 
64
+ def __post_init__(self) -> None:
65
+ if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
66
+ object.__setattr__(self, "model_id", embedding_model_id(self.dim))
67
+
60
68
  def embed(self, text: str) -> List[float]:
61
69
  vector = [0.0] * self.dim
62
70
  features = _tokenize(text)
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "8.0.0"
14
+ MARKETPLACE_VERSION = "8.2.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -6,9 +6,12 @@ Extracted from server.py to reduce module size.
6
6
 
7
7
  import json
8
8
  import logging
9
+ import os
10
+ import subprocess
11
+ import sys
9
12
  from datetime import datetime, timedelta
10
13
  from pathlib import Path
11
- from typing import Dict, List, Optional
14
+ from typing import Callable, Dict, List, Optional
12
15
 
13
16
  import httpx
14
17
  from fastapi import HTTPException
@@ -789,3 +792,139 @@ async def install_skill(plugin: str, skill: str) -> Dict:
789
792
  "license": entry["license"],
790
793
  "author": entry["author"],
791
794
  }
795
+
796
+
797
+ def create_mcp_install_state(data_dir: Path) -> Dict[str, Callable]:
798
+ """Return bound MCP state helpers for given data_dir. No global side effects."""
799
+ MCP_FILE = Path(data_dir) / "mcp_installs.json"
800
+
801
+ def load_mcp_installs() -> Dict:
802
+ if not os.path.exists(MCP_FILE):
803
+ return {"installed": {}, "updated_at": None}
804
+ try:
805
+ with open(MCP_FILE, "r", encoding="utf-8") as f:
806
+ data = json.load(f)
807
+ if "installed" not in data:
808
+ data["installed"] = {}
809
+ return data
810
+ except Exception as e:
811
+ logging.warning("load_mcp_installs failed: %s", e)
812
+ return {"installed": {}, "updated_at": None}
813
+
814
+ def save_mcp_installs(data: Dict) -> None:
815
+ data["updated_at"] = datetime.now().isoformat()
816
+ with open(MCP_FILE, "w", encoding="utf-8") as f:
817
+ json.dump(data, f, ensure_ascii=False, indent=2)
818
+
819
+ def mcp_public_item(item: Dict, installed_state: Dict) -> Dict:
820
+ state = installed_state.get(item.get("id"), {}) or {}
821
+ installed = item.get("install_mode") in {"builtin", "bundled"} or bool(state.get("installed"))
822
+ connector_pending = item.get("install_mode") == "connector" and not state.get("authenticated")
823
+ authenticated = item.get("install_mode") != "connector" or bool(state.get("authenticated"))
824
+ return {
825
+ "id": item.get("id"),
826
+ "name": item.get("name"),
827
+ "category": item.get("category", ""),
828
+ "install_mode": item.get("install_mode"),
829
+ "description": item.get("description", ""),
830
+ "capabilities": item.get("capabilities", []),
831
+ "connector_url": item.get("connector_url"),
832
+ "external_url": item.get("external_url"),
833
+ "package": item.get("package"),
834
+ "homepage": item.get("homepage"),
835
+ "source": item.get("source", "local"),
836
+ "installed": installed,
837
+ "status": state.get("status") or ("active" if installed and not connector_pending else "needs_auth" if connector_pending else "available"),
838
+ "authenticated": authenticated,
839
+ "updated_at": state.get("updated_at"),
840
+ }
841
+
842
+ async def recommend_mcps(query: str, limit: int = 5) -> List[Dict]:
843
+ text = (query or "").lower()
844
+ installed = load_mcp_installs().get("installed", {})
845
+ registry = await _get_combined_registry()
846
+ scored: List[Dict] = []
847
+ for item in registry:
848
+ score = 0
849
+ hits: List[str] = []
850
+ for keyword in item.get("keywords", []):
851
+ if keyword.lower() in text:
852
+ score += 3 if len(keyword) > 2 else 1
853
+ hits.append(keyword)
854
+ if not hits and text:
855
+ desc_words = item.get("description", "").lower().split()
856
+ for word in text.split():
857
+ if len(word) > 2 and word in desc_words:
858
+ score += 1
859
+ hits.append(word)
860
+ if item.get("id") == "filesystem" and any(w in text for w in ["만들", "구현", "build", "deploy", "코드", "앱"]):
861
+ score += 2
862
+ if score:
863
+ public = mcp_public_item(item, installed)
864
+ public["score"] = score
865
+ public["matched_keywords"] = hits[:6]
866
+ scored.append(public)
867
+ if not scored:
868
+ fallback_ids = ["filesystem", "browser", "documents"]
869
+ scored = [
870
+ {**mcp_public_item(item, installed), "score": 1, "matched_keywords": []}
871
+ for item in registry if item.get("id") in fallback_ids
872
+ ]
873
+ return sorted(scored, key=lambda x: x["score"], reverse=True)[:max(1, min(limit, 24))]
874
+
875
+ async def install_mcp(mcp_id: str) -> Dict:
876
+ registry = await _get_combined_registry()
877
+ item = next((entry for entry in registry if entry.get("id") == mcp_id), None)
878
+ if not item:
879
+ raise HTTPException(status_code=404, detail="MCP를 찾을 수 없습니다.")
880
+ data = load_mcp_installs()
881
+ state = data.setdefault("installed", {})
882
+ status = "active"
883
+ message = "MCP가 활성화되었습니다."
884
+ if item.get("install_mode") == "connector":
885
+ status = "needs_auth"
886
+ message = "커넥터 인증이 필요합니다. Codex 앱의 connector 설정에서 계정을 연결하면 바로 사용할 수 있습니다."
887
+ elif item.get("install_mode") == "pip":
888
+ packages = item.get("pip_packages") or []
889
+ for pkg in packages:
890
+ completed = subprocess.run(
891
+ [sys.executable, "-m", "pip", "install", "--upgrade", pkg],
892
+ capture_output=True, text=True, timeout=900, check=False,
893
+ )
894
+ if completed.returncode != 0:
895
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
896
+ message = f"필수 패키지 설치 완료: {', '.join(packages)}"
897
+ elif item.get("install_mode") == "pypi":
898
+ pkg = item.get("package", "")
899
+ version = item.get("package_version")
900
+ pkg_str = f"{pkg}=={version}" if version else pkg
901
+ completed = subprocess.run([sys.executable, "-m", "pip", "install", pkg_str], capture_output=True, text=True, timeout=300, check=False)
902
+ if completed.returncode != 0:
903
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
904
+ message = f"pip 패키지 설치 완료: {pkg_str}"
905
+ elif item.get("install_mode") == "npm":
906
+ pkg = item.get("package", "")
907
+ version = item.get("package_version")
908
+ pkg_str = f"{pkg}@{version}" if version else pkg
909
+ completed = subprocess.run(["npm", "install", "-g", pkg_str], capture_output=True, text=True, timeout=300, check=False)
910
+ if completed.returncode != 0:
911
+ raise HTTPException(status_code=500, detail=(completed.stderr or "")[-2000:] or f"{pkg} 설치 실패")
912
+ message = f"npm 패키지 설치 완료: {pkg_str}"
913
+ state[mcp_id] = {
914
+ "installed": True,
915
+ "status": status,
916
+ "authenticated": item.get("install_mode") != "connector",
917
+ "updated_at": datetime.now().isoformat(),
918
+ }
919
+ save_mcp_installs(data)
920
+ public = mcp_public_item(item, state)
921
+ public["message"] = message
922
+ return public
923
+
924
+ return {
925
+ "load_mcp_installs": load_mcp_installs,
926
+ "save_mcp_installs": save_mcp_installs,
927
+ "mcp_public_item": mcp_public_item,
928
+ "recommend_mcps": recommend_mcps,
929
+ "install_mcp": install_mcp,
930
+ }
@@ -9,6 +9,16 @@ effects at import time.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ __all__ = [
13
+ "ToolPolicy",
14
+ "ToolPermission",
15
+ "TOOL_CATALOG_BRIEF",
16
+ "FILE_CREATE_ACTIONS",
17
+ "LOCAL_WRITE_BLOCKED_PREFIXES",
18
+ "RISK_LEVEL_MAP",
19
+ "ToolRegistry",
20
+ ]
21
+
12
22
  from dataclasses import dataclass, field
13
23
  from typing import Any, Callable, Dict, Mapping, Optional, TypedDict
14
24
 
@@ -0,0 +1,132 @@
1
+ """Graph answer trace persistence extracted from WorkspaceOSStore."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from .workspace_os_utils import _json_hash, _listify, _now
7
+
8
+
9
+ class WorkspaceGraphTrace:
10
+ def __init__(self, store: Any):
11
+ self._store = store
12
+
13
+ def __getattr__(self, name: str) -> Any:
14
+ return getattr(self._store, name)
15
+
16
+ def build_graph_trace(self, question: str, graph: Any, context: str = "", *, limit: int = 8) -> Dict[str, Any]:
17
+ if graph is None:
18
+ return {
19
+ "source_files": [],
20
+ "graph_nodes": [],
21
+ "graph_edges": [],
22
+ "confidence": 0.0,
23
+ "retrieval_metadata": {
24
+ "query": question,
25
+ "matched_nodes": 0,
26
+ "graph_enabled": False,
27
+ "context_chars": len(context or ""),
28
+ },
29
+ }
30
+
31
+ matches: List[Dict[str, Any]] = []
32
+ search_error = ""
33
+ try:
34
+ matches = graph.search(question, limit=limit).get("matches", [])
35
+ except Exception as exc:
36
+ search_error = str(exc)
37
+ matches = []
38
+
39
+ source_files: List[Dict[str, Any]] = []
40
+ seen_sources = set()
41
+ for match in matches:
42
+ meta = match.get("metadata") or {}
43
+ source = (
44
+ meta.get("relative_path")
45
+ or meta.get("file_path")
46
+ or meta.get("filename")
47
+ or meta.get("blob_path")
48
+ or meta.get("source")
49
+ )
50
+ if source and source not in seen_sources:
51
+ seen_sources.add(source)
52
+ source_files.append({
53
+ "source": source,
54
+ "node_id": match.get("id"),
55
+ "node_title": match.get("title"),
56
+ "node_type": match.get("type"),
57
+ "jump": {
58
+ "graph": f"/graph?node={match.get('id')}",
59
+ "source": source,
60
+ },
61
+ })
62
+
63
+ edges: List[Dict[str, Any]] = []
64
+ edge_seen = set()
65
+ for match in matches[:5]:
66
+ node_id = match.get("id")
67
+ if not node_id:
68
+ continue
69
+ try:
70
+ for edge in graph.neighbors(node_id).get("edges", []):
71
+ key = (edge.get("from"), edge.get("to"), edge.get("type"))
72
+ if key in edge_seen:
73
+ continue
74
+ edge_seen.add(key)
75
+ edges.append(edge)
76
+ if len(edges) >= 24:
77
+ break
78
+ except Exception:
79
+ continue
80
+
81
+ if matches:
82
+ confidence = min(0.95, 0.35 + min(len(matches), limit) / max(limit, 1) * 0.45 + (0.10 if edges else 0.0))
83
+ else:
84
+ confidence = 0.05 if context else 0.0
85
+
86
+ return {
87
+ "source_files": source_files,
88
+ "graph_nodes": matches,
89
+ "graph_edges": edges,
90
+ "confidence": round(confidence, 4),
91
+ "retrieval_metadata": {
92
+ "query": question,
93
+ "matched_nodes": len(matches),
94
+ "matched_edges": len(edges),
95
+ "graph_enabled": True,
96
+ "context_chars": len(context or ""),
97
+ "search_error": search_error,
98
+ },
99
+ }
100
+
101
+ def record_trace(
102
+ self,
103
+ *,
104
+ question: str,
105
+ response: str,
106
+ conversation_id: Optional[str],
107
+ user_email: Optional[str],
108
+ trace: Dict[str, Any],
109
+ workspace_id: Optional[str] = None,
110
+ ) -> Dict[str, Any]:
111
+ state = self.load_state()
112
+ trace_id = f"trace-{_json_hash([question, response, conversation_id, _now()])[:16]}"
113
+ record = {
114
+ "id": trace_id,
115
+ "question": question,
116
+ "response_preview": str(response or "")[:700],
117
+ "conversation_id": conversation_id,
118
+ "user_email": user_email,
119
+ "workspace_id": self._resolve_scope(workspace_id, state),
120
+ "created_at": _now(),
121
+ **trace,
122
+ }
123
+ state.setdefault("traces", []).append(record)
124
+ self.save_state(state)
125
+ self.record_timeline_event("graph", "answer_trace", {"trace_id": trace_id, "conversation_id": conversation_id})
126
+ return record
127
+
128
+ def list_traces(self, conversation_id: Optional[str] = None, limit: int = 50, workspace_id: Optional[str] = None) -> Dict[str, Any]:
129
+ traces = self._scoped(_listify(self.load_state().get("traces")), workspace_id)
130
+ if conversation_id:
131
+ traces = [trace for trace in traces if trace.get("conversation_id") == conversation_id]
132
+ return {"traces": list(reversed(traces[-max(1, min(limit, 200)):]))}
@@ -0,0 +1,75 @@
1
+ """Memory methods extracted from WorkspaceOSStore for decomposition.
2
+
3
+ WorkspaceMemory manager.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from .workspace_os_utils import _json_hash, _listify, _now
10
+
11
+
12
+ class WorkspaceMemory:
13
+ def __init__(self, store: Any):
14
+ self._store = store
15
+
16
+ def upsert_memory(
17
+ self,
18
+ *,
19
+ kind: str,
20
+ content: str,
21
+ user_email: Optional[str],
22
+ tags: Optional[List[str]] = None,
23
+ memory_id: Optional[str] = None,
24
+ metadata: Optional[Dict[str, Any]] = None,
25
+ graph: Any = None,
26
+ workspace_id: Optional[str] = None,
27
+ ) -> Dict[str, Any]:
28
+ MEMORY_KINDS = {"short_term", "workspace", "preferences", "decisions", "working_style", "frequently_used_tools", "long_term"}
29
+ if kind not in MEMORY_KINDS:
30
+ raise ValueError(f"unknown memory kind: {kind}")
31
+ if not str(content or "").strip():
32
+ raise ValueError("content is required")
33
+ state = self._store.load_state()
34
+ memories = _listify(state.get("memories"))
35
+ now = _now()
36
+ memory_id = memory_id or f"memory-{_json_hash([kind, content, user_email, now])[:16]}"
37
+ existing = next((item for item in memories if item.get("id") == memory_id), None)
38
+ record = existing or {"id": memory_id, "created_at": now}
39
+ record.update({
40
+ "kind": kind,
41
+ "content": content,
42
+ "user_email": user_email,
43
+ "tags": tags or [],
44
+ "metadata": {**(metadata or {}), "memory_scope": kind},
45
+ "workspace_id": self._store._resolve_scope(workspace_id, state) if existing is None else self._store._record_workspace(record),
46
+ "updated_at": now,
47
+ })
48
+ if graph is not None:
49
+ try:
50
+ ingested = graph.ingest_event(
51
+ "Memory",
52
+ f"{kind}: {content[:80]}",
53
+ user_email=user_email,
54
+ source="workspace_os",
55
+ metadata={"memory_id": memory_id, "kind": kind, "tags": tags or []},
56
+ )
57
+ record["graph_node_id"] = ingested.get("node_id")
58
+ except Exception as exc:
59
+ record["graph_error"] = str(exc)
60
+ if existing is None:
61
+ memories.append(record)
62
+ state["memories"] = memories
63
+ self._store.save_state(state)
64
+ self._store.record_timeline_event("memory", "memory_upserted", {"memory_id": memory_id, "kind": kind}, workspace_id=record.get("workspace_id"))
65
+ return record
66
+
67
+ def list_memories(self, user_email: Optional[str] = None, kind: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
68
+ memories = self._store._scoped(_listify(self._store.load_state().get("memories")), workspace_id)
69
+ if user_email:
70
+ memories = [item for item in memories if item.get("user_email") in {None, user_email}]
71
+ if kind:
72
+ memories = [item for item in memories if item.get("kind") == kind]
73
+ return {"memories": list(reversed(memories))}
74
+
75
+ # search_memories can be added similarly if needed