ltcai 1.1.0 → 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.
@@ -101,6 +101,11 @@ from latticeai.core.enterprise import (
101
101
  capability_registry,
102
102
  detect_edition,
103
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
104
109
  from latticeai.core.agent import (
105
110
  AgentState,
106
111
  AgentRunContext,
@@ -357,6 +362,9 @@ SSO_FILE = DATA_DIR / "sso_config.json"
357
362
  KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
358
363
  LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
359
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)
360
368
 
361
369
  def _require_graph():
362
370
  if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
@@ -727,6 +735,10 @@ def get_history():
727
735
  logging.warning("get_history failed: %s", e)
728
736
  return []
729
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
+
730
742
  def conversation_title(item: Dict) -> str:
731
743
  content = str(item.get("content") or "").strip()
732
744
  content = re.sub(r"\s+", " ", content)
@@ -1317,22 +1329,8 @@ async def admin_page():
1317
1329
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
1318
1330
  return response
1319
1331
 
1320
- @app.get("/workspace")
1321
- async def workspace_page(request: Request):
1322
- require_user(request)
1323
- workspace_path = STATIC_DIR / "workspace.html"
1324
- if not workspace_path.exists():
1325
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
1326
- return ui_file_response(workspace_path)
1327
-
1328
-
1329
- @app.get("/onboarding")
1330
- async def onboarding_page(request: Request):
1331
- require_user(request)
1332
- workspace_path = STATIC_DIR / "workspace.html"
1333
- if not workspace_path.exists():
1334
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
1335
- 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.
1336
1334
 
1337
1335
  @app.get("/status")
1338
1336
  async def status():
@@ -1437,102 +1435,7 @@ class VerifyCloudRequest(BaseModel):
1437
1435
  provider: Optional[str] = None
1438
1436
 
1439
1437
 
1440
- class WorkspaceOnboardingStepRequest(BaseModel):
1441
- step: str
1442
- status: str = "complete"
1443
- data: Dict = {}
1444
- error: str = ""
1445
-
1446
-
1447
- class WorkspaceOnboardingCompleteRequest(BaseModel):
1448
- data: Dict = {}
1449
-
1450
-
1451
- class WorkspaceSnapshotRequest(BaseModel):
1452
- name: str = "Workspace snapshot"
1453
-
1454
-
1455
- class WorkspaceSnapshotCompareRequest(BaseModel):
1456
- before_id: str
1457
- after_id: str
1458
-
1459
-
1460
- class WorkspaceMemoryRequest(BaseModel):
1461
- kind: str
1462
- content: str
1463
- tags: List[str] = []
1464
- memory_id: Optional[str] = None
1465
- metadata: Dict = {}
1466
-
1467
-
1468
- class WorkspaceAgentRunRequest(BaseModel):
1469
- agent_id: str = "agent:executor"
1470
- status: str = "ok"
1471
- input: str = ""
1472
- output: str = ""
1473
- timeline: List[Dict] = []
1474
- relationships: List[str] = []
1475
-
1476
-
1477
- class WorkspaceWorkflowRequest(BaseModel):
1478
- name: str
1479
- steps: List[Dict] = []
1480
- metadata: Dict = {}
1481
-
1482
-
1483
- class WorkspaceWorkflowEventRequest(BaseModel):
1484
- event_type: str
1485
- payload: Dict = {}
1486
-
1487
-
1488
- class WorkspaceComputerMemoryRequest(BaseModel):
1489
- enabled: bool = False
1490
- consent: Dict = {}
1491
- scopes: List[str] = []
1492
-
1493
-
1494
- class WorkspaceComputerActivityRequest(BaseModel):
1495
- activity: Dict = {}
1496
-
1497
-
1498
- class WorkspaceSkillActionRequest(BaseModel):
1499
- skill: str
1500
- plugin: Optional[str] = None
1501
- enabled: Optional[bool] = None
1502
- version: Optional[str] = None
1503
- metadata: Dict = {}
1504
-
1505
-
1506
- class WorkspaceVSCodeRequest(BaseModel):
1507
- action: str
1508
- file_path: Optional[str] = None
1509
- language: Optional[str] = None
1510
- content: str = ""
1511
- selection: str = ""
1512
- prompt: str = ""
1513
-
1514
-
1515
- class WorkspaceCreateRequest(BaseModel):
1516
- name: str
1517
- settings: Dict = {}
1518
-
1519
-
1520
- class WorkspaceUpdateRequest(BaseModel):
1521
- name: Optional[str] = None
1522
- settings: Optional[Dict] = None
1523
-
1524
-
1525
- class WorkspaceMemberRequest(BaseModel):
1526
- user_id: str
1527
- role: str = "member"
1528
-
1529
-
1530
- class WorkspaceMemberRoleRequest(BaseModel):
1531
- role: str
1532
-
1533
-
1534
- class WorkspaceActivateRequest(BaseModel):
1535
- workspace_id: str
1438
+ # Workspace request models moved to latticeai.api.workspace (v1.2.0 modularization).
1536
1439
 
1537
1440
 
1538
1441
  class GardenRequest(BaseModel):
@@ -1736,562 +1639,37 @@ def _workspace_graph():
1736
1639
  return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
1737
1640
 
1738
1641
 
1739
- def _workspace_scope(request: Request) -> Optional[str]:
1740
- """Resolve the active workspace for scoping from an optional header/query.
1741
-
1742
- Returns ``None`` when unset so the store falls back to the active workspace
1743
- (Personal by default), preserving pre-1.1 behaviour for legacy clients.
1744
- """
1745
- header = request.headers.get("X-Workspace-Id")
1746
- if header and header.strip():
1747
- return header.strip()
1748
- query = request.query_params.get("workspace_id")
1749
- return query.strip() if query and query.strip() else None
1750
-
1751
-
1752
- @app.get("/workspace/os")
1753
- async def workspace_os_summary(request: Request):
1754
- user = require_user(request)
1755
- summary = WORKSPACE_OS.summary()
1756
- summary["graph"] = _graph_stats_safe()
1757
- summary["models"] = _workspace_models_payload()
1758
- summary["workspace_registry"] = WORKSPACE_OS.list_workspaces(user_id=user or None)
1759
- summary["edition"] = capability_registry.describe()
1760
- return summary
1761
-
1762
-
1763
- @app.get("/workspace/onboarding/status")
1764
- async def workspace_onboarding_status(request: Request):
1765
- require_user(request)
1766
- return WORKSPACE_OS.onboarding_status(load_users(), _graph_stats_safe())
1767
-
1768
-
1769
- @app.post("/workspace/onboarding/step")
1770
- async def workspace_onboarding_step(req: WorkspaceOnboardingStepRequest, request: Request):
1771
- current_user = require_user(request)
1772
- return WORKSPACE_OS.update_onboarding_step(
1773
- req.step,
1774
- status=req.status,
1775
- data=req.data,
1776
- error=req.error,
1777
- user_email=current_user or None,
1778
- )
1779
-
1780
-
1781
- @app.post("/workspace/onboarding/complete")
1782
- async def workspace_onboarding_complete(req: WorkspaceOnboardingCompleteRequest, request: Request):
1783
- current_user = require_user(request)
1784
- append_audit_event("onboarding_complete", user_email=current_user, platform="AI Workspace OS")
1785
- return WORKSPACE_OS.complete_onboarding(req.data, user_email=current_user or None)
1786
-
1787
-
1788
- @app.get("/workspace/onboarding/hardware")
1789
- async def workspace_onboarding_hardware(request: Request):
1790
- require_user(request)
1791
- env = await asyncio.to_thread(scan_environment)
1792
- sysinfo = await local_sysinfo(request)
1793
- payload = {"environment": env, "sysinfo": sysinfo, "scanned_at": datetime.now().isoformat()}
1794
- WORKSPACE_OS.update_onboarding_step("hardware", status="complete", data=payload, user_email=get_current_user(request))
1795
- return payload
1796
-
1797
-
1798
- @app.get("/workspace/onboarding/model-recommendations")
1799
- async def workspace_onboarding_model_recommendations(request: Request):
1800
- require_user(request)
1801
- env = await asyncio.to_thread(scan_environment)
1802
- recommendations = get_recommendations(env)
1803
- payload = {
1804
- "environment": env,
1805
- "recommendations": recommendations,
1806
- "default_local_model": LOCAL_MODEL,
1807
- "default_public_model": PUBLIC_MODEL,
1808
- }
1809
- WORKSPACE_OS.update_onboarding_step("model_recommendation", status="complete", data=payload, user_email=get_current_user(request))
1810
- return payload
1811
-
1812
-
1813
- @app.get("/workspace/traces")
1814
- async def workspace_traces(request: Request, conversation_id: Optional[str] = None, limit: int = 50):
1815
- require_user(request)
1816
- return WORKSPACE_OS.list_traces(conversation_id=conversation_id, limit=limit, workspace_id=_workspace_scope(request))
1817
-
1818
-
1819
- @app.get("/workspace/indexing")
1820
- async def workspace_indexing_dashboard(request: Request):
1821
- require_user(request)
1822
- graph = _workspace_graph()
1823
- watcher_status = LOCAL_KG_WATCHER.status() if LOCAL_KG_WATCHER else {"available": False, "active": {}}
1824
- return WORKSPACE_OS.build_indexing_dashboard(graph, watcher_status)
1825
-
1826
-
1827
- @app.post("/workspace/indexing/{source_id}/pause")
1828
- async def workspace_indexing_pause(source_id: str, request: Request):
1829
- require_user(request)
1830
- _require_graph()
1831
- return WORKSPACE_OS.pause_indexing(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
1832
-
1833
-
1834
- @app.post("/workspace/indexing/{source_id}/resume")
1835
- async def workspace_indexing_resume(source_id: str, request: Request):
1836
- require_user(request)
1837
- _require_graph()
1838
- return WORKSPACE_OS.resume_indexing(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
1839
-
1840
-
1841
- @app.post("/workspace/indexing/{source_id}/remove")
1842
- async def workspace_indexing_remove(source_id: str, request: Request):
1843
- require_user(request)
1844
- _require_graph()
1845
- return WORKSPACE_OS.remove_index_source(KNOWLEDGE_GRAPH, source_id, LOCAL_KG_WATCHER)
1846
-
1847
-
1848
- @app.get("/workspace/snapshots")
1849
- async def workspace_snapshots(request: Request):
1850
- require_user(request)
1851
- return WORKSPACE_OS.list_snapshots(workspace_id=_workspace_scope(request))
1852
-
1853
-
1854
- @app.post("/workspace/snapshots")
1855
- async def workspace_snapshot_create(req: WorkspaceSnapshotRequest, request: Request):
1856
- current_user = require_user(request)
1857
- result = WORKSPACE_OS.create_snapshot(
1858
- name=req.name,
1859
- graph=_workspace_graph(),
1860
- history=get_history(),
1861
- settings=_workspace_settings_payload(),
1862
- models=_workspace_models_payload(),
1863
- workspace_id=_workspace_scope(request),
1864
- )
1865
- append_audit_event("workspace_snapshot", user_email=current_user, snapshot_id=result["snapshot"]["id"])
1866
- return result
1867
-
1868
-
1869
- @app.post("/workspace/snapshots/compare")
1870
- async def workspace_snapshot_compare(req: WorkspaceSnapshotCompareRequest, request: Request):
1871
- require_user(request)
1872
- try:
1873
- return WORKSPACE_OS.compare_snapshots(req.before_id, req.after_id)
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/snapshots/{snapshot_id}")
1879
- async def workspace_snapshot_get(snapshot_id: str, request: Request):
1880
- require_user(request)
1881
- try:
1882
- return WORKSPACE_OS.get_snapshot(snapshot_id)
1883
- except FileNotFoundError as exc:
1884
- raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
1885
-
1886
-
1887
- @app.get("/workspace/snapshots/{snapshot_id}/{area}")
1888
- async def workspace_snapshot_area(snapshot_id: str, area: str, request: Request):
1889
- require_user(request)
1890
- try:
1891
- return WORKSPACE_OS.snapshot_view(snapshot_id, area)
1892
- except FileNotFoundError as exc:
1893
- raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
1894
-
1895
-
1896
- @app.post("/workspace/snapshots/{snapshot_id}/export")
1897
- async def workspace_snapshot_export(snapshot_id: str, request: Request):
1898
- current_user = require_user(request)
1899
- try:
1900
- result = WORKSPACE_OS.export_snapshot(snapshot_id)
1901
- except FileNotFoundError as exc:
1902
- raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
1903
- append_audit_event("workspace_snapshot_export", user_email=current_user, snapshot_id=snapshot_id, path=result.get("export_path"))
1904
- return result
1905
-
1906
-
1907
- @app.get("/workspace/time-machine")
1908
- async def workspace_time_machine(request: Request, limit: int = 100):
1909
- require_user(request)
1910
- return WORKSPACE_OS.timeline(get_audit_log(), limit=limit, workspace_id=_workspace_scope(request))
1911
-
1912
-
1913
- @app.get("/workspace/time-machine/{snapshot_id}/{area}")
1914
- async def workspace_time_machine_view(snapshot_id: str, area: str, request: Request):
1915
- require_user(request)
1916
- try:
1917
- return WORKSPACE_OS.snapshot_view(snapshot_id, area)
1918
- except FileNotFoundError as exc:
1919
- raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
1920
-
1921
-
1922
- @app.get("/workspace/memories")
1923
- async def workspace_memories(request: Request, kind: Optional[str] = None):
1924
- current_user = require_user(request)
1925
- return WORKSPACE_OS.list_memories(user_email=current_user or None, kind=kind, workspace_id=_workspace_scope(request))
1926
-
1927
-
1928
- @app.get("/workspace/memories/search")
1929
- async def workspace_memory_search(q: str, request: Request, limit: int = 20):
1930
- current_user = require_user(request)
1931
- return WORKSPACE_OS.search_memories(q, user_email=current_user or None, limit=limit, workspace_id=_workspace_scope(request))
1932
-
1933
-
1934
- @app.post("/workspace/memories")
1935
- async def workspace_memory_upsert(req: WorkspaceMemoryRequest, request: Request):
1936
- current_user = require_user(request)
1937
- try:
1938
- record = WORKSPACE_OS.upsert_memory(
1939
- kind=req.kind,
1940
- content=req.content,
1941
- tags=req.tags,
1942
- memory_id=req.memory_id,
1943
- metadata=req.metadata,
1944
- user_email=current_user or None,
1945
- graph=_workspace_graph(),
1946
- workspace_id=_workspace_scope(request),
1947
- )
1948
- except ValueError as exc:
1949
- raise HTTPException(status_code=400, detail=str(exc)) from exc
1950
- return {"memory": record}
1951
-
1952
-
1953
- @app.delete("/workspace/memories/{memory_id}")
1954
- async def workspace_memory_delete(memory_id: str, request: Request):
1955
- require_user(request)
1956
- try:
1957
- return WORKSPACE_OS.delete_memory(memory_id)
1958
- except FileNotFoundError as exc:
1959
- raise HTTPException(status_code=404, detail=f"Memory not found: {exc}") from exc
1960
-
1961
-
1962
- @app.get("/workspace/agents")
1963
- async def workspace_agents(request: Request):
1964
- require_user(request)
1965
- return WORKSPACE_OS.list_agents(workspace_id=_workspace_scope(request))
1966
-
1967
-
1968
- @app.post("/workspace/agents/runs")
1969
- async def workspace_agent_run(req: WorkspaceAgentRunRequest, request: Request):
1970
- current_user = require_user(request)
1971
- run = WORKSPACE_OS.record_agent_run(
1972
- agent_id=req.agent_id,
1973
- status=req.status,
1974
- input_text=req.input,
1975
- output_text=req.output,
1976
- timeline=req.timeline,
1977
- relationships=req.relationships,
1978
- user_email=current_user or None,
1979
- graph=_workspace_graph(),
1980
- workspace_id=_workspace_scope(request),
1981
- )
1982
- return {"run": run}
1983
-
1984
-
1985
- @app.get("/workspace/relationships/{node_id:path}")
1986
- async def workspace_relationships(node_id: str, request: Request, target_id: Optional[str] = None):
1987
- require_user(request)
1988
- _require_graph()
1989
- return WORKSPACE_OS.relationship_explorer(KNOWLEDGE_GRAPH, node_id, target_id=target_id)
1990
-
1991
-
1992
- @app.get("/workspace/computer-memory")
1993
- async def workspace_computer_memory(request: Request):
1994
- require_user(request)
1995
- return WORKSPACE_OS.load_state().get("computer_memory")
1996
-
1997
-
1998
- @app.post("/workspace/computer-memory")
1999
- async def workspace_computer_memory_config(req: WorkspaceComputerMemoryRequest, request: Request):
2000
- current_user = require_user(request)
2001
- try:
2002
- config = WORKSPACE_OS.configure_computer_memory(
2003
- enabled=req.enabled,
2004
- approved_by=current_user or None,
2005
- consent=req.consent,
2006
- scopes=req.scopes or None,
2007
- )
2008
- except PermissionError as exc:
2009
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2010
- append_audit_event("computer_memory_config", user_email=current_user, enabled=req.enabled)
2011
- return {"computer_memory": config}
2012
-
2013
-
2014
- @app.post("/workspace/computer-memory/activity")
2015
- async def workspace_computer_memory_activity(req: WorkspaceComputerActivityRequest, request: Request):
2016
- require_user(request)
2017
- return WORKSPACE_OS.record_computer_activity(req.activity, graph=_workspace_graph())
2018
-
2019
-
2020
- @app.get("/workspace/workflows")
2021
- async def workspace_workflows(request: Request, q: str = ""):
2022
- require_user(request)
2023
- return WORKSPACE_OS.list_workflows(query=q, workspace_id=_workspace_scope(request))
2024
-
2025
-
2026
- @app.post("/workspace/workflows")
2027
- async def workspace_workflow_create(req: WorkspaceWorkflowRequest, request: Request):
2028
- current_user = require_user(request)
2029
- workflow = WORKSPACE_OS.create_workflow(
2030
- name=req.name,
2031
- steps=req.steps,
2032
- metadata=req.metadata,
2033
- user_email=current_user or None,
2034
- graph=_workspace_graph(),
2035
- workspace_id=_workspace_scope(request),
2036
- )
2037
- return {"workflow": workflow}
2038
-
2039
-
2040
- @app.post("/workspace/workflows/{workflow_id}/events")
2041
- async def workspace_workflow_event(workflow_id: str, req: WorkspaceWorkflowEventRequest, request: Request):
2042
- require_user(request)
2043
- try:
2044
- return {"workflow": WORKSPACE_OS.record_workflow_event(workflow_id, req.event_type, req.payload)}
2045
- except FileNotFoundError as exc:
2046
- raise HTTPException(status_code=404, detail=f"Workflow not found: {exc}") from exc
2047
-
2048
-
2049
- @app.get("/workspace/skills")
2050
- async def workspace_skills(request: Request):
2051
- require_user(request)
2052
- marketplace = []
2053
- try:
2054
- marketplace = await _fetch_skills_marketplace()
2055
- except Exception as exc:
2056
- logging.warning("workspace skills marketplace unavailable: %s", exc)
2057
- return WORKSPACE_OS.list_skill_registry(SKILLS_DIR, marketplace)
2058
-
2059
-
2060
- @app.post("/workspace/skills/install")
2061
- async def workspace_skill_install(req: WorkspaceSkillActionRequest, request: Request):
2062
- admin_email, _ = require_admin(request)
2063
- if req.plugin:
2064
- result = await install_skill(req.plugin, req.skill)
2065
- else:
2066
- result = {"status": "recorded", "skill": req.skill}
2067
- entry = WORKSPACE_OS.mark_skill_installed(req.skill, version=req.version or "local", metadata={"install_result": result, **req.metadata})
2068
- append_audit_event("skill_install", user_email=admin_email, plugin=req.plugin, skill=req.skill, workspace_os=True)
2069
- return {"skill": entry, "install": result}
2070
-
2071
-
2072
- @app.post("/workspace/skills/uninstall")
2073
- async def workspace_skill_uninstall(req: WorkspaceSkillActionRequest, request: Request):
2074
- admin_email, _ = require_admin(request)
2075
- removal = remove_skill_directory(SKILLS_DIR, req.skill)
2076
- entry = WORKSPACE_OS.mark_skill_uninstalled(req.skill)
2077
- append_audit_event("skill_uninstall", user_email=admin_email, skill=req.skill, workspace_os=True)
2078
- return {"skill": entry, "removal": removal}
2079
-
2080
-
2081
- @app.post("/workspace/skills/enable")
2082
- async def workspace_skill_enable(req: WorkspaceSkillActionRequest, request: Request):
2083
- require_user(request)
2084
- return {"skill": WORKSPACE_OS.set_skill_enabled(req.skill, True)}
2085
-
2086
-
2087
- @app.post("/workspace/skills/disable")
2088
- async def workspace_skill_disable(req: WorkspaceSkillActionRequest, request: Request):
2089
- require_user(request)
2090
- return {"skill": WORKSPACE_OS.set_skill_enabled(req.skill, False)}
2091
-
2092
-
2093
- @app.post("/workspace/skills/update")
2094
- async def workspace_skill_update(req: WorkspaceSkillActionRequest, request: Request):
2095
- admin_email, _ = require_admin(request)
2096
- if req.plugin:
2097
- result = await install_skill(req.plugin, req.skill)
2098
- else:
2099
- result = {"status": "version_recorded", "skill": req.skill}
2100
- entry = WORKSPACE_OS.mark_skill_installed(req.skill, version=req.version or "latest", metadata={"update_result": result, **req.metadata})
2101
- append_audit_event("skill_update", user_email=admin_email, plugin=req.plugin, skill=req.skill, workspace_os=True)
2102
- return {"skill": entry, "update": result}
2103
-
2104
-
2105
- @app.get("/workspace/audit-timeline")
2106
- async def workspace_audit_timeline(
2107
- request: Request,
2108
- user: Optional[str] = None,
2109
- event_type: Optional[str] = None,
2110
- model: Optional[str] = None,
2111
- since: Optional[str] = None,
2112
- until: Optional[str] = None,
2113
- limit: int = 100,
2114
- ):
2115
- require_admin(request)
2116
- return WORKSPACE_OS.filter_audit_timeline(
2117
- get_audit_log(),
2118
- user=user,
2119
- event_type=event_type,
2120
- model=model,
2121
- since=since,
2122
- until=until,
2123
- limit=limit,
2124
- )
2125
-
2126
-
2127
- @app.post("/workspace/vscode/send")
2128
- async def workspace_vscode_send(req: WorkspaceVSCodeRequest, request: Request):
2129
- current_user = require_user(request)
2130
- content = req.selection or req.content or req.prompt
2131
- workflow = WORKSPACE_OS.create_workflow(
2132
- name=f"VS Code: {req.action}",
2133
- steps=[
2134
- {"action": req.action, "file_path": req.file_path, "language": req.language},
2135
- {"action": "send_to_lattice", "chars": len(content or "")},
2136
- ],
2137
- metadata={
2138
- "file_path": req.file_path,
2139
- "language": req.language,
2140
- "content_preview": redact_secret_text(content or "")[:500],
2141
- },
2142
- user_email=current_user or None,
2143
- graph=_workspace_graph(),
2144
- )
2145
- if _workspace_graph() is not None and content:
2146
- try:
2147
- _workspace_graph().ingest_event(
2148
- "VSCodeWorkflow",
2149
- req.action,
2150
- user_email=current_user or None,
2151
- source="vscode",
2152
- metadata={
2153
- "file_path": req.file_path,
2154
- "language": req.language,
2155
- "chars": len(content),
2156
- "workflow_id": workflow["id"],
2157
- },
2158
- )
2159
- except Exception as exc:
2160
- logging.warning("vscode workflow graph ingest failed: %s", exc)
2161
- return {"status": "ok", "workflow": workflow}
2162
-
2163
-
2164
- # ── Organization Workspaces, membership, roles, and edition seam ─────────────────
2165
-
2166
- @app.get("/workspace/registry")
2167
- async def workspace_registry(request: Request):
2168
- user = require_user(request)
2169
- return WORKSPACE_OS.list_workspaces(user_id=user or None)
2170
-
2171
-
2172
- @app.get("/workspace/editions")
2173
- async def workspace_editions(request: Request):
2174
- require_user(request)
2175
- return capability_registry.describe()
2176
-
2177
-
2178
- @app.post("/workspace/activate")
2179
- async def workspace_activate(req: WorkspaceActivateRequest, request: Request):
2180
- user = require_user(request)
2181
- try:
2182
- return WORKSPACE_OS.set_active_workspace(req.workspace_id, user_id=user or None)
2183
- except FileNotFoundError as exc:
2184
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2185
- except PermissionError as exc:
2186
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2187
-
2188
-
2189
- @app.post("/workspace/orgs")
2190
- async def workspace_org_create(req: WorkspaceCreateRequest, request: Request):
2191
- user = require_user(request)
2192
- try:
2193
- workspace = WORKSPACE_OS.create_organization_workspace(
2194
- name=req.name,
2195
- owner_user_id=user or None,
2196
- settings=req.settings,
2197
- )
2198
- except ValueError as exc:
2199
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2200
- append_audit_event("workspace_created", user_email=user, workspace_id=workspace["workspace_id"])
2201
- return {"workspace": workspace}
2202
-
2203
-
2204
- @app.get("/workspace/orgs/{workspace_id}")
2205
- async def workspace_org_get(workspace_id: str, request: Request):
2206
- user = require_user(request)
2207
- try:
2208
- return {"workspace": WORKSPACE_OS.get_workspace(workspace_id, user_id=user or None)}
2209
- except FileNotFoundError as exc:
2210
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2211
-
2212
-
2213
- @app.get("/workspace/orgs/{workspace_id}/summary")
2214
- async def workspace_org_summary(workspace_id: str, request: Request):
2215
- user = require_user(request)
2216
- try:
2217
- return WORKSPACE_OS.workspace_summary(workspace_id, user_id=user or None)
2218
- except FileNotFoundError as exc:
2219
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2220
-
2221
-
2222
- @app.patch("/workspace/orgs/{workspace_id}")
2223
- async def workspace_org_update(workspace_id: str, req: WorkspaceUpdateRequest, request: Request):
2224
- user = require_user(request)
2225
- try:
2226
- workspace = WORKSPACE_OS.update_workspace(workspace_id, name=req.name, settings=req.settings, actor=user or None)
2227
- except FileNotFoundError as exc:
2228
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2229
- except PermissionError as exc:
2230
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2231
- except ValueError as exc:
2232
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2233
- append_audit_event("workspace_updated", user_email=user, workspace_id=workspace_id)
2234
- return {"workspace": workspace}
2235
-
2236
-
2237
- @app.post("/workspace/orgs/{workspace_id}/archive")
2238
- async def workspace_org_archive(workspace_id: str, request: Request):
2239
- user = require_user(request)
2240
- try:
2241
- workspace = WORKSPACE_OS.archive_workspace(workspace_id, actor=user or None)
2242
- except FileNotFoundError as exc:
2243
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2244
- except PermissionError as exc:
2245
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2246
- except ValueError as exc:
2247
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2248
- append_audit_event("workspace_archived", user_email=user, workspace_id=workspace_id)
2249
- return {"workspace": workspace}
2250
-
2251
-
2252
- @app.post("/workspace/orgs/{workspace_id}/members")
2253
- async def workspace_org_add_member(workspace_id: str, req: WorkspaceMemberRequest, request: Request):
2254
- user = require_user(request)
2255
- try:
2256
- workspace = WORKSPACE_OS.add_member(workspace_id, user_id=req.user_id, role=req.role, actor=user or None)
2257
- except FileNotFoundError as exc:
2258
- raise HTTPException(status_code=404, detail=f"Workspace not found: {exc}") from exc
2259
- except PermissionError as exc:
2260
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2261
- except ValueError as exc:
2262
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2263
- append_audit_event("workspace_member_added", user_email=user, workspace_id=workspace_id, member=req.user_id, role=req.role)
2264
- return {"workspace": workspace}
2265
-
2266
-
2267
- @app.patch("/workspace/orgs/{workspace_id}/members/{user_id}")
2268
- async def workspace_org_update_member(workspace_id: str, user_id: str, req: WorkspaceMemberRoleRequest, request: Request):
2269
- user = require_user(request)
2270
- try:
2271
- workspace = WORKSPACE_OS.update_member_role(workspace_id, user_id=user_id, role=req.role, actor=user or None)
2272
- except FileNotFoundError as exc:
2273
- raise HTTPException(status_code=404, detail=f"Not found: {exc}") from exc
2274
- except PermissionError as exc:
2275
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2276
- except ValueError as exc:
2277
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2278
- append_audit_event("workspace_member_role_updated", user_email=user, workspace_id=workspace_id, member=user_id, role=req.role)
2279
- return {"workspace": workspace}
2280
-
2281
-
2282
- @app.delete("/workspace/orgs/{workspace_id}/members/{user_id}")
2283
- async def workspace_org_remove_member(workspace_id: str, user_id: str, request: Request):
2284
- user = require_user(request)
2285
- try:
2286
- workspace = WORKSPACE_OS.remove_member(workspace_id, user_id=user_id, actor=user or None)
2287
- except FileNotFoundError as exc:
2288
- raise HTTPException(status_code=404, detail=f"Not found: {exc}") from exc
2289
- except PermissionError as exc:
2290
- raise HTTPException(status_code=403, detail=str(exc)) from exc
2291
- except ValueError as exc:
2292
- raise HTTPException(status_code=400, detail=str(exc)) from exc
2293
- append_audit_event("workspace_member_removed", user_email=user, workspace_id=workspace_id, member=user_id)
2294
- return {"workspace": workspace}
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
+ ))
2295
1673
 
2296
1674
 
2297
1675
  # ── Health & Info ──────────────────────────────────────────────────────────────
@@ -4182,37 +3560,22 @@ async def verify_cloud_models(force: bool = False, provider_filter: Optional[str
4182
3560
  results[model_ref] = record
4183
3561
  return results
4184
3562
 
4185
- @app.get("/health")
4186
- async def health(request: Request):
4187
- base = {
4188
- "status": "ok",
4189
- "version": APP_VERSION,
4190
- "mode": APP_MODE,
4191
- "platform": "AI Workspace OS",
4192
- }
4193
- if not get_current_user(request) and REQUIRE_AUTH:
4194
- return base
4195
- engines = await asyncio.to_thread(engine_status)
4196
- return {
4197
- **base,
4198
- "current_model": router.current_model_id,
4199
- "loaded_models": router.loaded_model_ids,
4200
- "device": "Apple Silicon MLX" if not IS_PUBLIC_MODE else "Public cloud/API runtime",
4201
- "features": runtime_features(),
4202
- "providers": router.detected_cloud_models(),
4203
- "engines": engines,
4204
- }
4205
-
4206
-
4207
- @app.get("/mode")
4208
- @app.get("/runtime_features")
4209
- async def mode():
4210
- return runtime_features()
4211
-
4212
-
4213
- @app.get("/engines")
4214
- async def engines():
4215
- 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
+ ))
4216
3579
 
4217
3580
 
4218
3581
  @app.post("/engines/install")
@@ -4615,7 +3978,7 @@ async def chat(req: ChatRequest, request: Request):
4615
3978
  except Exception:
4616
3979
  pass
4617
3980
 
4618
- trace_seed = WORKSPACE_OS.build_graph_trace(
3981
+ trace_seed = CHAT_SERVICE.build_graph_trace(
4619
3982
  req.message,
4620
3983
  KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
4621
3984
  context,
@@ -4653,7 +4016,7 @@ async def chat(req: ChatRequest, request: Request):
4653
4016
  full_text += footnote
4654
4017
  session.update(graph_md, full_text, req.conversation_id)
4655
4018
  save_to_history("assistant", full_text, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
4656
- trace_record = WORKSPACE_OS.record_trace(
4019
+ trace_record = CHAT_SERVICE.record_trace(
4657
4020
  question=req.message,
4658
4021
  response=full_text,
4659
4022
  conversation_id=req.conversation_id,
@@ -4679,7 +4042,7 @@ async def chat(req: ChatRequest, request: Request):
4679
4042
  result += footnote
4680
4043
  session.update(graph_md, result, req.conversation_id)
4681
4044
  save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
4682
- trace_record = WORKSPACE_OS.record_trace(
4045
+ trace_record = CHAT_SERVICE.record_trace(
4683
4046
  question=req.message,
4684
4047
  response=str(result),
4685
4048
  conversation_id=req.conversation_id,
@@ -4716,7 +4079,7 @@ async def chat(req: ChatRequest, request: Request):
4716
4079
  result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
4717
4080
 
4718
4081
  save_to_history("assistant", str(result), source=req.source or "web", conversation_id=req.conversation_id, **history_user)
4719
- trace_record = WORKSPACE_OS.record_trace(
4082
+ trace_record = CHAT_SERVICE.record_trace(
4720
4083
  question=req.message,
4721
4084
  response=str(result),
4722
4085
  conversation_id=req.conversation_id,
@@ -4820,12 +4183,12 @@ async def _stream_chat(
4820
4183
  yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
4821
4184
  history_user = get_history_user(req.user_email, req.user_nickname)
4822
4185
  save_to_history("assistant", full_response, source=req.source or "web", conversation_id=req.conversation_id, **history_user)
4823
- trace_record = WORKSPACE_OS.record_trace(
4186
+ trace_record = CHAT_SERVICE.record_trace(
4824
4187
  question=req.message,
4825
4188
  response=full_response,
4826
4189
  conversation_id=req.conversation_id,
4827
4190
  user_email=effective_email or req.user_email,
4828
- trace=trace_seed or WORKSPACE_OS.build_graph_trace(
4191
+ trace=trace_seed or CHAT_SERVICE.build_graph_trace(
4829
4192
  req.message,
4830
4193
  KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
4831
4194
  context,