ltcai 2.2.2 → 3.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 +66 -27
- package/codex_telegram_bot.py +6 -2
- package/docs/CHANGELOG.md +154 -0
- package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
- package/docs/V3_FRONTEND.md +136 -0
- package/knowledge_graph.py +649 -21
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +47 -0
- package/latticeai/api/agents.py +54 -31
- package/latticeai/api/auth.py +1 -1
- package/latticeai/api/chat.py +10 -2
- package/latticeai/api/search.py +236 -0
- package/latticeai/api/static_routes.py +21 -2
- package/latticeai/core/config.py +16 -0
- package/latticeai/core/embedding_providers.py +502 -0
- package/latticeai/core/local_embeddings.py +86 -0
- package/latticeai/core/logging_safety.py +62 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +49 -1
- package/latticeai/services/agent_runtime.py +245 -0
- package/latticeai/services/search_service.py +346 -0
- package/package.json +8 -4
- package/static/account.html +9 -4
- package/static/activity.html +4 -4
- package/static/admin.html +8 -3
- package/static/agents.html +4 -4
- package/static/chat.html +16 -11
- package/static/css/reference/account.css +439 -0
- package/static/css/reference/admin.css +610 -0
- package/static/css/reference/base.css +1658 -0
- package/static/{lattice-reference.css → css/reference/chat.css} +271 -3633
- package/static/css/reference/graph.css +1016 -0
- package/static/css/responsive.css +248 -1
- package/static/css/tokens.css +132 -126
- package/static/favicon.ico +0 -0
- package/static/graph.html +9 -4
- package/static/manifest.json +3 -3
- package/static/platform.css +1 -1
- package/static/plugins.html +4 -4
- package/static/scripts/account.js +4 -4
- package/static/scripts/chat.js +227 -77
- package/static/scripts/workspace.js +78 -0
- package/static/sw.js +5 -3
- package/static/v3/css/lattice.base.css +128 -0
- package/static/v3/css/lattice.components.css +447 -0
- package/static/v3/css/lattice.shell.css +407 -0
- package/static/v3/css/lattice.tokens.css +132 -0
- package/static/v3/css/lattice.views.css +277 -0
- package/static/v3/index.html +40 -0
- package/static/v3/js/app.js +26 -0
- package/static/v3/js/core/api.js +327 -0
- package/static/v3/js/core/components.js +215 -0
- package/static/v3/js/core/dom.js +148 -0
- package/static/v3/js/core/fixtures.js +171 -0
- package/static/v3/js/core/router.js +37 -0
- package/static/v3/js/core/routes.js +73 -0
- package/static/v3/js/core/shell.js +363 -0
- package/static/v3/js/core/store.js +113 -0
- package/static/v3/js/views/admin-audit.js +185 -0
- package/static/v3/js/views/admin-permissions.js +178 -0
- package/static/v3/js/views/admin-policies.js +103 -0
- package/static/v3/js/views/admin-private-vpc.js +138 -0
- package/static/v3/js/views/admin-security.js +181 -0
- package/static/v3/js/views/admin-users.js +168 -0
- package/static/v3/js/views/agents.js +194 -0
- package/static/v3/js/views/chat.js +450 -0
- package/static/v3/js/views/files.js +180 -0
- package/static/v3/js/views/home.js +119 -0
- package/static/v3/js/views/hybrid-search.js +195 -0
- package/static/v3/js/views/knowledge-graph.js +238 -0
- package/static/v3/js/views/models.js +247 -0
- package/static/v3/js/views/my-computer.js +237 -0
- package/static/v3/js/views/pipeline.js +161 -0
- package/static/v3/js/views/settings.js +258 -0
- package/static/workflows.html +4 -4
- package/static/workspace.css +408 -14
- package/static/workspace.html +43 -24
- package/telegram_bot.py +18 -14
package/latticeai/__init__.py
CHANGED
package/latticeai/api/admin.py
CHANGED
|
@@ -109,6 +109,53 @@ def create_admin_router(
|
|
|
109
109
|
report["graph"] = {"error": str(e)}
|
|
110
110
|
return report
|
|
111
111
|
|
|
112
|
+
# Canonical RBAC capability map — which product areas each role can reach.
|
|
113
|
+
# This is the real access policy the app enforces, not sample data.
|
|
114
|
+
_ROLE_CAPS = {
|
|
115
|
+
"owner": ["all"],
|
|
116
|
+
"admin": ["users", "policies", "audit", "security", "chat", "search", "files", "pipeline"],
|
|
117
|
+
"member": ["chat", "search", "files", "pipeline"],
|
|
118
|
+
"user": ["chat", "search", "files", "pipeline"],
|
|
119
|
+
"viewer": ["chat", "search"],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@router.get("/admin/roles")
|
|
123
|
+
async def admin_roles(request: Request):
|
|
124
|
+
_, users = require_admin(request)
|
|
125
|
+
counts: Dict[str, int] = defaultdict(int)
|
|
126
|
+
for email, user in users.items():
|
|
127
|
+
role = (get_user_role(email, users) or "user").lower()
|
|
128
|
+
counts[role] += 1
|
|
129
|
+
# Always surface the two RBAC tiers the app actually distinguishes so the
|
|
130
|
+
# matrix is meaningful even before extra users exist.
|
|
131
|
+
for base in ("admin", "user"):
|
|
132
|
+
counts.setdefault(base, counts.get(base, 0))
|
|
133
|
+
roles = [
|
|
134
|
+
{"role": role, "members": counts.get(role, 0), "caps": _ROLE_CAPS.get(role, ["chat", "search"])}
|
|
135
|
+
for role in sorted(counts, key=lambda r: (r != "owner", r != "admin", r))
|
|
136
|
+
]
|
|
137
|
+
return {"roles": roles}
|
|
138
|
+
|
|
139
|
+
@router.get("/admin/policies")
|
|
140
|
+
async def admin_policies(request: Request):
|
|
141
|
+
require_admin(request)
|
|
142
|
+
# The real, enforced governance posture of a local-first deployment.
|
|
143
|
+
return {
|
|
144
|
+
"policies": [
|
|
145
|
+
{"id": "local_file_access", "label": "Local file access",
|
|
146
|
+
"value": "Approval-token gated (per path/user/action)", "enforced": True},
|
|
147
|
+
{"id": "package_install", "label": "Package install",
|
|
148
|
+
"value": "Admin-only with audit trail", "enforced": True},
|
|
149
|
+
{"id": "data_residency", "label": "Data residency",
|
|
150
|
+
"value": "Single-tenant local storage (~/.ltcai)", "enforced": True},
|
|
151
|
+
{"id": "model_egress", "label": "Model egress",
|
|
152
|
+
"value": "Local-only by default (no external inference in local mode)", "enforced": True},
|
|
153
|
+
{"id": "invite_gate", "label": "Invite gate",
|
|
154
|
+
"value": "Required for new accounts" if invite_gate_enabled else "Open registration",
|
|
155
|
+
"enforced": bool(invite_gate_enabled)},
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
|
|
112
159
|
@router.get("/vpc/status")
|
|
113
160
|
async def vpc_status(request: Request):
|
|
114
161
|
require_user(request)
|
package/latticeai/api/agents.py
CHANGED
|
@@ -40,11 +40,54 @@ def create_agents_router(
|
|
|
40
40
|
append_audit_event: Callable[..., None],
|
|
41
41
|
ui_file_response: Optional[Callable[[Path], Any]] = None,
|
|
42
42
|
static_dir: Optional[Path] = None,
|
|
43
|
+
agent_runtime: Any = None,
|
|
43
44
|
) -> APIRouter:
|
|
44
45
|
from latticeai.core.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
|
|
46
|
+
from latticeai.services.agent_runtime import AgentRuntime
|
|
47
|
+
|
|
48
|
+
# Single AgentRuntime boundary: the router (and via it, the frontend) talks
|
|
49
|
+
# to this façade instead of reaching into the orchestrator/store directly.
|
|
50
|
+
runtime = agent_runtime or AgentRuntime(
|
|
51
|
+
store=store,
|
|
52
|
+
orchestrator_factory=orchestrator_factory,
|
|
53
|
+
workspace_graph=workspace_graph,
|
|
54
|
+
append_audit_event=append_audit_event,
|
|
55
|
+
)
|
|
45
56
|
|
|
46
57
|
router = APIRouter()
|
|
47
58
|
|
|
59
|
+
# ── AgentRuntime boundary endpoints ───────────────────────────────────
|
|
60
|
+
@router.get("/agents/api/runtime/status")
|
|
61
|
+
async def agent_runtime_status(request: Request):
|
|
62
|
+
require_user(request)
|
|
63
|
+
scope = gate_read(request)
|
|
64
|
+
return runtime.status(scope=scope)
|
|
65
|
+
|
|
66
|
+
@router.get("/agents/api/runtime/health")
|
|
67
|
+
async def agent_runtime_health(request: Request):
|
|
68
|
+
require_user(request)
|
|
69
|
+
return runtime.health()
|
|
70
|
+
|
|
71
|
+
@router.get("/agents/api/runtime/config")
|
|
72
|
+
async def agent_runtime_config(request: Request):
|
|
73
|
+
require_user(request)
|
|
74
|
+
return runtime.config()
|
|
75
|
+
|
|
76
|
+
@router.get("/agents/api/runs/{run_id}/events")
|
|
77
|
+
async def agent_run_events(run_id: str, request: Request):
|
|
78
|
+
require_user(request)
|
|
79
|
+
scope = gate_read(request)
|
|
80
|
+
try:
|
|
81
|
+
return runtime.events(run_id, scope=scope)
|
|
82
|
+
except FileNotFoundError as exc:
|
|
83
|
+
raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
|
|
84
|
+
|
|
85
|
+
@router.post("/agents/api/runs/{run_id}/stop")
|
|
86
|
+
async def agent_run_stop(run_id: str, request: Request):
|
|
87
|
+
require_user(request)
|
|
88
|
+
scope = gate_write(request)
|
|
89
|
+
return runtime.stop(run_id, scope=scope)
|
|
90
|
+
|
|
48
91
|
@router.get("/agents")
|
|
49
92
|
async def agents_page(request: Request):
|
|
50
93
|
require_user(request)
|
|
@@ -119,36 +162,16 @@ def create_agents_router(
|
|
|
119
162
|
async def agent_run(req: AgentRunRequest, request: Request):
|
|
120
163
|
current_user = require_user(request)
|
|
121
164
|
scope = gate_write(request)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
run = store.record_agent_run(
|
|
134
|
-
agent_id=result.agent_id,
|
|
135
|
-
status=result.status,
|
|
136
|
-
input_text=req.goal,
|
|
137
|
-
output_text=result.output,
|
|
138
|
-
timeline=result.timeline,
|
|
139
|
-
relationships=[ROLE_AGENT_IDS.get(r, f"agent:{r}") for r in result.roles_run],
|
|
140
|
-
handoffs=result.handoffs,
|
|
141
|
-
context_packets=result.context_packets,
|
|
142
|
-
plan=result.plan,
|
|
143
|
-
plan_review=result.plan_review,
|
|
144
|
-
review_history=result.review_history,
|
|
145
|
-
retry_history=result.retry_history,
|
|
146
|
-
memory_snapshots=result.memory_snapshots,
|
|
147
|
-
user_email=current_user or None,
|
|
148
|
-
graph=workspace_graph(),
|
|
149
|
-
workspace_id=scope,
|
|
150
|
-
)
|
|
151
|
-
append_audit_event("multi_agent_run", user_email=current_user, agent_id=result.agent_id, status=result.status, retries=result.retries)
|
|
152
|
-
return {"run": run, "result": result.as_dict()}
|
|
165
|
+
try:
|
|
166
|
+
return runtime.start(
|
|
167
|
+
req.goal,
|
|
168
|
+
user_email=current_user or None,
|
|
169
|
+
scope=scope,
|
|
170
|
+
roles=req.roles or None,
|
|
171
|
+
inputs=req.inputs,
|
|
172
|
+
max_retries=req.max_retries,
|
|
173
|
+
)
|
|
174
|
+
except ValueError as exc:
|
|
175
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
153
176
|
|
|
154
177
|
return router
|
package/latticeai/api/auth.py
CHANGED
|
@@ -167,7 +167,7 @@ def create_auth_router(
|
|
|
167
167
|
if users[email].get("disabled"):
|
|
168
168
|
raise HTTPException(status_code=403, detail="비활성화된 계정입니다.")
|
|
169
169
|
token = create_session(email)
|
|
170
|
-
resp = RedirectResponse("/
|
|
170
|
+
resp = RedirectResponse("/app", status_code=302)
|
|
171
171
|
resp.set_cookie("session_token", token, httponly=True, samesite="lax", max_age=session_ttl)
|
|
172
172
|
return resp
|
|
173
173
|
|
package/latticeai/api/chat.py
CHANGED
|
@@ -342,9 +342,17 @@ def create_chat_router(
|
|
|
342
342
|
|
|
343
343
|
if not router.current_model_id:
|
|
344
344
|
detail = "No model loaded. Call /models/load first."
|
|
345
|
-
if
|
|
345
|
+
if CONFIG.is_public:
|
|
346
346
|
detail = f"No public model loaded. Set OPENAI_API_KEY and LATTICEAI_PUBLIC_MODEL={PUBLIC_MODEL}, or call /models/load with an OpenAI-compatible model."
|
|
347
|
-
|
|
347
|
+
return JSONResponse(
|
|
348
|
+
status_code=400,
|
|
349
|
+
content={
|
|
350
|
+
"error": "no_model_loaded",
|
|
351
|
+
"detail": detail,
|
|
352
|
+
"message": detail,
|
|
353
|
+
"action": "load_model",
|
|
354
|
+
},
|
|
355
|
+
)
|
|
348
356
|
|
|
349
357
|
if req.model and req.model != router.current_model_id:
|
|
350
358
|
if req.model not in router.loaded_model_ids:
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""v3 knowledge graph, vector, and hybrid search API contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from latticeai.services.search_service import DEFAULT_HYBRID_WEIGHTS, SearchService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchRequest(BaseModel):
|
|
14
|
+
query: str = Field(..., min_length=1)
|
|
15
|
+
limit: int = 30
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VectorSearchRequest(SearchRequest):
|
|
19
|
+
min_score: float = 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HybridSearchRequest(SearchRequest):
|
|
23
|
+
keyword_limit: int = 30
|
|
24
|
+
vector_limit: int = 30
|
|
25
|
+
graph_limit: int = 30
|
|
26
|
+
weights: Optional[Dict[str, float]] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphNodeRequest(BaseModel):
|
|
30
|
+
node_id: str = Field(..., min_length=1)
|
|
31
|
+
include_neighbors: bool = True
|
|
32
|
+
depth: int = 1
|
|
33
|
+
limit: int = 100
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RelationshipSearchRequest(BaseModel):
|
|
37
|
+
query: str = ""
|
|
38
|
+
node_id: str = ""
|
|
39
|
+
relationship_type: str = ""
|
|
40
|
+
limit: int = 30
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IndexRebuildRequest(BaseModel):
|
|
44
|
+
full: bool = False
|
|
45
|
+
include_nodes: bool = True
|
|
46
|
+
include_chunks: bool = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_search_router(
|
|
50
|
+
*,
|
|
51
|
+
service: SearchService,
|
|
52
|
+
require_user: Callable[[Request], str],
|
|
53
|
+
embedding_info: Optional[Callable[[], Dict[str, Any]]] = None,
|
|
54
|
+
) -> APIRouter:
|
|
55
|
+
router = APIRouter()
|
|
56
|
+
|
|
57
|
+
def _guarded(request: Request) -> SearchService:
|
|
58
|
+
require_user(request)
|
|
59
|
+
return service
|
|
60
|
+
|
|
61
|
+
def _raise_graph_error(exc: Exception) -> None:
|
|
62
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
@router.post("/api/search/hybrid")
|
|
65
|
+
async def hybrid_search(req: HybridSearchRequest, request: Request) -> Dict[str, Any]:
|
|
66
|
+
try:
|
|
67
|
+
return _guarded(request).hybrid_search(
|
|
68
|
+
req.query,
|
|
69
|
+
limit=req.limit,
|
|
70
|
+
keyword_limit=req.keyword_limit,
|
|
71
|
+
vector_limit=req.vector_limit,
|
|
72
|
+
graph_limit=req.graph_limit,
|
|
73
|
+
weights=req.weights or DEFAULT_HYBRID_WEIGHTS,
|
|
74
|
+
)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
_raise_graph_error(exc)
|
|
77
|
+
|
|
78
|
+
@router.get("/api/search/hybrid")
|
|
79
|
+
async def hybrid_search_get(q: str, request: Request, limit: int = 30) -> Dict[str, Any]:
|
|
80
|
+
try:
|
|
81
|
+
return _guarded(request).hybrid_search(q, limit=limit)
|
|
82
|
+
except ValueError as exc:
|
|
83
|
+
_raise_graph_error(exc)
|
|
84
|
+
|
|
85
|
+
@router.post("/api/search/keyword")
|
|
86
|
+
async def keyword_search(req: SearchRequest, request: Request) -> Dict[str, Any]:
|
|
87
|
+
try:
|
|
88
|
+
return _guarded(request).keyword_search(req.query, limit=req.limit)
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
_raise_graph_error(exc)
|
|
91
|
+
|
|
92
|
+
@router.get("/api/search/keyword")
|
|
93
|
+
async def keyword_search_get(q: str, request: Request, limit: int = 30) -> Dict[str, Any]:
|
|
94
|
+
try:
|
|
95
|
+
return _guarded(request).keyword_search(q, limit=limit)
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
_raise_graph_error(exc)
|
|
98
|
+
|
|
99
|
+
@router.post("/api/search/vector")
|
|
100
|
+
async def vector_search(req: VectorSearchRequest, request: Request) -> Dict[str, Any]:
|
|
101
|
+
try:
|
|
102
|
+
return _guarded(request).vector_search(req.query, limit=req.limit, min_score=req.min_score)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
_raise_graph_error(exc)
|
|
105
|
+
|
|
106
|
+
@router.get("/api/search/vector")
|
|
107
|
+
async def vector_search_get(
|
|
108
|
+
q: str,
|
|
109
|
+
request: Request,
|
|
110
|
+
limit: int = 30,
|
|
111
|
+
min_score: float = 0.0,
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
try:
|
|
114
|
+
return _guarded(request).vector_search(q, limit=limit, min_score=min_score)
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
_raise_graph_error(exc)
|
|
117
|
+
|
|
118
|
+
@router.get("/api/graph")
|
|
119
|
+
async def graph(request: Request, limit: int = 300) -> Dict[str, Any]:
|
|
120
|
+
try:
|
|
121
|
+
return _guarded(request).graph(limit=limit)
|
|
122
|
+
except ValueError as exc:
|
|
123
|
+
_raise_graph_error(exc)
|
|
124
|
+
|
|
125
|
+
@router.get("/api/graph/node")
|
|
126
|
+
async def graph_node(
|
|
127
|
+
node_id: str,
|
|
128
|
+
request: Request,
|
|
129
|
+
include_neighbors: bool = True,
|
|
130
|
+
depth: int = 1,
|
|
131
|
+
limit: int = 100,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
try:
|
|
134
|
+
return _guarded(request).node(
|
|
135
|
+
node_id,
|
|
136
|
+
include_neighbors=include_neighbors,
|
|
137
|
+
depth=depth,
|
|
138
|
+
limit=limit,
|
|
139
|
+
)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
142
|
+
|
|
143
|
+
@router.post("/api/graph/node")
|
|
144
|
+
async def graph_node_post(req: GraphNodeRequest, request: Request) -> Dict[str, Any]:
|
|
145
|
+
try:
|
|
146
|
+
return _guarded(request).node(
|
|
147
|
+
req.node_id,
|
|
148
|
+
include_neighbors=req.include_neighbors,
|
|
149
|
+
depth=req.depth,
|
|
150
|
+
limit=req.limit,
|
|
151
|
+
)
|
|
152
|
+
except ValueError as exc:
|
|
153
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
154
|
+
|
|
155
|
+
@router.get("/api/graph/relationship")
|
|
156
|
+
async def graph_relationship(
|
|
157
|
+
request: Request,
|
|
158
|
+
q: str = "",
|
|
159
|
+
node_id: str = "",
|
|
160
|
+
relationship_type: str = "",
|
|
161
|
+
limit: int = 30,
|
|
162
|
+
) -> Dict[str, Any]:
|
|
163
|
+
try:
|
|
164
|
+
return _guarded(request).relationships(
|
|
165
|
+
query=q,
|
|
166
|
+
node_id=node_id,
|
|
167
|
+
relationship_type=relationship_type,
|
|
168
|
+
limit=limit,
|
|
169
|
+
)
|
|
170
|
+
except ValueError as exc:
|
|
171
|
+
_raise_graph_error(exc)
|
|
172
|
+
|
|
173
|
+
@router.post("/api/graph/relationship")
|
|
174
|
+
async def graph_relationship_post(req: RelationshipSearchRequest, request: Request) -> Dict[str, Any]:
|
|
175
|
+
try:
|
|
176
|
+
return _guarded(request).relationships(
|
|
177
|
+
query=req.query,
|
|
178
|
+
node_id=req.node_id,
|
|
179
|
+
relationship_type=req.relationship_type,
|
|
180
|
+
limit=req.limit,
|
|
181
|
+
)
|
|
182
|
+
except ValueError as exc:
|
|
183
|
+
_raise_graph_error(exc)
|
|
184
|
+
|
|
185
|
+
@router.get("/api/index/status")
|
|
186
|
+
async def index_status(request: Request) -> Dict[str, Any]:
|
|
187
|
+
try:
|
|
188
|
+
return _guarded(request).index_status()
|
|
189
|
+
except ValueError as exc:
|
|
190
|
+
_raise_graph_error(exc)
|
|
191
|
+
|
|
192
|
+
@router.post("/api/index/rebuild")
|
|
193
|
+
async def index_rebuild(req: IndexRebuildRequest, request: Request) -> Dict[str, Any]:
|
|
194
|
+
try:
|
|
195
|
+
return _guarded(request).rebuild_index(
|
|
196
|
+
full=req.full,
|
|
197
|
+
include_nodes=req.include_nodes,
|
|
198
|
+
include_chunks=req.include_chunks,
|
|
199
|
+
)
|
|
200
|
+
except ValueError as exc:
|
|
201
|
+
_raise_graph_error(exc)
|
|
202
|
+
|
|
203
|
+
@router.get("/api/embeddings/status")
|
|
204
|
+
async def embeddings_status(request: Request, refresh: bool = False) -> Dict[str, Any]:
|
|
205
|
+
require_user(request)
|
|
206
|
+
resolved = embedding_info() if embedding_info else {}
|
|
207
|
+
try:
|
|
208
|
+
return service.embeddings_status(resolved=resolved, refresh=refresh)
|
|
209
|
+
except ValueError as exc:
|
|
210
|
+
_raise_graph_error(exc)
|
|
211
|
+
|
|
212
|
+
@router.get("/api/embeddings/providers")
|
|
213
|
+
async def embeddings_providers(request: Request) -> Dict[str, Any]:
|
|
214
|
+
require_user(request)
|
|
215
|
+
resolved = embedding_info() if embedding_info else {}
|
|
216
|
+
return {
|
|
217
|
+
"active": resolved.get("active_provider"),
|
|
218
|
+
"requested": resolved.get("requested_provider"),
|
|
219
|
+
"providers": [
|
|
220
|
+
{"id": "hash", "label": "Local hash (fallback)", "grade": "fallback",
|
|
221
|
+
"requires": [], "detail": "Deterministic offline vectors — always available."},
|
|
222
|
+
{"id": "mlx", "label": "MLX (Apple Silicon)", "grade": "production",
|
|
223
|
+
"requires": ["LATTICEAI_EMBEDDING_MODEL"], "detail": "Local embedding model via MLX."},
|
|
224
|
+
{"id": "ollama", "label": "Ollama", "grade": "production",
|
|
225
|
+
"requires": ["LATTICEAI_EMBEDDING_MODEL", "LATTICEAI_EMBEDDING_BASE_URL"],
|
|
226
|
+
"detail": "Local/remote Ollama embedding server."},
|
|
227
|
+
{"id": "openai", "label": "OpenAI-compatible", "grade": "production",
|
|
228
|
+
"requires": ["LATTICEAI_EMBEDDING_MODEL", "LATTICEAI_EMBEDDING_BASE_URL", "LATTICEAI_EMBEDDING_API_KEY"],
|
|
229
|
+
"detail": "Any /v1/embeddings endpoint (OpenAI, LM Studio, vLLM, …)."},
|
|
230
|
+
{"id": "custom", "label": "Custom callable", "grade": "production",
|
|
231
|
+
"requires": ["LATTICEAI_EMBEDDING_CUSTOM_TARGET"],
|
|
232
|
+
"detail": "User-supplied module:callable returning vectors."},
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return router
|
|
@@ -82,6 +82,16 @@ def create_static_routes_router(
|
|
|
82
82
|
if not p.exists():
|
|
83
83
|
raise HTTPException(status_code=404)
|
|
84
84
|
return FileResponse(str(p), media_type="application/manifest+json")
|
|
85
|
+
|
|
86
|
+
@api_router.api_route("/favicon.ico", methods=["GET", "HEAD"])
|
|
87
|
+
async def favicon():
|
|
88
|
+
ico = STATIC_DIR / "favicon.ico"
|
|
89
|
+
png = STATIC_DIR / "icons" / "favicon-32.png"
|
|
90
|
+
if ico.exists():
|
|
91
|
+
return FileResponse(str(ico), media_type="image/x-icon")
|
|
92
|
+
if png.exists():
|
|
93
|
+
return FileResponse(str(png), media_type="image/png")
|
|
94
|
+
raise HTTPException(status_code=404)
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
@api_router.get("/sw.js")
|
|
@@ -97,8 +107,17 @@ def create_static_routes_router(
|
|
|
97
107
|
@api_router.get("/chat")
|
|
98
108
|
async def chat_page(request: Request):
|
|
99
109
|
return ui_file_response(STATIC_DIR / "chat.html")
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@api_router.get("/app")
|
|
113
|
+
async def app_shell(request: Request):
|
|
114
|
+
"""v3 single-page workspace shell (token-native design system)."""
|
|
115
|
+
page = STATIC_DIR / "v3" / "index.html"
|
|
116
|
+
if not page.exists():
|
|
117
|
+
raise HTTPException(status_code=404, detail="v3 shell not found.")
|
|
118
|
+
return ui_file_response(page)
|
|
119
|
+
|
|
120
|
+
|
|
102
121
|
@api_router.get("/admin")
|
|
103
122
|
async def admin_page():
|
|
104
123
|
admin_path = STATIC_DIR / "admin.html"
|
package/latticeai/core/config.py
CHANGED
|
@@ -93,6 +93,15 @@ class Config:
|
|
|
93
93
|
local_draft_model: str
|
|
94
94
|
auto_read_chat_paths: bool
|
|
95
95
|
|
|
96
|
+
# ── embeddings (retrieval vector signal) ────────────────────────
|
|
97
|
+
embedding_provider: str
|
|
98
|
+
embedding_model: str
|
|
99
|
+
embedding_base_url: str
|
|
100
|
+
embedding_api_key: str
|
|
101
|
+
embedding_dim: int
|
|
102
|
+
embedding_timeout: int
|
|
103
|
+
embedding_custom_target: str
|
|
104
|
+
|
|
96
105
|
# ── SSO / OIDC ──────────────────────────────────────────────────
|
|
97
106
|
sso_discovery_url: str
|
|
98
107
|
sso_client_id: str
|
|
@@ -164,6 +173,13 @@ class Config:
|
|
|
164
173
|
local_model=local_model,
|
|
165
174
|
local_draft_model=_value(env, "LATTICEAI_LOCAL_DRAFT_MODEL", ""),
|
|
166
175
|
auto_read_chat_paths=_bool(env, "LATTICEAI_AUTO_READ_CHAT_PATHS", default=False),
|
|
176
|
+
embedding_provider=_value(env, "LATTICEAI_EMBEDDING_PROVIDER", "hash").strip().lower(),
|
|
177
|
+
embedding_model=_value(env, "LATTICEAI_EMBEDDING_MODEL", ""),
|
|
178
|
+
embedding_base_url=_value(env, "LATTICEAI_EMBEDDING_BASE_URL", ""),
|
|
179
|
+
embedding_api_key=_value(env, "LATTICEAI_EMBEDDING_API_KEY", ""),
|
|
180
|
+
embedding_dim=_int(env, "LATTICEAI_VECTOR_DIM", 0),
|
|
181
|
+
embedding_timeout=_int(env, "LATTICEAI_EMBEDDING_TIMEOUT", 30),
|
|
182
|
+
embedding_custom_target=_value(env, "LATTICEAI_EMBEDDING_CUSTOM_TARGET", ""),
|
|
167
183
|
sso_discovery_url=_value(env, "OIDC_DISCOVERY_URL", ""),
|
|
168
184
|
sso_client_id=_value(env, "OIDC_CLIENT_ID", ""),
|
|
169
185
|
sso_client_secret=_value(env, "OIDC_CLIENT_SECRET", ""),
|