ltcai 3.6.0 → 4.0.1
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 +39 -31
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/kg-schema.md +51 -53
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -520
- package/knowledge_graph.py +37 -4629
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +16 -17
- package/latticeai/api/agents.py +20 -7
- package/latticeai/api/auth.py +46 -15
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +139 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +5 -8
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +11 -16
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +85 -6
- package/latticeai/api/workspace.py +93 -57
- package/latticeai/app_factory.py +1781 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +66 -10
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +477 -29
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1536
- package/latticeai/services/agent_runtime.py +243 -4
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +47 -0
- package/latticeai/services/kg_portability.py +33 -3
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +55 -16
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +10 -20
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +105 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +33 -25
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
- package/static/v3/js/core/shell.js +66 -37
- package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
- package/static/v3/js/core/store.js +11 -1
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
- package/static/v3/js/views/knowledge-graph.js +60 -44
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/account.html +0 -115
- package/static/activity.html +0 -73
- package/static/admin.html +0 -488
- package/static/agents.html +0 -139
- package/static/chat.html +0 -844
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -124
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.2ce3815a.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Brain Network API — device identity, peer pairing, knowledge exchange.
|
|
2
|
+
|
|
3
|
+
The /network/receive endpoint authenticates PEERS (signed device requests),
|
|
4
|
+
not user sessions; everything else requires a logged-in user, and pairing /
|
|
5
|
+
pushing are deliberate owner actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PeerPairRequest(BaseModel):
|
|
17
|
+
name: str
|
|
18
|
+
base_url: str
|
|
19
|
+
public_key: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PeerPushRequest(BaseModel):
|
|
23
|
+
workspace_id: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_network_router(*, network, identity, require_user) -> APIRouter:
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
|
|
29
|
+
@router.get("/network/identity")
|
|
30
|
+
async def network_identity(request: Request):
|
|
31
|
+
require_user(request)
|
|
32
|
+
return identity.describe()
|
|
33
|
+
|
|
34
|
+
@router.get("/network/peers")
|
|
35
|
+
async def network_peers(request: Request):
|
|
36
|
+
require_user(request)
|
|
37
|
+
return {"peers": network.list_peers()}
|
|
38
|
+
|
|
39
|
+
@router.post("/network/peers")
|
|
40
|
+
async def network_pair(req: PeerPairRequest, request: Request):
|
|
41
|
+
require_user(request)
|
|
42
|
+
try:
|
|
43
|
+
return {"status": "paired", "peer": network.add_peer(
|
|
44
|
+
name=req.name, base_url=req.base_url, public_key=req.public_key,
|
|
45
|
+
)}
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
48
|
+
|
|
49
|
+
@router.delete("/network/peers/{peer_id}")
|
|
50
|
+
async def network_unpair(peer_id: str, request: Request):
|
|
51
|
+
require_user(request)
|
|
52
|
+
try:
|
|
53
|
+
return network.remove_peer(peer_id)
|
|
54
|
+
except FileNotFoundError as exc:
|
|
55
|
+
raise HTTPException(status_code=404, detail=f"Unknown peer: {exc}") from exc
|
|
56
|
+
|
|
57
|
+
@router.post("/network/push/{peer_id}")
|
|
58
|
+
async def network_push(peer_id: str, req: PeerPushRequest, request: Request):
|
|
59
|
+
require_user(request)
|
|
60
|
+
try:
|
|
61
|
+
return network.push_to_peer(peer_id, workspace_id=req.workspace_id)
|
|
62
|
+
except FileNotFoundError as exc:
|
|
63
|
+
raise HTTPException(status_code=404, detail=f"Unknown peer: {exc}") from exc
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
raise HTTPException(status_code=502, detail=f"Push failed: {exc}") from exc
|
|
66
|
+
|
|
67
|
+
@router.post("/network/receive")
|
|
68
|
+
async def network_receive(request: Request):
|
|
69
|
+
# Peer-authenticated: a paired device's signature replaces the session.
|
|
70
|
+
body = await request.body()
|
|
71
|
+
try:
|
|
72
|
+
return network.receive(dict(request.headers), body)
|
|
73
|
+
except PermissionError as exc:
|
|
74
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
77
|
+
|
|
78
|
+
return router
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["create_network_router"]
|
package/latticeai/api/plugins.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Any, Callable, Dict, Optional
|
|
|
15
15
|
from fastapi import APIRouter, HTTPException, Request
|
|
16
16
|
from pydantic import BaseModel
|
|
17
17
|
|
|
18
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class PluginActionRequest(BaseModel):
|
|
20
22
|
plugin_id: str
|
|
@@ -50,12 +52,7 @@ def create_plugins_router(
|
|
|
50
52
|
@router.get("/plugins/sdk")
|
|
51
53
|
async def plugins_sdk_page(request: Request):
|
|
52
54
|
require_user(request)
|
|
53
|
-
|
|
54
|
-
raise HTTPException(status_code=404, detail="Plugin SDK UI not available.")
|
|
55
|
-
page = static_dir / "plugins.html"
|
|
56
|
-
if not page.exists():
|
|
57
|
-
raise HTTPException(status_code=404, detail="Plugin SDK UI not found.")
|
|
58
|
-
return ui_file_response(page)
|
|
55
|
+
return app_redirect("marketplace", request)
|
|
59
56
|
|
|
60
57
|
@router.get("/plugins/registry")
|
|
61
58
|
async def plugins_registry(request: Request):
|
|
@@ -10,12 +10,14 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import secrets
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Callable,
|
|
13
|
+
from typing import Any, Callable, Optional, Set
|
|
14
14
|
|
|
15
|
-
from fastapi import APIRouter,
|
|
15
|
+
from fastapi import APIRouter, Request
|
|
16
16
|
from fastapi.responses import StreamingResponse
|
|
17
17
|
from pydantic import BaseModel
|
|
18
18
|
|
|
19
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class PresenceRequest(BaseModel):
|
|
21
23
|
client_id: Optional[str] = None
|
|
@@ -36,12 +38,7 @@ def create_realtime_router(
|
|
|
36
38
|
@router.get("/activity")
|
|
37
39
|
async def activity_page(request: Request):
|
|
38
40
|
require_user(request)
|
|
39
|
-
|
|
40
|
-
raise HTTPException(status_code=404, detail="Activity UI not available.")
|
|
41
|
-
page = static_dir / "activity.html"
|
|
42
|
-
if not page.exists():
|
|
43
|
-
raise HTTPException(status_code=404, detail="Activity UI not found.")
|
|
44
|
-
return ui_file_response(page)
|
|
41
|
+
return app_redirect("activity", request)
|
|
45
42
|
|
|
46
43
|
@router.get("/realtime/stream")
|
|
47
44
|
async def realtime_stream(request: Request):
|
package/latticeai/api/search.py
CHANGED
|
@@ -47,17 +47,41 @@ class IndexRebuildRequest(BaseModel):
|
|
|
47
47
|
include_chunks: bool = True
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
class _ScopedSearchService:
|
|
51
|
+
"""Injects the caller's workspace scope into every search call —
|
|
52
|
+
enforcement lives at this one chokepoint, not in each handler."""
|
|
53
|
+
|
|
54
|
+
_SCOPED = {"keyword_search", "vector_search", "graph_search", "hybrid_search"}
|
|
55
|
+
|
|
56
|
+
def __init__(self, service: SearchService, allowed):
|
|
57
|
+
self._service = service
|
|
58
|
+
self._allowed = allowed
|
|
59
|
+
|
|
60
|
+
def __getattr__(self, name):
|
|
61
|
+
attr = getattr(self._service, name)
|
|
62
|
+
if name in self._SCOPED:
|
|
63
|
+
def scoped(*args, **kwargs):
|
|
64
|
+
kwargs.setdefault("allowed_workspaces", self._allowed)
|
|
65
|
+
return attr(*args, **kwargs)
|
|
66
|
+
return scoped
|
|
67
|
+
return attr
|
|
68
|
+
|
|
69
|
+
|
|
50
70
|
def create_search_router(
|
|
51
71
|
*,
|
|
52
72
|
service: SearchService,
|
|
53
73
|
require_user: Callable[[Request], str],
|
|
54
74
|
embedding_info: Optional[Callable[[], Dict[str, Any]]] = None,
|
|
75
|
+
allowed_workspaces_for: Optional[Callable[[Optional[str]], Any]] = None,
|
|
55
76
|
) -> APIRouter:
|
|
56
77
|
router = APIRouter()
|
|
57
78
|
|
|
58
79
|
def _guarded(request: Request) -> SearchService:
|
|
59
|
-
require_user(request)
|
|
60
|
-
|
|
80
|
+
user = require_user(request)
|
|
81
|
+
allowed = None
|
|
82
|
+
if allowed_workspaces_for is not None and user:
|
|
83
|
+
allowed = allowed_workspaces_for(user)
|
|
84
|
+
return _ScopedSearchService(service, allowed)
|
|
61
85
|
|
|
62
86
|
def _raise_graph_error(exc: Exception) -> None:
|
|
63
87
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
@@ -27,13 +27,12 @@ import io
|
|
|
27
27
|
import json
|
|
28
28
|
import logging
|
|
29
29
|
import re
|
|
30
|
-
import time
|
|
31
30
|
from collections import defaultdict
|
|
32
31
|
from datetime import datetime
|
|
33
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
32
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
34
33
|
|
|
35
34
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
36
|
-
from fastapi.responses import Response
|
|
35
|
+
from fastapi.responses import Response
|
|
37
36
|
from pydantic import BaseModel
|
|
38
37
|
|
|
39
38
|
from ..core import timezones
|
package/latticeai/api/setup.py
CHANGED
|
@@ -15,8 +15,8 @@ from auto_setup import (
|
|
|
15
15
|
recommend as auto_setup_recommend,
|
|
16
16
|
verify as auto_setup_verify,
|
|
17
17
|
)
|
|
18
|
-
from
|
|
19
|
-
from
|
|
18
|
+
from latticeai.models.router import parse_model_ref
|
|
19
|
+
from setup_wizard import get_recommendations, install_stream, open_url, scan_environment
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def create_setup_router(*, model_router, require_user) -> APIRouter:
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
import subprocess
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from pathlib import Path
|
|
@@ -11,6 +10,8 @@ from typing import Callable, Optional
|
|
|
11
10
|
from fastapi import APIRouter, Cookie, HTTPException, Request
|
|
12
11
|
from fastapi.responses import FileResponse, HTMLResponse
|
|
13
12
|
|
|
13
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
14
|
+
|
|
14
15
|
def ui_file_response(path: Path) -> FileResponse:
|
|
15
16
|
response = FileResponse(path)
|
|
16
17
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
@@ -45,20 +46,20 @@ def create_static_routes_router(
|
|
|
45
46
|
async def root(request: Request, code: Optional[str] = None, authorized: Optional[str] = Cookie(None)):
|
|
46
47
|
"""로그인/회원가입 페이지. 초대 게이트 활성화 시 코드 검증 후 진입."""
|
|
47
48
|
if not INVITE_GATE_ENABLED:
|
|
48
|
-
return
|
|
49
|
+
return app_redirect("account", request)
|
|
49
50
|
|
|
50
51
|
# 1. 이미 쿠키로 인증된 경우
|
|
51
52
|
if authorized == "true":
|
|
52
|
-
return
|
|
53
|
+
return app_redirect("account", request)
|
|
53
54
|
|
|
54
55
|
# 2. 초대 코드가 일치하는 경우 (최초 진입)
|
|
55
56
|
if code == INVITE_CODE:
|
|
56
|
-
response =
|
|
57
|
+
response = app_redirect("account", request)
|
|
57
58
|
response.set_cookie(key="authorized", value="true", httponly=True, samesite="lax", max_age=60*60*24*7)
|
|
58
59
|
return response
|
|
59
60
|
|
|
60
61
|
# 3. 인증 실패 시 차단 화면
|
|
61
|
-
return HTMLResponse(content=
|
|
62
|
+
return HTMLResponse(content="""
|
|
62
63
|
<body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
|
63
64
|
<div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
|
|
64
65
|
<div style="font-size:48px; margin-bottom:20px;">🔒</div>
|
|
@@ -73,7 +74,7 @@ def create_static_routes_router(
|
|
|
73
74
|
@api_router.get("/account")
|
|
74
75
|
async def account_page():
|
|
75
76
|
"""Direct login/register page route used by logout and manual navigation."""
|
|
76
|
-
return
|
|
77
|
+
return app_redirect("account")
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
@api_router.get("/manifest.json")
|
|
@@ -106,7 +107,7 @@ def create_static_routes_router(
|
|
|
106
107
|
|
|
107
108
|
@api_router.get("/chat")
|
|
108
109
|
async def chat_page(request: Request):
|
|
109
|
-
return
|
|
110
|
+
return app_redirect("chat", request)
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
@api_router.get("/app")
|
|
@@ -119,13 +120,8 @@ def create_static_routes_router(
|
|
|
119
120
|
|
|
120
121
|
|
|
121
122
|
@api_router.get("/admin")
|
|
122
|
-
async def admin_page():
|
|
123
|
-
|
|
124
|
-
if not admin_path.exists():
|
|
125
|
-
raise HTTPException(status_code=404, detail="Admin UI not found.")
|
|
126
|
-
response = FileResponse(admin_path)
|
|
127
|
-
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
128
|
-
return response
|
|
123
|
+
async def admin_page(request: Request):
|
|
124
|
+
return app_redirect("admin/users", request)
|
|
129
125
|
|
|
130
126
|
# /workspace and /onboarding UI pages are served by the workspace router
|
|
131
127
|
# (latticeai.api.workspace), included below after its dependencies are defined.
|
|
@@ -145,7 +141,7 @@ def create_static_routes_router(
|
|
|
145
141
|
async def local_sysinfo(request: Request):
|
|
146
142
|
"""CPU / RAM / GPU(MLX) 사용량을 반환합니다."""
|
|
147
143
|
require_user(request)
|
|
148
|
-
import
|
|
144
|
+
import re as _re
|
|
149
145
|
result = {"cpu_pct": 0.0, "ram_pct": 0.0, "gpu_mem_pct": 0.0, "gpu_mem_gb": 0.0}
|
|
150
146
|
try:
|
|
151
147
|
# CPU
|
|
@@ -157,7 +153,6 @@ def create_static_routes_router(
|
|
|
157
153
|
result["cpu_pct"] = round(float(m.group(1)) + float(m.group(2)), 1)
|
|
158
154
|
# RAM
|
|
159
155
|
vm_out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=4).stdout
|
|
160
|
-
page_size = 16384
|
|
161
156
|
pages: dict = {}
|
|
162
157
|
for line in vm_out.splitlines():
|
|
163
158
|
for key in ["Pages free", "Pages active", "Pages inactive", "Pages wired down", "Pages occupied by compressor"]:
|
package/latticeai/api/tools.py
CHANGED
|
@@ -178,6 +178,7 @@ class ToolGitShowRequest(BaseModel):
|
|
|
178
178
|
def create_tools_router(
|
|
179
179
|
*,
|
|
180
180
|
config,
|
|
181
|
+
ingestion_pipeline,
|
|
181
182
|
data_dir: Path,
|
|
182
183
|
static_dir: Path,
|
|
183
184
|
model_router,
|
|
@@ -438,6 +439,7 @@ def create_tools_router(
|
|
|
438
439
|
current_user=current_user,
|
|
439
440
|
enable_graph=ENABLE_GRAPH,
|
|
440
441
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
442
|
+
ingestion_pipeline=ingestion_pipeline,
|
|
441
443
|
bytes_match_extension=_bytes_match_extension,
|
|
442
444
|
classify_sensitive_message=classify_sensitive_message,
|
|
443
445
|
append_audit_event=append_audit_event,
|
|
@@ -588,6 +590,7 @@ def create_tools_router(
|
|
|
588
590
|
tool_response=_tool_response,
|
|
589
591
|
require_graph=_require_graph,
|
|
590
592
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
593
|
+
ingestion_pipeline=ingestion_pipeline,
|
|
591
594
|
data_dir=DATA_DIR,
|
|
592
595
|
))
|
|
593
596
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Compatibility redirects from retired legacy pages into the v4 SPA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
from fastapi.responses import RedirectResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def app_redirect(fragment: str, request: Optional[Request] = None) -> RedirectResponse:
|
|
12
|
+
"""Redirect a legacy GET route to the equivalent /app hash route.
|
|
13
|
+
|
|
14
|
+
Existing browser bookmarks keep working while the legacy HTML/JS/CSS pages
|
|
15
|
+
are removed from the shipped artifact. Query strings are preserved after the
|
|
16
|
+
hash so SPA route params remain addressable.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
frag = fragment.strip("/")
|
|
20
|
+
query = ""
|
|
21
|
+
if request is not None and request.url.query:
|
|
22
|
+
query = f"?{request.url.query}"
|
|
23
|
+
return RedirectResponse(url=f"/app#/{frag}{query}", status_code=308)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["app_redirect"]
|
|
@@ -19,6 +19,8 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
19
19
|
from fastapi import APIRouter, HTTPException, Request
|
|
20
20
|
from pydantic import BaseModel
|
|
21
21
|
|
|
22
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
class WorkflowDefinitionRequest(BaseModel):
|
|
24
26
|
name: str
|
|
@@ -32,6 +34,10 @@ class WorkflowUpdateRequest(BaseModel):
|
|
|
32
34
|
metadata: Optional[Dict[str, Any]] = None
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
class WorkflowResumeRequest(BaseModel):
|
|
38
|
+
approved: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
class WorkflowRunRequest(BaseModel):
|
|
36
42
|
inputs: Dict[str, Any] = {}
|
|
37
43
|
|
|
@@ -58,6 +64,8 @@ def create_workflow_designer_router(
|
|
|
58
64
|
ui_file_response: Optional[Callable[[Path], Any]] = None,
|
|
59
65
|
static_dir: Optional[Path] = None,
|
|
60
66
|
hooks: Any = None,
|
|
67
|
+
run_executor: Any = None,
|
|
68
|
+
trigger_service: Any = None,
|
|
61
69
|
) -> APIRouter:
|
|
62
70
|
from latticeai.core.workflow_engine import (
|
|
63
71
|
WorkflowEngine,
|
|
@@ -72,12 +80,7 @@ def create_workflow_designer_router(
|
|
|
72
80
|
@router.get("/workflows")
|
|
73
81
|
async def workflows_page(request: Request):
|
|
74
82
|
require_user(request)
|
|
75
|
-
|
|
76
|
-
raise HTTPException(status_code=404, detail="Workflow Designer UI not available.")
|
|
77
|
-
page = static_dir / "workflows.html"
|
|
78
|
-
if not page.exists():
|
|
79
|
-
raise HTTPException(status_code=404, detail="Workflow Designer UI not found.")
|
|
80
|
-
return ui_file_response(page)
|
|
83
|
+
return app_redirect("workflows", request)
|
|
81
84
|
|
|
82
85
|
@router.get("/workflows/api/definitions")
|
|
83
86
|
async def list_definitions(request: Request, q: str = ""):
|
|
@@ -147,6 +150,16 @@ def create_workflow_designer_router(
|
|
|
147
150
|
workflow = store.get_workflow(workflow_id, workspace_id=scope)
|
|
148
151
|
except FileNotFoundError as exc:
|
|
149
152
|
raise HTTPException(status_code=404, detail=f"Workflow not found: {exc}") from exc
|
|
153
|
+
if run_executor is not None:
|
|
154
|
+
result = await run_executor.start_workflow(
|
|
155
|
+
workflow,
|
|
156
|
+
workflow_id=workflow_id,
|
|
157
|
+
user_email=current_user or None,
|
|
158
|
+
scope=scope,
|
|
159
|
+
inputs=req.inputs,
|
|
160
|
+
)
|
|
161
|
+
append_audit_event("workflow_run_queued", user_email=current_user, workflow_id=workflow_id, status="queued")
|
|
162
|
+
return result
|
|
150
163
|
runners = build_runners(current_user or None, scope)
|
|
151
164
|
engine = WorkflowEngine(runners, hooks=hooks)
|
|
152
165
|
result = engine.run(workflow, inputs=req.inputs)
|
|
@@ -159,10 +172,69 @@ def create_workflow_designer_router(
|
|
|
159
172
|
user_email=current_user or None,
|
|
160
173
|
graph=workspace_graph(),
|
|
161
174
|
workspace_id=scope,
|
|
175
|
+
mode="live",
|
|
176
|
+
pause={"node": result.paused_node, "pending": result.pending_approval,
|
|
177
|
+
"context": result.paused_context} if result.status == "awaiting_approval" else None,
|
|
162
178
|
)
|
|
163
179
|
append_audit_event("workflow_run", user_email=current_user, workflow_id=workflow_id, status=result.status)
|
|
164
180
|
return {"run": run, "result": result.as_dict()}
|
|
165
181
|
|
|
182
|
+
@router.post("/workflows/api/runs/{run_id}/stop")
|
|
183
|
+
async def stop_run(run_id: str, request: Request):
|
|
184
|
+
require_user(request)
|
|
185
|
+
scope = gate_write(request)
|
|
186
|
+
if run_executor is None:
|
|
187
|
+
try:
|
|
188
|
+
run = store.get_workflow_run(run_id, workspace_id=scope)
|
|
189
|
+
except FileNotFoundError as exc:
|
|
190
|
+
raise HTTPException(status_code=404, detail=f"Workflow run not found: {run_id}") from exc
|
|
191
|
+
return {
|
|
192
|
+
"stopped": False,
|
|
193
|
+
"reason": "asynchronous cancellation is not supported by the synchronous runtime",
|
|
194
|
+
"run_id": run_id,
|
|
195
|
+
"status": run.get("status"),
|
|
196
|
+
}
|
|
197
|
+
return run_executor.cancel(run_id, kind="workflow", scope=scope)
|
|
198
|
+
|
|
199
|
+
@router.post("/workflows/api/runs/{run_id}/resume")
|
|
200
|
+
async def resume_run(run_id: str, req: WorkflowResumeRequest, request: Request):
|
|
201
|
+
"""Decide a paused (awaiting_approval) run: approve → the paused node
|
|
202
|
+
executes and the run continues; deny → the run fails honestly."""
|
|
203
|
+
current_user = require_user(request)
|
|
204
|
+
scope = gate_write(request)
|
|
205
|
+
run_record = store.get_workflow_run(run_id, workspace_id=scope)
|
|
206
|
+
pause = run_record.get("pause") or {}
|
|
207
|
+
if run_record.get("status") != "awaiting_approval" or not pause.get("node"):
|
|
208
|
+
raise HTTPException(status_code=409, detail="run is not awaiting approval")
|
|
209
|
+
workflow = store.get_workflow(run_record.get("workflow_id"), workspace_id=scope)
|
|
210
|
+
runners = build_runners(current_user or None, scope)
|
|
211
|
+
engine = WorkflowEngine(runners, hooks=hooks)
|
|
212
|
+
result = engine.resume(
|
|
213
|
+
workflow,
|
|
214
|
+
paused_node=pause["node"],
|
|
215
|
+
paused_context=pause.get("context") or {},
|
|
216
|
+
approved=bool(req.approved),
|
|
217
|
+
prior_timeline=run_record.get("timeline") or [],
|
|
218
|
+
)
|
|
219
|
+
resumed = store.record_workflow_run(
|
|
220
|
+
workflow_id=run_record.get("workflow_id"),
|
|
221
|
+
name=run_record.get("name") or "workflow",
|
|
222
|
+
status=result.status,
|
|
223
|
+
timeline=result.timeline,
|
|
224
|
+
outputs=result.outputs,
|
|
225
|
+
user_email=current_user or None,
|
|
226
|
+
graph=workspace_graph(),
|
|
227
|
+
workspace_id=scope,
|
|
228
|
+
mode="live",
|
|
229
|
+
pause={"node": result.paused_node, "pending": result.pending_approval,
|
|
230
|
+
"context": result.paused_context} if result.status == "awaiting_approval" else None,
|
|
231
|
+
)
|
|
232
|
+
store.mark_workflow_run_resolved(run_id, resumed_run_id=resumed["id"],
|
|
233
|
+
approved=bool(req.approved), workspace_id=scope)
|
|
234
|
+
append_audit_event("workflow_run_resume", user_email=current_user,
|
|
235
|
+
run_id=run_id, approved=bool(req.approved), status=result.status)
|
|
236
|
+
return {"run": resumed, "result": result.as_dict(), "resumed_from": run_id}
|
|
237
|
+
|
|
166
238
|
@router.get("/workflows/api/definitions/{workflow_id}/runs")
|
|
167
239
|
async def list_runs(workflow_id: str, request: Request, limit: int = 50):
|
|
168
240
|
require_user(request)
|
|
@@ -175,6 +247,13 @@ def create_workflow_designer_router(
|
|
|
175
247
|
scope = gate_read(request)
|
|
176
248
|
return store.list_workflow_runs(limit=limit, workspace_id=scope)
|
|
177
249
|
|
|
250
|
+
@router.get("/workflows/api/triggers")
|
|
251
|
+
async def trigger_status(request: Request):
|
|
252
|
+
require_user(request)
|
|
253
|
+
if trigger_service is None:
|
|
254
|
+
return {"running": False, "tick_seconds": None, "armed": []}
|
|
255
|
+
return trigger_service.describe()
|
|
256
|
+
|
|
178
257
|
@router.get("/workflows/api/runs/{run_id}/replay")
|
|
179
258
|
async def workflow_run_replay(run_id: str, request: Request):
|
|
180
259
|
require_user(request)
|