ltcai 0.6.0 → 1.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.
- package/README.md +20 -5
- package/docs/CHANGELOG.md +51 -0
- package/knowledge_graph.py +33 -0
- package/latticeai/__init__.py +3 -1
- package/latticeai/core/workspace_os.py +1178 -0
- package/latticeai/server_app.py +605 -6
- package/package.json +6 -3
- package/static/admin.html +1 -0
- package/static/graph.html +1 -0
- package/static/manifest.json +2 -2
- package/static/scripts/chat.js +4 -2
- package/static/scripts/graph.js +3 -3
- package/static/scripts/workspace.js +382 -0
- package/static/sw.js +5 -1
- package/static/workspace.css +515 -0
- package/static/workspace.html +199 -0
package/latticeai/server_app.py
CHANGED
|
@@ -91,6 +91,11 @@ from latticeai.core.graph_curator import (
|
|
|
91
91
|
mask_secrets as _curator_mask_secrets,
|
|
92
92
|
)
|
|
93
93
|
from latticeai.core.config import Config
|
|
94
|
+
from latticeai.core.workspace_os import (
|
|
95
|
+
WORKSPACE_OS_VERSION,
|
|
96
|
+
WorkspaceOSStore,
|
|
97
|
+
remove_skill_directory,
|
|
98
|
+
)
|
|
94
99
|
from latticeai.core.agent import (
|
|
95
100
|
AgentState,
|
|
96
101
|
AgentRunContext,
|
|
@@ -243,6 +248,7 @@ async def single_text_stream(text: str, model: str = "system") -> AsyncIterator[
|
|
|
243
248
|
# The module-level names below are kept as a compatibility surface for the rest
|
|
244
249
|
# of server.py; all of them are now derived from a single CONFIG instance.
|
|
245
250
|
CONFIG = Config.from_env()
|
|
251
|
+
APP_VERSION = WORKSPACE_OS_VERSION
|
|
246
252
|
|
|
247
253
|
APP_MODE = CONFIG.app_mode
|
|
248
254
|
IS_PUBLIC_MODE = CONFIG.is_public
|
|
@@ -345,6 +351,7 @@ AUDIT_FILE = DATA_DIR / "audit_log.json"
|
|
|
345
351
|
SSO_FILE = DATA_DIR / "sso_config.json"
|
|
346
352
|
KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
|
|
347
353
|
LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
|
|
354
|
+
WORKSPACE_OS = WorkspaceOSStore(DATA_DIR)
|
|
348
355
|
|
|
349
356
|
def _require_graph():
|
|
350
357
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -1125,7 +1132,7 @@ async def lifespan(app: FastAPI):
|
|
|
1125
1132
|
except Exception:
|
|
1126
1133
|
pass
|
|
1127
1134
|
|
|
1128
|
-
app = FastAPI(title=f"Lattice AI Server ({APP_MODE})", version=
|
|
1135
|
+
app = FastAPI(title=f"Lattice AI Server ({APP_MODE})", version=APP_VERSION, lifespan=lifespan)
|
|
1129
1136
|
|
|
1130
1137
|
CORS_ALLOWED_ORIGINS = [
|
|
1131
1138
|
f"http://localhost:{DEFAULT_PORT}",
|
|
@@ -1305,6 +1312,23 @@ async def admin_page():
|
|
|
1305
1312
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
1306
1313
|
return response
|
|
1307
1314
|
|
|
1315
|
+
@app.get("/workspace")
|
|
1316
|
+
async def workspace_page(request: Request):
|
|
1317
|
+
require_user(request)
|
|
1318
|
+
workspace_path = STATIC_DIR / "workspace.html"
|
|
1319
|
+
if not workspace_path.exists():
|
|
1320
|
+
raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
|
|
1321
|
+
return ui_file_response(workspace_path)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
@app.get("/onboarding")
|
|
1325
|
+
async def onboarding_page(request: Request):
|
|
1326
|
+
require_user(request)
|
|
1327
|
+
workspace_path = STATIC_DIR / "workspace.html"
|
|
1328
|
+
if not workspace_path.exists():
|
|
1329
|
+
raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
|
|
1330
|
+
return ui_file_response(workspace_path)
|
|
1331
|
+
|
|
1308
1332
|
@app.get("/status")
|
|
1309
1333
|
async def status():
|
|
1310
1334
|
"""서버 상태 및 현재 로드된 모델 정보를 반환합니다."""
|
|
@@ -1408,6 +1432,81 @@ class VerifyCloudRequest(BaseModel):
|
|
|
1408
1432
|
provider: Optional[str] = None
|
|
1409
1433
|
|
|
1410
1434
|
|
|
1435
|
+
class WorkspaceOnboardingStepRequest(BaseModel):
|
|
1436
|
+
step: str
|
|
1437
|
+
status: str = "complete"
|
|
1438
|
+
data: Dict = {}
|
|
1439
|
+
error: str = ""
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
class WorkspaceOnboardingCompleteRequest(BaseModel):
|
|
1443
|
+
data: Dict = {}
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
class WorkspaceSnapshotRequest(BaseModel):
|
|
1447
|
+
name: str = "Workspace snapshot"
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
class WorkspaceSnapshotCompareRequest(BaseModel):
|
|
1451
|
+
before_id: str
|
|
1452
|
+
after_id: str
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
class WorkspaceMemoryRequest(BaseModel):
|
|
1456
|
+
kind: str
|
|
1457
|
+
content: str
|
|
1458
|
+
tags: List[str] = []
|
|
1459
|
+
memory_id: Optional[str] = None
|
|
1460
|
+
metadata: Dict = {}
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
class WorkspaceAgentRunRequest(BaseModel):
|
|
1464
|
+
agent_id: str = "agent:executor"
|
|
1465
|
+
status: str = "ok"
|
|
1466
|
+
input: str = ""
|
|
1467
|
+
output: str = ""
|
|
1468
|
+
timeline: List[Dict] = []
|
|
1469
|
+
relationships: List[str] = []
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
class WorkspaceWorkflowRequest(BaseModel):
|
|
1473
|
+
name: str
|
|
1474
|
+
steps: List[Dict] = []
|
|
1475
|
+
metadata: Dict = {}
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
class WorkspaceWorkflowEventRequest(BaseModel):
|
|
1479
|
+
event_type: str
|
|
1480
|
+
payload: Dict = {}
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
class WorkspaceComputerMemoryRequest(BaseModel):
|
|
1484
|
+
enabled: bool = False
|
|
1485
|
+
consent: Dict = {}
|
|
1486
|
+
scopes: List[str] = []
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
class WorkspaceComputerActivityRequest(BaseModel):
|
|
1490
|
+
activity: Dict = {}
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
class WorkspaceSkillActionRequest(BaseModel):
|
|
1494
|
+
skill: str
|
|
1495
|
+
plugin: Optional[str] = None
|
|
1496
|
+
enabled: Optional[bool] = None
|
|
1497
|
+
version: Optional[str] = None
|
|
1498
|
+
metadata: Dict = {}
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
class WorkspaceVSCodeRequest(BaseModel):
|
|
1502
|
+
action: str
|
|
1503
|
+
file_path: Optional[str] = None
|
|
1504
|
+
language: Optional[str] = None
|
|
1505
|
+
content: str = ""
|
|
1506
|
+
selection: str = ""
|
|
1507
|
+
prompt: str = ""
|
|
1508
|
+
|
|
1509
|
+
|
|
1411
1510
|
class GardenRequest(BaseModel):
|
|
1412
1511
|
raw_data: str
|
|
1413
1512
|
category: Optional[str] = None # 10_Wiki / 00_Raw / Skills
|
|
@@ -1580,6 +1679,441 @@ class ToolGitShowRequest(BaseModel):
|
|
|
1580
1679
|
cwd: Optional[str] = "."
|
|
1581
1680
|
|
|
1582
1681
|
|
|
1682
|
+
# ── Workspace OS 1.0 API ─────────────────────────────────────────────────────
|
|
1683
|
+
|
|
1684
|
+
def _workspace_settings_payload() -> Dict:
|
|
1685
|
+
return {
|
|
1686
|
+
"mode": APP_MODE,
|
|
1687
|
+
"host": DEFAULT_HOST,
|
|
1688
|
+
"port": DEFAULT_PORT,
|
|
1689
|
+
"require_auth": REQUIRE_AUTH,
|
|
1690
|
+
"enable_graph": ENABLE_GRAPH,
|
|
1691
|
+
"allow_local_models": ALLOW_LOCAL_MODELS,
|
|
1692
|
+
"static_dir": str(STATIC_DIR),
|
|
1693
|
+
"data_dir": str(DATA_DIR),
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _workspace_models_payload() -> Dict:
|
|
1698
|
+
return {
|
|
1699
|
+
"current_model": router.current_model_id,
|
|
1700
|
+
"loaded_models": router.loaded_model_ids,
|
|
1701
|
+
"public_model": PUBLIC_MODEL,
|
|
1702
|
+
"local_model": LOCAL_MODEL,
|
|
1703
|
+
"local_draft_model": LOCAL_DRAFT_MODEL,
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _workspace_graph():
|
|
1708
|
+
return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
@app.get("/workspace/os")
|
|
1712
|
+
async def workspace_os_summary(request: Request):
|
|
1713
|
+
require_user(request)
|
|
1714
|
+
summary = WORKSPACE_OS.summary()
|
|
1715
|
+
summary["graph"] = _graph_stats_safe()
|
|
1716
|
+
summary["models"] = _workspace_models_payload()
|
|
1717
|
+
return summary
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
@app.get("/workspace/onboarding/status")
|
|
1721
|
+
async def workspace_onboarding_status(request: Request):
|
|
1722
|
+
require_user(request)
|
|
1723
|
+
return WORKSPACE_OS.onboarding_status(load_users(), _graph_stats_safe())
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
@app.post("/workspace/onboarding/step")
|
|
1727
|
+
async def workspace_onboarding_step(req: WorkspaceOnboardingStepRequest, request: Request):
|
|
1728
|
+
current_user = require_user(request)
|
|
1729
|
+
return WORKSPACE_OS.update_onboarding_step(
|
|
1730
|
+
req.step,
|
|
1731
|
+
status=req.status,
|
|
1732
|
+
data=req.data,
|
|
1733
|
+
error=req.error,
|
|
1734
|
+
user_email=current_user or None,
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
@app.post("/workspace/onboarding/complete")
|
|
1739
|
+
async def workspace_onboarding_complete(req: WorkspaceOnboardingCompleteRequest, request: Request):
|
|
1740
|
+
current_user = require_user(request)
|
|
1741
|
+
append_audit_event("onboarding_complete", user_email=current_user, platform="AI Workspace OS")
|
|
1742
|
+
return WORKSPACE_OS.complete_onboarding(req.data, user_email=current_user or None)
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
@app.get("/workspace/onboarding/hardware")
|
|
1746
|
+
async def workspace_onboarding_hardware(request: Request):
|
|
1747
|
+
require_user(request)
|
|
1748
|
+
env = await asyncio.to_thread(scan_environment)
|
|
1749
|
+
sysinfo = await local_sysinfo(request)
|
|
1750
|
+
payload = {"environment": env, "sysinfo": sysinfo, "scanned_at": datetime.now().isoformat()}
|
|
1751
|
+
WORKSPACE_OS.update_onboarding_step("hardware", status="complete", data=payload, user_email=get_current_user(request))
|
|
1752
|
+
return payload
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
@app.get("/workspace/onboarding/model-recommendations")
|
|
1756
|
+
async def workspace_onboarding_model_recommendations(request: Request):
|
|
1757
|
+
require_user(request)
|
|
1758
|
+
env = await asyncio.to_thread(scan_environment)
|
|
1759
|
+
recommendations = get_recommendations(env)
|
|
1760
|
+
payload = {
|
|
1761
|
+
"environment": env,
|
|
1762
|
+
"recommendations": recommendations,
|
|
1763
|
+
"default_local_model": LOCAL_MODEL,
|
|
1764
|
+
"default_public_model": PUBLIC_MODEL,
|
|
1765
|
+
}
|
|
1766
|
+
WORKSPACE_OS.update_onboarding_step("model_recommendation", status="complete", data=payload, user_email=get_current_user(request))
|
|
1767
|
+
return payload
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
@app.get("/workspace/traces")
|
|
1771
|
+
async def workspace_traces(request: Request, conversation_id: Optional[str] = None, limit: int = 50):
|
|
1772
|
+
require_user(request)
|
|
1773
|
+
return WORKSPACE_OS.list_traces(conversation_id=conversation_id, limit=limit)
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
@app.get("/workspace/indexing")
|
|
1777
|
+
async def workspace_indexing_dashboard(request: Request):
|
|
1778
|
+
require_user(request)
|
|
1779
|
+
graph = _workspace_graph()
|
|
1780
|
+
watcher_status = LOCAL_KG_WATCHER.status() if LOCAL_KG_WATCHER else {"available": False, "active": {}}
|
|
1781
|
+
return WORKSPACE_OS.build_indexing_dashboard(graph, watcher_status)
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
@app.post("/workspace/indexing/{source_id}/pause")
|
|
1785
|
+
async def workspace_indexing_pause(source_id: str, request: Request):
|
|
1786
|
+
require_user(request)
|
|
1787
|
+
_require_graph()
|
|
1788
|
+
return WORKSPACE_OS.pause_indexing(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
@app.post("/workspace/indexing/{source_id}/resume")
|
|
1792
|
+
async def workspace_indexing_resume(source_id: str, request: Request):
|
|
1793
|
+
require_user(request)
|
|
1794
|
+
_require_graph()
|
|
1795
|
+
return WORKSPACE_OS.resume_indexing(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
@app.post("/workspace/indexing/{source_id}/remove")
|
|
1799
|
+
async def workspace_indexing_remove(source_id: str, request: Request):
|
|
1800
|
+
require_user(request)
|
|
1801
|
+
_require_graph()
|
|
1802
|
+
return WORKSPACE_OS.remove_index_source(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
@app.get("/workspace/snapshots")
|
|
1806
|
+
async def workspace_snapshots(request: Request):
|
|
1807
|
+
require_user(request)
|
|
1808
|
+
return WORKSPACE_OS.list_snapshots()
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
@app.post("/workspace/snapshots")
|
|
1812
|
+
async def workspace_snapshot_create(req: WorkspaceSnapshotRequest, request: Request):
|
|
1813
|
+
current_user = require_user(request)
|
|
1814
|
+
result = WORKSPACE_OS.create_snapshot(
|
|
1815
|
+
name=req.name,
|
|
1816
|
+
graph=_workspace_graph(),
|
|
1817
|
+
history=get_history(),
|
|
1818
|
+
settings=_workspace_settings_payload(),
|
|
1819
|
+
models=_workspace_models_payload(),
|
|
1820
|
+
)
|
|
1821
|
+
append_audit_event("workspace_snapshot", user_email=current_user, snapshot_id=result["snapshot"]["id"])
|
|
1822
|
+
return result
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
@app.post("/workspace/snapshots/compare")
|
|
1826
|
+
async def workspace_snapshot_compare(req: WorkspaceSnapshotCompareRequest, request: Request):
|
|
1827
|
+
require_user(request)
|
|
1828
|
+
try:
|
|
1829
|
+
return WORKSPACE_OS.compare_snapshots(req.before_id, req.after_id)
|
|
1830
|
+
except FileNotFoundError as exc:
|
|
1831
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
@app.get("/workspace/snapshots/{snapshot_id}")
|
|
1835
|
+
async def workspace_snapshot_get(snapshot_id: str, request: Request):
|
|
1836
|
+
require_user(request)
|
|
1837
|
+
try:
|
|
1838
|
+
return WORKSPACE_OS.get_snapshot(snapshot_id)
|
|
1839
|
+
except FileNotFoundError as exc:
|
|
1840
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
@app.get("/workspace/snapshots/{snapshot_id}/{area}")
|
|
1844
|
+
async def workspace_snapshot_area(snapshot_id: str, area: str, request: Request):
|
|
1845
|
+
require_user(request)
|
|
1846
|
+
try:
|
|
1847
|
+
return WORKSPACE_OS.snapshot_view(snapshot_id, area)
|
|
1848
|
+
except FileNotFoundError as exc:
|
|
1849
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
@app.post("/workspace/snapshots/{snapshot_id}/export")
|
|
1853
|
+
async def workspace_snapshot_export(snapshot_id: str, request: Request):
|
|
1854
|
+
current_user = require_user(request)
|
|
1855
|
+
try:
|
|
1856
|
+
result = WORKSPACE_OS.export_snapshot(snapshot_id)
|
|
1857
|
+
except FileNotFoundError as exc:
|
|
1858
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
1859
|
+
append_audit_event("workspace_snapshot_export", user_email=current_user, snapshot_id=snapshot_id, path=result.get("export_path"))
|
|
1860
|
+
return result
|
|
1861
|
+
|
|
1862
|
+
|
|
1863
|
+
@app.get("/workspace/time-machine")
|
|
1864
|
+
async def workspace_time_machine(request: Request, limit: int = 100):
|
|
1865
|
+
require_user(request)
|
|
1866
|
+
return WORKSPACE_OS.timeline(get_audit_log(), limit=limit)
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
@app.get("/workspace/time-machine/{snapshot_id}/{area}")
|
|
1870
|
+
async def workspace_time_machine_view(snapshot_id: str, area: str, request: Request):
|
|
1871
|
+
require_user(request)
|
|
1872
|
+
try:
|
|
1873
|
+
return WORKSPACE_OS.snapshot_view(snapshot_id, area)
|
|
1874
|
+
except FileNotFoundError as exc:
|
|
1875
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
@app.get("/workspace/memories")
|
|
1879
|
+
async def workspace_memories(request: Request, kind: Optional[str] = None):
|
|
1880
|
+
current_user = require_user(request)
|
|
1881
|
+
return WORKSPACE_OS.list_memories(user_email=current_user or None, kind=kind)
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
@app.get("/workspace/memories/search")
|
|
1885
|
+
async def workspace_memory_search(q: str, request: Request, limit: int = 20):
|
|
1886
|
+
current_user = require_user(request)
|
|
1887
|
+
return WORKSPACE_OS.search_memories(q, user_email=current_user or None, limit=limit)
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
@app.post("/workspace/memories")
|
|
1891
|
+
async def workspace_memory_upsert(req: WorkspaceMemoryRequest, request: Request):
|
|
1892
|
+
current_user = require_user(request)
|
|
1893
|
+
try:
|
|
1894
|
+
record = WORKSPACE_OS.upsert_memory(
|
|
1895
|
+
kind=req.kind,
|
|
1896
|
+
content=req.content,
|
|
1897
|
+
tags=req.tags,
|
|
1898
|
+
memory_id=req.memory_id,
|
|
1899
|
+
metadata=req.metadata,
|
|
1900
|
+
user_email=current_user or None,
|
|
1901
|
+
graph=_workspace_graph(),
|
|
1902
|
+
)
|
|
1903
|
+
except ValueError as exc:
|
|
1904
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
1905
|
+
return {"memory": record}
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
@app.delete("/workspace/memories/{memory_id}")
|
|
1909
|
+
async def workspace_memory_delete(memory_id: str, request: Request):
|
|
1910
|
+
require_user(request)
|
|
1911
|
+
try:
|
|
1912
|
+
return WORKSPACE_OS.delete_memory(memory_id)
|
|
1913
|
+
except FileNotFoundError as exc:
|
|
1914
|
+
raise HTTPException(status_code=404, detail=f"Memory not found: {exc}") from exc
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
@app.get("/workspace/agents")
|
|
1918
|
+
async def workspace_agents(request: Request):
|
|
1919
|
+
require_user(request)
|
|
1920
|
+
return WORKSPACE_OS.list_agents()
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
@app.post("/workspace/agents/runs")
|
|
1924
|
+
async def workspace_agent_run(req: WorkspaceAgentRunRequest, request: Request):
|
|
1925
|
+
current_user = require_user(request)
|
|
1926
|
+
run = WORKSPACE_OS.record_agent_run(
|
|
1927
|
+
agent_id=req.agent_id,
|
|
1928
|
+
status=req.status,
|
|
1929
|
+
input_text=req.input,
|
|
1930
|
+
output_text=req.output,
|
|
1931
|
+
timeline=req.timeline,
|
|
1932
|
+
relationships=req.relationships,
|
|
1933
|
+
user_email=current_user or None,
|
|
1934
|
+
graph=_workspace_graph(),
|
|
1935
|
+
)
|
|
1936
|
+
return {"run": run}
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
@app.get("/workspace/relationships/{node_id:path}")
|
|
1940
|
+
async def workspace_relationships(node_id: str, request: Request, target_id: Optional[str] = None):
|
|
1941
|
+
require_user(request)
|
|
1942
|
+
_require_graph()
|
|
1943
|
+
return WORKSPACE_OS.relationship_explorer(KNOWLEDGE_GRAPH, node_id, target_id=target_id)
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
@app.get("/workspace/computer-memory")
|
|
1947
|
+
async def workspace_computer_memory(request: Request):
|
|
1948
|
+
require_user(request)
|
|
1949
|
+
return WORKSPACE_OS.load_state().get("computer_memory")
|
|
1950
|
+
|
|
1951
|
+
|
|
1952
|
+
@app.post("/workspace/computer-memory")
|
|
1953
|
+
async def workspace_computer_memory_config(req: WorkspaceComputerMemoryRequest, request: Request):
|
|
1954
|
+
current_user = require_user(request)
|
|
1955
|
+
try:
|
|
1956
|
+
config = WORKSPACE_OS.configure_computer_memory(
|
|
1957
|
+
enabled=req.enabled,
|
|
1958
|
+
approved_by=current_user or None,
|
|
1959
|
+
consent=req.consent,
|
|
1960
|
+
scopes=req.scopes or None,
|
|
1961
|
+
)
|
|
1962
|
+
except PermissionError as exc:
|
|
1963
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
1964
|
+
append_audit_event("computer_memory_config", user_email=current_user, enabled=req.enabled)
|
|
1965
|
+
return {"computer_memory": config}
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
@app.post("/workspace/computer-memory/activity")
|
|
1969
|
+
async def workspace_computer_memory_activity(req: WorkspaceComputerActivityRequest, request: Request):
|
|
1970
|
+
require_user(request)
|
|
1971
|
+
return WORKSPACE_OS.record_computer_activity(req.activity, graph=_workspace_graph())
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
@app.get("/workspace/workflows")
|
|
1975
|
+
async def workspace_workflows(request: Request, q: str = ""):
|
|
1976
|
+
require_user(request)
|
|
1977
|
+
return WORKSPACE_OS.list_workflows(query=q)
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
@app.post("/workspace/workflows")
|
|
1981
|
+
async def workspace_workflow_create(req: WorkspaceWorkflowRequest, request: Request):
|
|
1982
|
+
current_user = require_user(request)
|
|
1983
|
+
workflow = WORKSPACE_OS.create_workflow(
|
|
1984
|
+
name=req.name,
|
|
1985
|
+
steps=req.steps,
|
|
1986
|
+
metadata=req.metadata,
|
|
1987
|
+
user_email=current_user or None,
|
|
1988
|
+
graph=_workspace_graph(),
|
|
1989
|
+
)
|
|
1990
|
+
return {"workflow": workflow}
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
@app.post("/workspace/workflows/{workflow_id}/events")
|
|
1994
|
+
async def workspace_workflow_event(workflow_id: str, req: WorkspaceWorkflowEventRequest, request: Request):
|
|
1995
|
+
require_user(request)
|
|
1996
|
+
try:
|
|
1997
|
+
return {"workflow": WORKSPACE_OS.record_workflow_event(workflow_id, req.event_type, req.payload)}
|
|
1998
|
+
except FileNotFoundError as exc:
|
|
1999
|
+
raise HTTPException(status_code=404, detail=f"Workflow not found: {exc}") from exc
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
@app.get("/workspace/skills")
|
|
2003
|
+
async def workspace_skills(request: Request):
|
|
2004
|
+
require_user(request)
|
|
2005
|
+
marketplace = []
|
|
2006
|
+
try:
|
|
2007
|
+
marketplace = await _fetch_skills_marketplace()
|
|
2008
|
+
except Exception as exc:
|
|
2009
|
+
logging.warning("workspace skills marketplace unavailable: %s", exc)
|
|
2010
|
+
return WORKSPACE_OS.list_skill_registry(SKILLS_DIR, marketplace)
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
@app.post("/workspace/skills/install")
|
|
2014
|
+
async def workspace_skill_install(req: WorkspaceSkillActionRequest, request: Request):
|
|
2015
|
+
admin_email, _ = require_admin(request)
|
|
2016
|
+
if req.plugin:
|
|
2017
|
+
result = await install_skill(req.plugin, req.skill)
|
|
2018
|
+
else:
|
|
2019
|
+
result = {"status": "recorded", "skill": req.skill}
|
|
2020
|
+
entry = WORKSPACE_OS.mark_skill_installed(req.skill, version=req.version or "local", metadata={"install_result": result, **req.metadata})
|
|
2021
|
+
append_audit_event("skill_install", user_email=admin_email, plugin=req.plugin, skill=req.skill, workspace_os=True)
|
|
2022
|
+
return {"skill": entry, "install": result}
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
@app.post("/workspace/skills/uninstall")
|
|
2026
|
+
async def workspace_skill_uninstall(req: WorkspaceSkillActionRequest, request: Request):
|
|
2027
|
+
admin_email, _ = require_admin(request)
|
|
2028
|
+
removal = remove_skill_directory(SKILLS_DIR, req.skill)
|
|
2029
|
+
entry = WORKSPACE_OS.mark_skill_uninstalled(req.skill)
|
|
2030
|
+
append_audit_event("skill_uninstall", user_email=admin_email, skill=req.skill, workspace_os=True)
|
|
2031
|
+
return {"skill": entry, "removal": removal}
|
|
2032
|
+
|
|
2033
|
+
|
|
2034
|
+
@app.post("/workspace/skills/enable")
|
|
2035
|
+
async def workspace_skill_enable(req: WorkspaceSkillActionRequest, request: Request):
|
|
2036
|
+
require_user(request)
|
|
2037
|
+
return {"skill": WORKSPACE_OS.set_skill_enabled(req.skill, True)}
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
@app.post("/workspace/skills/disable")
|
|
2041
|
+
async def workspace_skill_disable(req: WorkspaceSkillActionRequest, request: Request):
|
|
2042
|
+
require_user(request)
|
|
2043
|
+
return {"skill": WORKSPACE_OS.set_skill_enabled(req.skill, False)}
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
@app.post("/workspace/skills/update")
|
|
2047
|
+
async def workspace_skill_update(req: WorkspaceSkillActionRequest, request: Request):
|
|
2048
|
+
admin_email, _ = require_admin(request)
|
|
2049
|
+
if req.plugin:
|
|
2050
|
+
result = await install_skill(req.plugin, req.skill)
|
|
2051
|
+
else:
|
|
2052
|
+
result = {"status": "version_recorded", "skill": req.skill}
|
|
2053
|
+
entry = WORKSPACE_OS.mark_skill_installed(req.skill, version=req.version or "latest", metadata={"update_result": result, **req.metadata})
|
|
2054
|
+
append_audit_event("skill_update", user_email=admin_email, plugin=req.plugin, skill=req.skill, workspace_os=True)
|
|
2055
|
+
return {"skill": entry, "update": result}
|
|
2056
|
+
|
|
2057
|
+
|
|
2058
|
+
@app.get("/workspace/audit-timeline")
|
|
2059
|
+
async def workspace_audit_timeline(
|
|
2060
|
+
request: Request,
|
|
2061
|
+
user: Optional[str] = None,
|
|
2062
|
+
event_type: Optional[str] = None,
|
|
2063
|
+
model: Optional[str] = None,
|
|
2064
|
+
since: Optional[str] = None,
|
|
2065
|
+
until: Optional[str] = None,
|
|
2066
|
+
limit: int = 100,
|
|
2067
|
+
):
|
|
2068
|
+
require_admin(request)
|
|
2069
|
+
return WORKSPACE_OS.filter_audit_timeline(
|
|
2070
|
+
get_audit_log(),
|
|
2071
|
+
user=user,
|
|
2072
|
+
event_type=event_type,
|
|
2073
|
+
model=model,
|
|
2074
|
+
since=since,
|
|
2075
|
+
until=until,
|
|
2076
|
+
limit=limit,
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
@app.post("/workspace/vscode/send")
|
|
2081
|
+
async def workspace_vscode_send(req: WorkspaceVSCodeRequest, request: Request):
|
|
2082
|
+
current_user = require_user(request)
|
|
2083
|
+
content = req.selection or req.content or req.prompt
|
|
2084
|
+
workflow = WORKSPACE_OS.create_workflow(
|
|
2085
|
+
name=f"VS Code: {req.action}",
|
|
2086
|
+
steps=[
|
|
2087
|
+
{"action": req.action, "file_path": req.file_path, "language": req.language},
|
|
2088
|
+
{"action": "send_to_lattice", "chars": len(content or "")},
|
|
2089
|
+
],
|
|
2090
|
+
metadata={
|
|
2091
|
+
"file_path": req.file_path,
|
|
2092
|
+
"language": req.language,
|
|
2093
|
+
"content_preview": redact_secret_text(content or "")[:500],
|
|
2094
|
+
},
|
|
2095
|
+
user_email=current_user or None,
|
|
2096
|
+
graph=_workspace_graph(),
|
|
2097
|
+
)
|
|
2098
|
+
if _workspace_graph() is not None and content:
|
|
2099
|
+
try:
|
|
2100
|
+
_workspace_graph().ingest_event(
|
|
2101
|
+
"VSCodeWorkflow",
|
|
2102
|
+
req.action,
|
|
2103
|
+
user_email=current_user or None,
|
|
2104
|
+
source="vscode",
|
|
2105
|
+
metadata={
|
|
2106
|
+
"file_path": req.file_path,
|
|
2107
|
+
"language": req.language,
|
|
2108
|
+
"chars": len(content),
|
|
2109
|
+
"workflow_id": workflow["id"],
|
|
2110
|
+
},
|
|
2111
|
+
)
|
|
2112
|
+
except Exception as exc:
|
|
2113
|
+
logging.warning("vscode workflow graph ingest failed: %s", exc)
|
|
2114
|
+
return {"status": "ok", "workflow": workflow}
|
|
2115
|
+
|
|
2116
|
+
|
|
1583
2117
|
# ── Health & Info ──────────────────────────────────────────────────────────────
|
|
1584
2118
|
|
|
1585
2119
|
ENGINE_INSTALLERS = {
|
|
@@ -3470,7 +4004,12 @@ async def verify_cloud_models(force: bool = False, provider_filter: Optional[str
|
|
|
3470
4004
|
|
|
3471
4005
|
@app.get("/health")
|
|
3472
4006
|
async def health(request: Request):
|
|
3473
|
-
base = {
|
|
4007
|
+
base = {
|
|
4008
|
+
"status": "ok",
|
|
4009
|
+
"version": APP_VERSION,
|
|
4010
|
+
"mode": APP_MODE,
|
|
4011
|
+
"platform": "AI Workspace OS",
|
|
4012
|
+
}
|
|
3474
4013
|
if not get_current_user(request) and REQUIRE_AUTH:
|
|
3475
4014
|
return base
|
|
3476
4015
|
engines = await asyncio.to_thread(engine_status)
|
|
@@ -3896,6 +4435,12 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
3896
4435
|
except Exception:
|
|
3897
4436
|
pass
|
|
3898
4437
|
|
|
4438
|
+
trace_seed = WORKSPACE_OS.build_graph_trace(
|
|
4439
|
+
req.message,
|
|
4440
|
+
KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
|
|
4441
|
+
context,
|
|
4442
|
+
)
|
|
4443
|
+
|
|
3899
4444
|
history_message = f"{req.message}\n[Image attached]" if req.image_data else req.message
|
|
3900
4445
|
save_to_history("user", history_message, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
3901
4446
|
if req.source != "telegram":
|
|
@@ -3928,8 +4473,16 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
3928
4473
|
full_text += footnote
|
|
3929
4474
|
session.update(graph_md, full_text, req.conversation_id)
|
|
3930
4475
|
save_to_history("assistant", full_text, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4476
|
+
trace_record = WORKSPACE_OS.record_trace(
|
|
4477
|
+
question=req.message,
|
|
4478
|
+
response=full_text,
|
|
4479
|
+
conversation_id=req.conversation_id,
|
|
4480
|
+
user_email=effective_email,
|
|
4481
|
+
trace=trace_seed,
|
|
4482
|
+
)
|
|
3931
4483
|
if req.source != "telegram":
|
|
3932
4484
|
asyncio.create_task(broadcast_web_chat("assistant", full_text))
|
|
4485
|
+
yield f"data: {json.dumps({'text': '', 'trace_id': trace_record['id'], 'trace': trace_record}, ensure_ascii=False)}\n\n"
|
|
3933
4486
|
yield "data: [DONE]\n\n"
|
|
3934
4487
|
return StreamingResponse(
|
|
3935
4488
|
_stream_doc_gen(),
|
|
@@ -3946,9 +4499,16 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
3946
4499
|
result += footnote
|
|
3947
4500
|
session.update(graph_md, result, req.conversation_id)
|
|
3948
4501
|
save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4502
|
+
trace_record = WORKSPACE_OS.record_trace(
|
|
4503
|
+
question=req.message,
|
|
4504
|
+
response=str(result),
|
|
4505
|
+
conversation_id=req.conversation_id,
|
|
4506
|
+
user_email=effective_email,
|
|
4507
|
+
trace=trace_seed,
|
|
4508
|
+
)
|
|
3949
4509
|
if req.source != "telegram":
|
|
3950
4510
|
asyncio.create_task(broadcast_web_chat("assistant", str(result)))
|
|
3951
|
-
return JSONResponse(content={"response": str(result)})
|
|
4511
|
+
return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
|
|
3952
4512
|
|
|
3953
4513
|
if req.stream:
|
|
3954
4514
|
recent_context = build_recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
|
|
@@ -3956,7 +4516,7 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
3956
4516
|
if recent_context:
|
|
3957
4517
|
stream_context = f"[RECENT CONVERSATION]\n{recent_context}\n\n{context}".strip()
|
|
3958
4518
|
return StreamingResponse(
|
|
3959
|
-
_stream_chat(req, stream_context, req.image_data),
|
|
4519
|
+
_stream_chat(req, stream_context, req.image_data, trace_seed=trace_seed, effective_email=effective_email),
|
|
3960
4520
|
media_type="text/event-stream",
|
|
3961
4521
|
headers={"X-Model": router.current_model_id},
|
|
3962
4522
|
)
|
|
@@ -3976,10 +4536,17 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
3976
4536
|
result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
|
|
3977
4537
|
|
|
3978
4538
|
save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4539
|
+
trace_record = WORKSPACE_OS.record_trace(
|
|
4540
|
+
question=req.message,
|
|
4541
|
+
response=str(result),
|
|
4542
|
+
conversation_id=req.conversation_id,
|
|
4543
|
+
user_email=effective_email,
|
|
4544
|
+
trace=trace_seed,
|
|
4545
|
+
)
|
|
3979
4546
|
if req.source != "telegram":
|
|
3980
4547
|
asyncio.create_task(broadcast_web_chat("assistant", str(result)))
|
|
3981
4548
|
|
|
3982
|
-
return JSONResponse(content={"response": str(result)})
|
|
4549
|
+
return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
|
|
3983
4550
|
|
|
3984
4551
|
|
|
3985
4552
|
@app.get("/history")
|
|
@@ -4050,7 +4617,14 @@ async def search_history(q: str, request: Request):
|
|
|
4050
4617
|
grouped[cid]["messages"].append(item)
|
|
4051
4618
|
return {"results": list(grouped.values())[-30:], "query": q}
|
|
4052
4619
|
|
|
4053
|
-
async def _stream_chat(
|
|
4620
|
+
async def _stream_chat(
|
|
4621
|
+
req: ChatRequest,
|
|
4622
|
+
context: str = "",
|
|
4623
|
+
image_data: str = None,
|
|
4624
|
+
*,
|
|
4625
|
+
trace_seed: Optional[Dict] = None,
|
|
4626
|
+
effective_email: Optional[str] = None,
|
|
4627
|
+
) -> AsyncIterator[str]:
|
|
4054
4628
|
full_response = ""
|
|
4055
4629
|
async for chunk in router.stream_generate(req.message, context, req.max_tokens, req.temperature, image_data):
|
|
4056
4630
|
clean_chunk = chunk
|
|
@@ -4066,8 +4640,20 @@ async def _stream_chat(req: ChatRequest, context: str = "", image_data: str = No
|
|
|
4066
4640
|
yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
|
|
4067
4641
|
history_user = get_history_user(req.user_email, req.user_nickname)
|
|
4068
4642
|
save_to_history("assistant", full_response, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4643
|
+
trace_record = WORKSPACE_OS.record_trace(
|
|
4644
|
+
question=req.message,
|
|
4645
|
+
response=full_response,
|
|
4646
|
+
conversation_id=req.conversation_id,
|
|
4647
|
+
user_email=effective_email or req.user_email,
|
|
4648
|
+
trace=trace_seed or WORKSPACE_OS.build_graph_trace(
|
|
4649
|
+
req.message,
|
|
4650
|
+
KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
|
|
4651
|
+
context,
|
|
4652
|
+
),
|
|
4653
|
+
)
|
|
4069
4654
|
if req.source != "telegram":
|
|
4070
4655
|
asyncio.create_task(broadcast_web_chat("assistant", full_response))
|
|
4656
|
+
yield f"data: {json.dumps({'chunk': '', 'model': router.current_model_id, 'trace_id': trace_record['id'], 'trace': trace_record}, ensure_ascii=False)}\n\n"
|
|
4071
4657
|
yield "data: [DONE]\n\n"
|
|
4072
4658
|
|
|
4073
4659
|
|
|
@@ -4277,6 +4863,19 @@ async def _agent_finish(
|
|
|
4277
4863
|
message = ctx.final_message or "작업을 완료했습니다."
|
|
4278
4864
|
save_to_history("user", req.message, source=req.source or "web", conversation_id=req.conversation_id)
|
|
4279
4865
|
save_to_history("assistant", message, source=req.source or "web", conversation_id=req.conversation_id)
|
|
4866
|
+
try:
|
|
4867
|
+
WORKSPACE_OS.record_agent_run(
|
|
4868
|
+
agent_id="agent:executor",
|
|
4869
|
+
status="ok" if ctx.state == AgentState.DONE else "failed",
|
|
4870
|
+
input_text=req.message,
|
|
4871
|
+
output_text=message,
|
|
4872
|
+
user_email=current_user or None,
|
|
4873
|
+
timeline=ctx.transcript,
|
|
4874
|
+
relationships=["agent:planner", "agent:reviewer"],
|
|
4875
|
+
graph=_workspace_graph(),
|
|
4876
|
+
)
|
|
4877
|
+
except Exception as exc:
|
|
4878
|
+
logging.warning("workspace agent run record failed: %s", exc)
|
|
4280
4879
|
created_files = _collect_created_files(ctx.transcript)
|
|
4281
4880
|
return {
|
|
4282
4881
|
"status": "ok" if ctx.state == AgentState.DONE else "failed",
|