ltcai 2.2.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -34
- package/docs/CHANGELOG.md +119 -0
- package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
- package/docs/V3_FRONTEND.md +139 -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 +5 -2
- package/latticeai/api/chat.py +10 -2
- package/latticeai/api/search.py +240 -0
- package/latticeai/api/static_routes.py +11 -2
- package/latticeai/core/config.py +18 -0
- package/latticeai/core/embedding_providers.py +625 -0
- package/latticeai/core/local_embeddings.py +86 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +65 -1
- package/latticeai/services/agent_runtime.py +245 -0
- package/latticeai/services/search_service.py +346 -0
- package/package.json +13 -6
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +10 -10
- package/static/css/reference/account.css +137 -1
- package/static/css/reference/chat.css +31 -37
- package/static/css/responsive.css +42 -0
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +125 -130
- package/static/graph.html +9 -9
- package/static/manifest.json +3 -3
- package/static/plugins.html +4 -4
- package/static/scripts/account.js +4 -4
- package/static/scripts/chat.js +40 -8
- package/static/scripts/workspace.js +78 -0
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +47 -0
- package/static/v3/css/lattice.base.css +128 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +447 -0
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.shell.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.tokens.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/css/lattice.views.css +277 -0
- package/static/v3/index.html +69 -0
- package/static/v3/js/app.46fb61d9.js +26 -0
- package/static/v3/js/app.js +26 -0
- package/static/v3/js/core/api.22a41d42.js +344 -0
- package/static/v3/js/core/api.js +344 -0
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +222 -0
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/dom.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/router.js +37 -0
- package/static/v3/js/core/routes.f935dd50.js +78 -0
- package/static/v3/js/core/routes.js +78 -0
- package/static/v3/js/core/shell.1b6199d6.js +363 -0
- package/static/v3/js/core/shell.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/core/store.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +185 -0
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +177 -0
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +102 -0
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +135 -0
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +180 -0
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +168 -0
- package/static/v3/js/views/agents.14e48bdd.js +193 -0
- package/static/v3/js/views/agents.js +193 -0
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +449 -0
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +186 -0
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/home.js +119 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +195 -0
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +237 -0
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +256 -0
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +237 -0
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +157 -0
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +250 -0
- package/static/workflows.html +4 -4
- package/static/workspace.css +340 -2
- package/static/workspace.html +43 -24
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Entry point
|
|
3
|
+
* Boots the shell. Views are lazy-loaded by the router (see core/routes.js).
|
|
4
|
+
* ========================================================================== */
|
|
5
|
+
|
|
6
|
+
import { boot } from "./core/shell.js";
|
|
7
|
+
|
|
8
|
+
const root = document.getElementById("app");
|
|
9
|
+
if (root) boot(root);
|
|
10
|
+
|
|
11
|
+
// CDN fonts/icons are progressive enhancement. If Tabler's webfont is blocked
|
|
12
|
+
// or offline, compact text fallbacks keep icon-only controls identifiable.
|
|
13
|
+
if (document.fonts && document.fonts.ready) {
|
|
14
|
+
document.fonts.ready.then(() => {
|
|
15
|
+
if (!document.fonts.check('16px "tabler-icons"')) {
|
|
16
|
+
document.documentElement.dataset.ltIcons = "fallback";
|
|
17
|
+
}
|
|
18
|
+
}).catch(() => { document.documentElement.dataset.ltIcons = "fallback"; });
|
|
19
|
+
} else {
|
|
20
|
+
document.documentElement.dataset.ltIcons = "fallback";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Best-effort PWA hook (silent if unsupported / not served).
|
|
24
|
+
if ("serviceWorker" in navigator) {
|
|
25
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
26
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Integration adapter
|
|
3
|
+
*
|
|
4
|
+
* Every adapter call hits the real endpoint first (including /api/index/status,
|
|
5
|
+
* /api/graph, /api/search/hybrid, and /chat). If that endpoint is
|
|
6
|
+
* missing/unavailable, it returns an unavailable source with empty data so the
|
|
7
|
+
* UI can render a clear unavailable state without inventing counters or health.
|
|
8
|
+
*
|
|
9
|
+
* Return shape (never throws): { ok, status, data, source, error }
|
|
10
|
+
* source: "live" → returned by a real backend endpoint
|
|
11
|
+
* "unavailable" → endpoint missing/down; no fake payload
|
|
12
|
+
* ========================================================================== */
|
|
13
|
+
|
|
14
|
+
import { store } from "./store.34ebd5e6.js";
|
|
15
|
+
|
|
16
|
+
const TIMEOUT_MS = 8000;
|
|
17
|
+
const EMPTY_INDEX_STATUS = { generated_at: null, pipelines: {}, sources: [] };
|
|
18
|
+
const EMPTY_GRAPH_STATS = { nodes: {}, edges: {}, total_nodes: 0, total_edges: 0 };
|
|
19
|
+
const EMPTY_WORKSPACE_OS = { counts: {}, models: {} };
|
|
20
|
+
const EMPTY_SYSINFO = { cpu_pct: null, ram_pct: null, gpu_mem_pct: null, gpu_mem_gb: null };
|
|
21
|
+
const EMPTY_ADMIN = {
|
|
22
|
+
summary: { total_users: null, active_users: null, admin_users: null, total_messages: null },
|
|
23
|
+
users: [],
|
|
24
|
+
audit: { recent_events: [] },
|
|
25
|
+
security: {},
|
|
26
|
+
roles: { roles: [] },
|
|
27
|
+
policies: { policies: [] },
|
|
28
|
+
vpc: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function raw(path, { method = "GET", body, headers } = {}) {
|
|
32
|
+
const ctrl = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
34
|
+
try {
|
|
35
|
+
const ws = store.get().workspaceId;
|
|
36
|
+
const res = await fetch(path, {
|
|
37
|
+
method,
|
|
38
|
+
credentials: "same-origin",
|
|
39
|
+
signal: ctrl.signal,
|
|
40
|
+
headers: {
|
|
41
|
+
"Accept": "application/json",
|
|
42
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
43
|
+
...(ws ? { "X-Workspace-Id": ws } : {}),
|
|
44
|
+
...headers,
|
|
45
|
+
},
|
|
46
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
47
|
+
});
|
|
48
|
+
let data = null;
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
51
|
+
return { ok: res.ok, status: res.status, data };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, status: 0, data: null, error: err && err.name === "AbortError" ? "timeout" : String(err) };
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unavailableData(shape) {
|
|
60
|
+
const value = typeof shape === "function" ? shape() : shape;
|
|
61
|
+
if (Array.isArray(value)) return [];
|
|
62
|
+
if (value && typeof value === "object") return {};
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Try the live endpoint; on any non-2xx/transport failure, return empty data. */
|
|
67
|
+
async function withFallback(path, opts, shape) {
|
|
68
|
+
const res = await raw(path, opts);
|
|
69
|
+
if (res.ok && res.data && !res.data.raw) {
|
|
70
|
+
return { ...res, source: "live" };
|
|
71
|
+
}
|
|
72
|
+
return { ok: false, status: res.status, data: unavailableData(shape), source: "unavailable", error: res.error };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const api = {
|
|
76
|
+
raw,
|
|
77
|
+
|
|
78
|
+
/** Generic GET with unavailable fallback. */
|
|
79
|
+
async get(path, shape = null) {
|
|
80
|
+
return withFallback(path, {}, shape);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/* ── Documented future surfaces ─────────────────────────────────────── */
|
|
84
|
+
|
|
85
|
+
/** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
|
|
86
|
+
indexStatus() {
|
|
87
|
+
return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** POST /api/index/rebuild — rebuild the derived vector index (real run). */
|
|
91
|
+
rebuildIndex(opts = {}) {
|
|
92
|
+
return raw("/api/index/rebuild", { method: "POST", body: { full: false, include_nodes: true, include_chunks: true, ...opts } });
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** GET /api/graph — knowledge graph (nodes + edges). Falls back through the
|
|
96
|
+
* current /knowledge-graph/graph route before reporting unavailable. */
|
|
97
|
+
async graph(params = {}) {
|
|
98
|
+
const qs = new URLSearchParams(params).toString();
|
|
99
|
+
const primary = await raw(`/api/graph${qs ? "?" + qs : ""}`);
|
|
100
|
+
if (primary.ok && primary.data && Array.isArray(primary.data.nodes)) {
|
|
101
|
+
return { ...primary, source: "live" };
|
|
102
|
+
}
|
|
103
|
+
const legacy = await raw("/knowledge-graph/graph");
|
|
104
|
+
if (legacy.ok && legacy.data && Array.isArray(legacy.data.nodes)) {
|
|
105
|
+
return { ...legacy, source: "live" };
|
|
106
|
+
}
|
|
107
|
+
return { ok: false, status: primary.status || legacy.status || 0, data: { nodes: [], edges: [] }, source: "unavailable", error: primary.error || legacy.error };
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
graphStats() {
|
|
111
|
+
return withFallback("/knowledge-graph/stats", {}, EMPTY_GRAPH_STATS);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** POST /api/search/hybrid — fused KG + vector retrieval.
|
|
115
|
+
* The backend returns `{ matches: [...] }` where each match carries
|
|
116
|
+
* `source_scores: { keyword, vector, graph }`. Normalize that into the flat
|
|
117
|
+
* result shape the view renders (title/path/snippet/score + per-signal). A
|
|
118
|
+
* legacy `results` array is also accepted defensively. */
|
|
119
|
+
async hybridSearch(query, opts = {}) {
|
|
120
|
+
const res = await raw("/api/search/hybrid", { method: "POST", body: { query, ...opts } });
|
|
121
|
+
const live = res.ok && res.data
|
|
122
|
+
? (Array.isArray(res.data.matches) ? res.data.matches
|
|
123
|
+
: Array.isArray(res.data.results) ? res.data.results
|
|
124
|
+
: null)
|
|
125
|
+
: null;
|
|
126
|
+
if (live) {
|
|
127
|
+
const items = live.map((m) => {
|
|
128
|
+
const ss = m.source_scores || {};
|
|
129
|
+
const meta = m.metadata || {};
|
|
130
|
+
return {
|
|
131
|
+
id: m.id || m.node_id,
|
|
132
|
+
title: m.title || m.id || "Untitled",
|
|
133
|
+
path: meta.path || meta.source || m.path || m.type || "",
|
|
134
|
+
snippet: m.snippet || m.summary || "",
|
|
135
|
+
score: typeof m.score === "number" ? m.score : 0,
|
|
136
|
+
vector: Number(ss.vector ?? m.vector) || 0,
|
|
137
|
+
lexical: Number(ss.keyword ?? m.lexical) || 0,
|
|
138
|
+
graph: Number(ss.graph ?? m.graph) || 0,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return { ok: true, status: res.status, data: items, source: "live", weights: res.data.weights || null };
|
|
142
|
+
}
|
|
143
|
+
return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/* ── Existing surfaces (used where helpful, all fallback-safe) ──────── */
|
|
147
|
+
workspaceOs() { return withFallback("/workspace/os", {}, EMPTY_WORKSPACE_OS); },
|
|
148
|
+
async models() {
|
|
149
|
+
const res = await raw("/models");
|
|
150
|
+
if (res.ok && res.data && !res.data.raw) {
|
|
151
|
+
const data = res.data;
|
|
152
|
+
const loadedIds = Array.isArray(data.loaded) ? data.loaded : [];
|
|
153
|
+
const recommended = Array.isArray(data.recommended) ? data.recommended.map((m) => ({
|
|
154
|
+
...m,
|
|
155
|
+
name: m.name || m.display_name || m.id,
|
|
156
|
+
family: m.family || m.modality || "local",
|
|
157
|
+
state: loadedIds.includes(m.id) || data.current === m.id ? "loaded" : "available",
|
|
158
|
+
})) : [];
|
|
159
|
+
const loadedOnly = loadedIds
|
|
160
|
+
.filter((id) => !recommended.some((m) => m.id === id))
|
|
161
|
+
.map((id) => ({ id, name: id, family: "local", state: data.current === id ? "loaded" : "available" }));
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
status: res.status,
|
|
165
|
+
source: "live",
|
|
166
|
+
data: { ...data, catalog: Array.isArray(data.catalog) ? data.catalog : [...recommended, ...loadedOnly] },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { ok: false, status: res.status, data: { current: null, catalog: [] }, source: "unavailable", error: res.error };
|
|
170
|
+
},
|
|
171
|
+
loadModel(modelId, engine) {
|
|
172
|
+
return raw("/models/load", { method: "POST", body: { model_id: modelId, engine: engine || null } });
|
|
173
|
+
},
|
|
174
|
+
unloadModel(modelId) {
|
|
175
|
+
return raw(`/models/unload/${encodeURIComponent(modelId)}`, { method: "DELETE" });
|
|
176
|
+
},
|
|
177
|
+
sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
|
|
178
|
+
|
|
179
|
+
adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
|
|
180
|
+
adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
|
|
181
|
+
adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
|
|
182
|
+
adminSecurity() { return withFallback("/admin/security/overview", {}, EMPTY_ADMIN.security); },
|
|
183
|
+
adminRoles() { return withFallback("/admin/roles", {}, EMPTY_ADMIN.roles); },
|
|
184
|
+
adminPolicies() { return withFallback("/admin/policies", {}, EMPTY_ADMIN.policies); },
|
|
185
|
+
vpcStatus() { return withFallback("/vpc/status", {}, EMPTY_ADMIN.vpc); },
|
|
186
|
+
|
|
187
|
+
/* ── Embeddings (real backend: /api/embeddings/*) ───────────────────── */
|
|
188
|
+
/** GET /api/embeddings/status — active provider, grade, dimensions, last index. */
|
|
189
|
+
async embeddingsStatus() {
|
|
190
|
+
const res = await raw("/api/embeddings/status");
|
|
191
|
+
if (res.ok && res.data && res.data.provider) {
|
|
192
|
+
return { ok: true, status: res.status, data: res.data, source: "live" };
|
|
193
|
+
}
|
|
194
|
+
// No backend → report unavailable honestly (never fabricate a provider).
|
|
195
|
+
return {
|
|
196
|
+
ok: false, status: res.status, source: "unavailable",
|
|
197
|
+
data: { provider: null, active_provider: null, model: null,
|
|
198
|
+
model_id: null, dimensions: null, grade: "unavailable",
|
|
199
|
+
state: "unavailable", fell_back: false, health: { status: "unavailable", detail: "backend unavailable" },
|
|
200
|
+
last_indexed_at: null },
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
embeddingsProviders() { return withFallback("/api/embeddings/providers", {}, { active: "hash", providers: [] }); },
|
|
204
|
+
|
|
205
|
+
/* ── Agents (real backend: AgentRuntime /agents/api/runtime/*) ───────── */
|
|
206
|
+
/** GET /agents/api/runtime/status — roles, roster, runs, health from the runtime. */
|
|
207
|
+
async agentRuntime() {
|
|
208
|
+
const res = await raw("/agents/api/runtime/status");
|
|
209
|
+
if (res.ok && res.data && res.data.runtime && Array.isArray(res.data.agents)) {
|
|
210
|
+
return { ok: true, status: res.status, data: res.data, source: "live" };
|
|
211
|
+
}
|
|
212
|
+
// Fallback: unavailable roster, no fabricated run ledger.
|
|
213
|
+
return {
|
|
214
|
+
ok: false, status: res.status, source: "unavailable",
|
|
215
|
+
data: { runtime: { ready: false, total_runs: 0, active_runs: 0 },
|
|
216
|
+
health: { status: "unknown", checks: {} }, roles: [],
|
|
217
|
+
agents: [], runs: [] },
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
/** POST /agents/api/run — execute the multi-agent pipeline for a goal. */
|
|
221
|
+
runAgent(goal, roles) { return raw("/agents/api/run", { method: "POST", body: { goal, roles: roles || [] } }); },
|
|
222
|
+
|
|
223
|
+
/* ── Local computer memory (real backend: /workspace/computer-memory) ── */
|
|
224
|
+
computerMemory() { return raw("/workspace/computer-memory"); },
|
|
225
|
+
setComputerMemory(enabled) {
|
|
226
|
+
return raw("/workspace/computer-memory", { method: "POST", body: { enabled, consent: { approved: !!enabled } } });
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/* ── Organization workspaces (real backend: /workspace/orgs) ────────── */
|
|
230
|
+
createOrg(name) { return raw("/workspace/orgs", { method: "POST", body: { name } }); },
|
|
231
|
+
|
|
232
|
+
/* ── Chat (real backend: SSE /chat + /history/*) ────────────────────── */
|
|
233
|
+
|
|
234
|
+
/** GET /history/conversations — conversation list. */
|
|
235
|
+
async chatHistory() {
|
|
236
|
+
const res = await raw("/history/conversations");
|
|
237
|
+
const list = res.ok && Array.isArray(res.data) ? res.data
|
|
238
|
+
: res.ok && res.data && Array.isArray(res.data.conversations) ? res.data.conversations
|
|
239
|
+
: null;
|
|
240
|
+
if (list) return { ok: true, status: res.status, data: list, source: "live" };
|
|
241
|
+
return { ok: false, status: res.status, data: [], source: "unavailable" };
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/** GET /history/conversations/{id} — messages for one conversation. */
|
|
245
|
+
async conversation(id) {
|
|
246
|
+
const res = await raw(`/history/conversations/${encodeURIComponent(id)}`);
|
|
247
|
+
if (res.ok && res.data && Array.isArray(res.data.messages)) {
|
|
248
|
+
return { ok: true, status: res.status, data: res.data.messages, source: "live" };
|
|
249
|
+
}
|
|
250
|
+
return { ok: false, status: res.status, data: [], source: "unavailable" };
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
deleteConversation(id) {
|
|
254
|
+
return raw(`/history/conversations/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* POST /chat — streams the assistant reply over SSE.
|
|
259
|
+
* Parses `data: {chunk, model, trace}` events (terminator `[DONE]`), calling
|
|
260
|
+
* onChunk(delta, fullText) and onTrace(trace). If the endpoint is missing or
|
|
261
|
+
* not an event-stream, reports that chat is unavailable (no generated answer is
|
|
262
|
+
* invented). Resolves { source, text, trace, model, aborted }.
|
|
263
|
+
*/
|
|
264
|
+
async streamChat(body, { onChunk, onTrace, signal } = {}) {
|
|
265
|
+
const ws = store.get().workspaceId;
|
|
266
|
+
let res;
|
|
267
|
+
try {
|
|
268
|
+
res = await fetch("/chat", {
|
|
269
|
+
method: "POST",
|
|
270
|
+
credentials: "same-origin",
|
|
271
|
+
signal,
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": "application/json",
|
|
274
|
+
"Accept": "text/event-stream",
|
|
275
|
+
...(ws ? { "X-Workspace-Id": ws } : {}),
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify({ stream: true, max_tokens: 2048, temperature: 0.2, ...body }),
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (err && err.name === "AbortError") return { source: "live", text: "", aborted: true };
|
|
281
|
+
return simulateChat(body, { onChunk, onTrace, signal });
|
|
282
|
+
}
|
|
283
|
+
const ctype = res.headers.get("content-type") || "";
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
let data = null;
|
|
286
|
+
try { data = await res.clone().json(); } catch {}
|
|
287
|
+
const detail = data && (data.detail || data.message || data.error);
|
|
288
|
+
const noModel = data && (data.error === "no_model_loaded" || /no .*model .*loaded/i.test(String(detail || "")));
|
|
289
|
+
if (noModel) {
|
|
290
|
+
return { source: "live", text: "", error: "no_model_loaded", errorMessage: String(detail || "No local model is loaded.") };
|
|
291
|
+
}
|
|
292
|
+
return simulateChat(body, { onChunk, onTrace, signal });
|
|
293
|
+
}
|
|
294
|
+
if (!res.body || !ctype.includes("text/event-stream")) {
|
|
295
|
+
return simulateChat(body, { onChunk, onTrace, signal });
|
|
296
|
+
}
|
|
297
|
+
const reader = res.body.getReader();
|
|
298
|
+
const decoder = new TextDecoder();
|
|
299
|
+
let buf = "", text = "", trace = null, model = null;
|
|
300
|
+
try {
|
|
301
|
+
for (;;) {
|
|
302
|
+
const { value, done } = await reader.read();
|
|
303
|
+
if (done) break;
|
|
304
|
+
buf += decoder.decode(value, { stream: true });
|
|
305
|
+
const parts = buf.split("\n\n");
|
|
306
|
+
buf = parts.pop();
|
|
307
|
+
for (const part of parts) {
|
|
308
|
+
const line = part.split("\n").find((l) => l.startsWith("data:"));
|
|
309
|
+
if (!line) continue;
|
|
310
|
+
const rawData = line.slice(5).trim();
|
|
311
|
+
if (rawData === "[DONE]") return { source: "live", text, trace, model };
|
|
312
|
+
let data; try { data = JSON.parse(rawData); } catch { continue; }
|
|
313
|
+
if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
|
|
314
|
+
if (data.model) model = data.model;
|
|
315
|
+
if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err && err.name === "AbortError") return { source: "live", text, trace, model, aborted: true };
|
|
320
|
+
if (!text) return simulateChat(body, { onChunk, onTrace, signal });
|
|
321
|
+
}
|
|
322
|
+
return { source: "live", text, trace, model };
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
327
|
+
|
|
328
|
+
/** Transparent unavailable stream — used only when no chat backend is available. */
|
|
329
|
+
async function simulateChat(body, { onChunk, onTrace, signal } = {}) {
|
|
330
|
+
const q = (body && body.message) || "your question";
|
|
331
|
+
const reply =
|
|
332
|
+
`Chat is unavailable because the Lattice backend or active model is not reachable. ` +
|
|
333
|
+
`Start the server, load a model, and rebuild retrieval before sending “${q}”.`;
|
|
334
|
+
let text = "";
|
|
335
|
+
for (const word of reply.split(" ")) {
|
|
336
|
+
if (signal && signal.aborted) return { source: "unavailable", text, aborted: true };
|
|
337
|
+
const delta = (text ? " " : "") + word;
|
|
338
|
+
text += delta;
|
|
339
|
+
onChunk && onChunk(delta, text);
|
|
340
|
+
await sleep(16);
|
|
341
|
+
}
|
|
342
|
+
onTrace && onTrace(null);
|
|
343
|
+
return { source: "unavailable", text, trace: null };
|
|
344
|
+
}
|