ltcai 1.0.1 → 1.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 +34 -0
- package/docs/CHANGELOG.md +99 -0
- package/docs/EDITION_STRATEGY.md +56 -0
- package/docs/ENTERPRISE.md +78 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/health.py +45 -0
- package/latticeai/api/workspace.py +748 -0
- package/latticeai/core/enterprise.py +152 -0
- package/latticeai/core/workspace_os.py +409 -38
- package/latticeai/server_app.py +73 -530
- package/latticeai/services/__init__.py +6 -0
- package/latticeai/services/chat_service.py +53 -0
- package/latticeai/services/model_service.py +51 -0
- package/latticeai/services/workspace_service.py +117 -0
- package/package.json +1 -1
- package/static/scripts/workspace.js +149 -0
- package/static/sw.js +1 -1
- package/static/workspace.css +31 -0
- package/static/workspace.html +41 -2
package/latticeai/server_app.py
CHANGED
|
@@ -96,6 +96,16 @@ from latticeai.core.workspace_os import (
|
|
|
96
96
|
WorkspaceOSStore,
|
|
97
97
|
remove_skill_directory,
|
|
98
98
|
)
|
|
99
|
+
from latticeai.core.enterprise import (
|
|
100
|
+
EnterpriseCapability,
|
|
101
|
+
capability_registry,
|
|
102
|
+
detect_edition,
|
|
103
|
+
)
|
|
104
|
+
from latticeai.services.workspace_service import WorkspaceService
|
|
105
|
+
from latticeai.services.model_service import ModelService
|
|
106
|
+
from latticeai.services.chat_service import ChatService
|
|
107
|
+
from latticeai.api.workspace import create_workspace_router
|
|
108
|
+
from latticeai.api.health import create_health_router
|
|
99
109
|
from latticeai.core.agent import (
|
|
100
110
|
AgentState,
|
|
101
111
|
AgentRunContext,
|
|
@@ -352,6 +362,9 @@ SSO_FILE = DATA_DIR / "sso_config.json"
|
|
|
352
362
|
KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
|
|
353
363
|
LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
|
|
354
364
|
WORKSPACE_OS = WorkspaceOSStore(DATA_DIR)
|
|
365
|
+
# Service layer (latticeai.services) wraps the store with scope/permission
|
|
366
|
+
# guardrails; routers and the app assembly share this single instance.
|
|
367
|
+
WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
|
|
355
368
|
|
|
356
369
|
def _require_graph():
|
|
357
370
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -722,6 +735,10 @@ def get_history():
|
|
|
722
735
|
logging.warning("get_history failed: %s", e)
|
|
723
736
|
return []
|
|
724
737
|
|
|
738
|
+
# Chat service seam: behaviour-preserving façade for history access and
|
|
739
|
+
# Workspace-OS answer-trace recording used by the (unchanged) streaming chat path.
|
|
740
|
+
CHAT_SERVICE = ChatService(store=WORKSPACE_OS, get_history=get_history)
|
|
741
|
+
|
|
725
742
|
def conversation_title(item: Dict) -> str:
|
|
726
743
|
content = str(item.get("content") or "").strip()
|
|
727
744
|
content = re.sub(r"\s+", " ", content)
|
|
@@ -1312,22 +1329,8 @@ async def admin_page():
|
|
|
1312
1329
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
1313
1330
|
return response
|
|
1314
1331
|
|
|
1315
|
-
|
|
1316
|
-
|
|
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)
|
|
1332
|
+
# /workspace and /onboarding UI pages are served by the workspace router
|
|
1333
|
+
# (latticeai.api.workspace), included below after its dependencies are defined.
|
|
1331
1334
|
|
|
1332
1335
|
@app.get("/status")
|
|
1333
1336
|
async def status():
|
|
@@ -1432,79 +1435,7 @@ class VerifyCloudRequest(BaseModel):
|
|
|
1432
1435
|
provider: Optional[str] = None
|
|
1433
1436
|
|
|
1434
1437
|
|
|
1435
|
-
|
|
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 = ""
|
|
1438
|
+
# Workspace request models moved to latticeai.api.workspace (v1.2.0 modularization).
|
|
1508
1439
|
|
|
1509
1440
|
|
|
1510
1441
|
class GardenRequest(BaseModel):
|
|
@@ -1708,410 +1639,37 @@ def _workspace_graph():
|
|
|
1708
1639
|
return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
|
|
1709
1640
|
|
|
1710
1641
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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}
|
|
1642
|
+
# ── Workspace OS + Organization router (latticeai.api.workspace, v1.2.0) ──────
|
|
1643
|
+
app.include_router(create_workspace_router(
|
|
1644
|
+
service=WORKSPACE_SERVICE,
|
|
1645
|
+
require_user=require_user,
|
|
1646
|
+
require_admin=require_admin,
|
|
1647
|
+
get_current_user=get_current_user,
|
|
1648
|
+
append_audit_event=append_audit_event,
|
|
1649
|
+
graph_stats=_graph_stats_safe,
|
|
1650
|
+
workspace_models=_workspace_models_payload,
|
|
1651
|
+
workspace_settings=_workspace_settings_payload,
|
|
1652
|
+
get_history=get_history,
|
|
1653
|
+
get_audit_log=get_audit_log,
|
|
1654
|
+
require_graph=_require_graph,
|
|
1655
|
+
workspace_graph=_workspace_graph,
|
|
1656
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
1657
|
+
local_kg_watcher=LOCAL_KG_WATCHER,
|
|
1658
|
+
load_users=load_users,
|
|
1659
|
+
scan_environment=scan_environment,
|
|
1660
|
+
local_sysinfo=local_sysinfo,
|
|
1661
|
+
get_recommendations=get_recommendations,
|
|
1662
|
+
fetch_skills_marketplace=_fetch_skills_marketplace,
|
|
1663
|
+
install_skill=install_skill,
|
|
1664
|
+
remove_skill_directory=remove_skill_directory,
|
|
1665
|
+
redact_secret_text=redact_secret_text,
|
|
1666
|
+
skills_dir=SKILLS_DIR,
|
|
1667
|
+
capability_registry=capability_registry,
|
|
1668
|
+
ui_file_response=ui_file_response,
|
|
1669
|
+
static_dir=STATIC_DIR,
|
|
1670
|
+
local_model=LOCAL_MODEL,
|
|
1671
|
+
public_model=PUBLIC_MODEL,
|
|
1672
|
+
))
|
|
2115
1673
|
|
|
2116
1674
|
|
|
2117
1675
|
# ── Health & Info ──────────────────────────────────────────────────────────────
|
|
@@ -4002,37 +3560,22 @@ async def verify_cloud_models(force: bool = False, provider_filter: Optional[str
|
|
|
4002
3560
|
results[model_ref] = record
|
|
4003
3561
|
return results
|
|
4004
3562
|
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
"features": runtime_features(),
|
|
4022
|
-
"providers": router.detected_cloud_models(),
|
|
4023
|
-
"engines": engines,
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
@app.get("/mode")
|
|
4028
|
-
@app.get("/runtime_features")
|
|
4029
|
-
async def mode():
|
|
4030
|
-
return runtime_features()
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
@app.get("/engines")
|
|
4034
|
-
async def engines():
|
|
4035
|
-
return {"engines": await asyncio.to_thread(engine_status), "current": router.current_model_id}
|
|
3563
|
+
# ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
|
|
3564
|
+
# /health, /mode, /runtime_features, /engines(GET) now live in the health router.
|
|
3565
|
+
# Heavier engine mutation endpoints remain below in server_app.
|
|
3566
|
+
MODEL_SERVICE = ModelService(
|
|
3567
|
+
model_router=router,
|
|
3568
|
+
runtime_features=runtime_features,
|
|
3569
|
+
is_public=IS_PUBLIC_MODE,
|
|
3570
|
+
)
|
|
3571
|
+
app.include_router(create_health_router(
|
|
3572
|
+
model_service=MODEL_SERVICE,
|
|
3573
|
+
engine_status=engine_status,
|
|
3574
|
+
get_current_user=get_current_user,
|
|
3575
|
+
require_auth=REQUIRE_AUTH,
|
|
3576
|
+
app_version=APP_VERSION,
|
|
3577
|
+
app_mode=APP_MODE,
|
|
3578
|
+
))
|
|
4036
3579
|
|
|
4037
3580
|
|
|
4038
3581
|
@app.post("/engines/install")
|
|
@@ -4435,7 +3978,7 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
4435
3978
|
except Exception:
|
|
4436
3979
|
pass
|
|
4437
3980
|
|
|
4438
|
-
trace_seed =
|
|
3981
|
+
trace_seed = CHAT_SERVICE.build_graph_trace(
|
|
4439
3982
|
req.message,
|
|
4440
3983
|
KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
|
|
4441
3984
|
context,
|
|
@@ -4473,7 +4016,7 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
4473
4016
|
full_text += footnote
|
|
4474
4017
|
session.update(graph_md, full_text, req.conversation_id)
|
|
4475
4018
|
save_to_history("assistant", full_text, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4476
|
-
trace_record =
|
|
4019
|
+
trace_record = CHAT_SERVICE.record_trace(
|
|
4477
4020
|
question=req.message,
|
|
4478
4021
|
response=full_text,
|
|
4479
4022
|
conversation_id=req.conversation_id,
|
|
@@ -4499,7 +4042,7 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
4499
4042
|
result += footnote
|
|
4500
4043
|
session.update(graph_md, result, req.conversation_id)
|
|
4501
4044
|
save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4502
|
-
trace_record =
|
|
4045
|
+
trace_record = CHAT_SERVICE.record_trace(
|
|
4503
4046
|
question=req.message,
|
|
4504
4047
|
response=str(result),
|
|
4505
4048
|
conversation_id=req.conversation_id,
|
|
@@ -4536,7 +4079,7 @@ async def chat(req: ChatRequest, request: Request):
|
|
|
4536
4079
|
result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
|
|
4537
4080
|
|
|
4538
4081
|
save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4539
|
-
trace_record =
|
|
4082
|
+
trace_record = CHAT_SERVICE.record_trace(
|
|
4540
4083
|
question=req.message,
|
|
4541
4084
|
response=str(result),
|
|
4542
4085
|
conversation_id=req.conversation_id,
|
|
@@ -4640,12 +4183,12 @@ async def _stream_chat(
|
|
|
4640
4183
|
yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
|
|
4641
4184
|
history_user = get_history_user(req.user_email, req.user_nickname)
|
|
4642
4185
|
save_to_history("assistant", full_response, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
|
|
4643
|
-
trace_record =
|
|
4186
|
+
trace_record = CHAT_SERVICE.record_trace(
|
|
4644
4187
|
question=req.message,
|
|
4645
4188
|
response=full_response,
|
|
4646
4189
|
conversation_id=req.conversation_id,
|
|
4647
4190
|
user_email=effective_email or req.user_email,
|
|
4648
|
-
trace=trace_seed or
|
|
4191
|
+
trace=trace_seed or CHAT_SERVICE.build_graph_trace(
|
|
4649
4192
|
req.message,
|
|
4650
4193
|
KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
|
|
4651
4194
|
context,
|