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.
- package/README.md +33 -72
- package/docs/CHANGELOG.md +52 -874
- package/docs/DEVELOPMENT.md +26 -3
- package/docs/LEGACY_COMPATIBILITY.md +3 -1
- package/docs/TRUST_MODEL.md +26 -26
- package/docs/WHY_LATTICE.md +28 -25
- package/docs/kg-schema.md +6 -4
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +9 -1
- package/lattice_brain/graph/schema.py +6 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/computer_use.py +2 -0
- package/latticeai/api/memory.py +13 -0
- package/latticeai/app_factory.py +43 -162
- package/latticeai/brain/__init__.py +1 -1
- package/latticeai/core/config.py +10 -0
- package/latticeai/core/local_embeddings.py +8 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +140 -1
- package/latticeai/core/tool_registry.py +10 -0
- package/latticeai/core/workspace_graph_trace.py +132 -0
- package/latticeai/core/workspace_memory.py +75 -0
- package/latticeai/core/workspace_os.py +135 -1283
- package/latticeai/core/workspace_os_utils.py +132 -0
- package/latticeai/core/workspace_permissions.py +99 -0
- package/latticeai/core/workspace_plugins.py +97 -0
- package/latticeai/core/workspace_runs.py +612 -0
- package/latticeai/core/workspace_skills.py +114 -0
- package/latticeai/core/workspace_snapshots.py +195 -0
- package/latticeai/core/workspace_timeline.py +107 -0
- package/latticeai/runtime/audit_runtime.py +64 -0
- package/latticeai/runtime/config_runtime.py +4 -0
- package/latticeai/runtime/sso_runtime.py +52 -0
- package/latticeai/services/architecture_readiness.py +6 -5
- package/latticeai/services/memory_service.py +215 -7
- package/latticeai/services/model_capability_registry.py +68 -0
- package/latticeai/services/model_engines.py +603 -0
- package/latticeai/services/model_loading.py +482 -0
- package/latticeai/services/model_recommendation.py +5 -0
- package/latticeai/services/model_runtime.py +146 -837
- package/latticeai/services/product_readiness.py +19 -18
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +11 -2
- 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 +11 -102
- package/static/app/assets/Act-D9jIknFd.js +1 -0
- package/static/app/assets/Brain-CFOtWbPN.js +321 -0
- package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
- package/static/app/assets/Library-C5Q2yWee.js +1 -0
- package/static/app/assets/System-BLbjdr1_.js +1 -0
- package/static/app/assets/core-CwxXejkd.js +1 -2
- package/static/app/assets/index-BqammyNu.js +16 -0
- package/static/app/assets/index-ty1iGgZu.css +2 -0
- package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
- package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
- package/static/app/index.html +3 -10
- package/static/app/theme-boot.js +8 -0
- package/static/sw.js +1 -1
- package/tools/__init__.py +2 -1
- package/tools/computer.py +21 -0
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
- package/docs/HANDOVER_v3.6.0.md +0 -46
- package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
- package/docs/V2_ARCHITECTURE.md +0 -561
- package/docs/V3_2_AUDIT.md +0 -82
- package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
- package/docs/V3_FRONTEND.md +0 -140
- package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
- package/docs/V4_1_VALIDATION_REPORT.md +0 -47
- package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
- package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
- package/docs/V4_2_VALIDATION_REPORT.md +0 -89
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
- package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
- package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
- package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
- package/docs/V4_3_VALIDATION_REPORT.md +0 -58
- package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
- package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
- package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
- package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
- package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
- package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
- package/docs/V4_5_1_UX_REPORT.md +0 -45
- package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
- package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
- package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
- package/frontend/index.html +0 -24
- package/frontend/openapi.json +0 -15425
- package/frontend/src/App.tsx +0 -243
- package/frontend/src/api/client.ts +0 -580
- package/frontend/src/api/openapi.ts +0 -17892
- package/frontend/src/components/AdminAccessGate.tsx +0 -70
- package/frontend/src/components/FeedbackState.tsx +0 -45
- package/frontend/src/components/LanguageSwitcher.tsx +0 -23
- package/frontend/src/components/LivingBrain.tsx +0 -220
- package/frontend/src/components/ProductFlow.tsx +0 -171
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
- package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
- package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
- package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
- package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
- package/frontend/src/components/primitives.tsx +0 -392
- package/frontend/src/components/ui/badge.tsx +0 -27
- package/frontend/src/components/ui/button.tsx +0 -37
- package/frontend/src/components/ui/card.tsx +0 -22
- package/frontend/src/components/ui/input.tsx +0 -16
- package/frontend/src/components/ui/textarea.tsx +0 -16
- package/frontend/src/features/admin/AdminConsole.tsx +0 -334
- package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
- package/frontend/src/features/brain/BrainComposer.tsx +0 -85
- package/frontend/src/features/brain/BrainConversation.tsx +0 -688
- package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
- package/frontend/src/features/brain/BrainHome.tsx +0 -624
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
- package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
- package/frontend/src/features/brain/brainData.ts +0 -246
- package/frontend/src/features/brain/graphLayout.ts +0 -37
- package/frontend/src/features/brain/types.ts +0 -150
- package/frontend/src/features/review/ReviewCard.tsx +0 -100
- package/frontend/src/features/review/ReviewInbox.tsx +0 -127
- package/frontend/src/features/review/reviewHelpers.ts +0 -69
- package/frontend/src/i18n.ts +0 -1303
- package/frontend/src/lib/utils.ts +0 -33
- package/frontend/src/main.tsx +0 -23
- package/frontend/src/pages/Act.tsx +0 -458
- package/frontend/src/pages/Ask.tsx +0 -14
- package/frontend/src/pages/Brain.tsx +0 -914
- package/frontend/src/pages/Capture.tsx +0 -258
- package/frontend/src/pages/Library.tsx +0 -486
- package/frontend/src/pages/System.tsx +0 -621
- package/frontend/src/routes.ts +0 -92
- package/frontend/src/store/appStore.ts +0 -94
- package/frontend/src/styles.css +0 -6579
- package/static/app/assets/Act-DOvf59ru.js +0 -2
- package/static/app/assets/Act-DOvf59ru.js.map +0 -1
- package/static/app/assets/Brain-C7_0mEiI.js +0 -322
- package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
- package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
- package/static/app/assets/Library-BJPEEm5O.js +0 -2
- package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
- package/static/app/assets/System-D6t9jo9V.js +0 -2
- package/static/app/assets/System-D6t9jo9V.js.map +0 -1
- package/static/app/assets/core-CwxXejkd.js.map +0 -1
- package/static/app/assets/index-C7g26IF6.css +0 -2
- package/static/app/assets/index-DbcEYJQ2.js +0 -17
- package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
- package/static/app/assets/primitives-CD38lt4n.js +0 -2
- package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
- package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
package/latticeai/core/config.py
CHANGED
|
@@ -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)
|
|
@@ -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
|