ltcai 4.0.1 → 4.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.
Files changed (192) hide show
  1. package/README.md +33 -24
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +84 -0
  4. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  5. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  6. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  7. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
  8. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
  9. package/docs/V4_2_VALIDATION_REPORT.md +89 -0
  10. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -26
  11. package/frontend/index.html +24 -0
  12. package/frontend/openapi.json +14436 -0
  13. package/frontend/src/App.tsx +184 -0
  14. package/frontend/src/api/client.ts +320 -0
  15. package/frontend/src/api/openapi.ts +16921 -0
  16. package/frontend/src/components/primitives.tsx +204 -0
  17. package/frontend/src/components/ui/badge.tsx +27 -0
  18. package/frontend/src/components/ui/button.tsx +37 -0
  19. package/frontend/src/components/ui/card.tsx +22 -0
  20. package/frontend/src/components/ui/input.tsx +16 -0
  21. package/frontend/src/components/ui/textarea.tsx +16 -0
  22. package/frontend/src/lib/utils.ts +33 -0
  23. package/frontend/src/main.tsx +23 -0
  24. package/frontend/src/pages/Act.tsx +245 -0
  25. package/frontend/src/pages/Ask.tsx +200 -0
  26. package/frontend/src/pages/Brain.tsx +267 -0
  27. package/frontend/src/pages/Capture.tsx +158 -0
  28. package/frontend/src/pages/Library.tsx +187 -0
  29. package/frontend/src/pages/System.tsx +378 -0
  30. package/frontend/src/routes.ts +85 -0
  31. package/frontend/src/store/appStore.ts +54 -0
  32. package/frontend/src/styles.css +107 -0
  33. package/kg_schema.py +1 -1
  34. package/knowledge_graph.py +4 -4
  35. package/lattice_brain/__init__.py +70 -0
  36. package/lattice_brain/_kg_common.py +1 -0
  37. package/lattice_brain/archive.py +133 -0
  38. package/lattice_brain/context.py +3 -0
  39. package/lattice_brain/conversations.py +3 -0
  40. package/lattice_brain/core.py +82 -0
  41. package/lattice_brain/discovery.py +1 -0
  42. package/lattice_brain/documents.py +1 -0
  43. package/lattice_brain/embeddings.py +82 -0
  44. package/lattice_brain/identity.py +13 -0
  45. package/lattice_brain/ingest.py +1 -0
  46. package/lattice_brain/memory.py +3 -0
  47. package/lattice_brain/network.py +1 -0
  48. package/lattice_brain/projection.py +1 -0
  49. package/lattice_brain/provenance.py +1 -0
  50. package/lattice_brain/retrieval.py +1 -0
  51. package/lattice_brain/schema.py +1 -0
  52. package/lattice_brain/storage/__init__.py +22 -0
  53. package/lattice_brain/storage/base.py +72 -0
  54. package/lattice_brain/storage/docker.py +105 -0
  55. package/lattice_brain/storage/factory.py +31 -0
  56. package/lattice_brain/storage/migration.py +190 -0
  57. package/lattice_brain/storage/postgres.py +123 -0
  58. package/lattice_brain/storage/sqlite.py +128 -0
  59. package/lattice_brain/store.py +3 -0
  60. package/lattice_brain/write_master.py +1 -0
  61. package/latticeai/__init__.py +1 -1
  62. package/latticeai/api/portability.py +69 -0
  63. package/latticeai/api/setup.py +5 -4
  64. package/latticeai/api/static_routes.py +4 -4
  65. package/latticeai/app_factory.py +17 -10
  66. package/latticeai/brain/__init__.py +6 -6
  67. package/latticeai/brain/_kg_common.py +1 -1
  68. package/latticeai/brain/network.py +1 -1
  69. package/latticeai/brain/retrieval.py +15 -0
  70. package/latticeai/brain/store.py +22 -6
  71. package/latticeai/core/config.py +8 -0
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +1 -1
  74. package/latticeai/core/workspace_os.py +1 -1
  75. package/latticeai/services/kg_portability.py +82 -1
  76. package/package.json +55 -15
  77. package/scripts/build_frontend_assets.mjs +38 -0
  78. package/scripts/bump_version.py +4 -1
  79. package/scripts/export_openapi.py +31 -0
  80. package/scripts/lint_frontend.mjs +91 -0
  81. package/scripts/migrate_brain_storage.py +53 -0
  82. package/scripts/run_python.mjs +47 -0
  83. package/scripts/wheel_smoke.py +3 -0
  84. package/src-tauri/Cargo.lock +4833 -0
  85. package/src-tauri/Cargo.toml +19 -0
  86. package/src-tauri/build.rs +3 -0
  87. package/src-tauri/capabilities/default.json +7 -0
  88. package/src-tauri/src/main.rs +78 -0
  89. package/src-tauri/tauri.conf.json +39 -0
  90. package/static/app/asset-manifest.json +32 -0
  91. package/static/app/assets/core-CwxXejkd.js +2 -0
  92. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  93. package/static/app/assets/index-CDjiH_se.css +2 -0
  94. package/static/app/assets/index-C_HAkbAg.js +333 -0
  95. package/static/app/assets/index-C_HAkbAg.js.map +1 -0
  96. package/static/app/index.html +25 -0
  97. package/static/manifest.json +2 -2
  98. package/static/sw.js +4 -4
  99. package/scripts/build_v3_assets.mjs +0 -170
  100. package/scripts/lint_v3.mjs +0 -120
  101. package/static/v3/asset-manifest.json +0 -63
  102. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  103. package/static/v3/css/lattice.base.css +0 -128
  104. package/static/v3/css/lattice.components.cde18231.css +0 -472
  105. package/static/v3/css/lattice.components.css +0 -472
  106. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  107. package/static/v3/css/lattice.shell.css +0 -452
  108. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  109. package/static/v3/css/lattice.tokens.css +0 -135
  110. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  111. package/static/v3/css/lattice.views.css +0 -360
  112. package/static/v3/index.html +0 -68
  113. package/static/v3/js/app.c5c80c46.js +0 -26
  114. package/static/v3/js/app.js +0 -26
  115. package/static/v3/js/core/api.ba0fbf14.js +0 -625
  116. package/static/v3/js/core/api.js +0 -625
  117. package/static/v3/js/core/components.f25b3b93.js +0 -230
  118. package/static/v3/js/core/components.js +0 -230
  119. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  120. package/static/v3/js/core/dom.js +0 -148
  121. package/static/v3/js/core/i18n.880e1fec.js +0 -575
  122. package/static/v3/js/core/i18n.js +0 -575
  123. package/static/v3/js/core/router.584570f2.js +0 -37
  124. package/static/v3/js/core/router.js +0 -37
  125. package/static/v3/js/core/routes.37522821.js +0 -101
  126. package/static/v3/js/core/routes.js +0 -101
  127. package/static/v3/js/core/shell.e3f6bbfa.js +0 -420
  128. package/static/v3/js/core/shell.js +0 -420
  129. package/static/v3/js/core/store.7b2aa044.js +0 -123
  130. package/static/v3/js/core/store.js +0 -123
  131. package/static/v3/js/views/account.eff40715.js +0 -143
  132. package/static/v3/js/views/account.js +0 -143
  133. package/static/v3/js/views/activity.0d271ef9.js +0 -67
  134. package/static/v3/js/views/activity.js +0 -67
  135. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  136. package/static/v3/js/views/admin-audit.js +0 -185
  137. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  138. package/static/v3/js/views/admin-permissions.js +0 -177
  139. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  140. package/static/v3/js/views/admin-policies.js +0 -102
  141. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  142. package/static/v3/js/views/admin-private-vpc.js +0 -135
  143. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  144. package/static/v3/js/views/admin-security.js +0 -180
  145. package/static/v3/js/views/admin-users.f7ac7b43.js +0 -166
  146. package/static/v3/js/views/admin-users.js +0 -166
  147. package/static/v3/js/views/agents.17c5288d.js +0 -564
  148. package/static/v3/js/views/agents.js +0 -564
  149. package/static/v3/js/views/chat.e250e2cc.js +0 -624
  150. package/static/v3/js/views/chat.js +0 -624
  151. package/static/v3/js/views/files.adad14c1.js +0 -365
  152. package/static/v3/js/views/files.js +0 -365
  153. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  154. package/static/v3/js/views/graph-canvas.js +0 -509
  155. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  156. package/static/v3/js/views/home.js +0 -200
  157. package/static/v3/js/views/hooks.37895880.js +0 -220
  158. package/static/v3/js/views/hooks.js +0 -220
  159. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  160. package/static/v3/js/views/hybrid-search.js +0 -194
  161. package/static/v3/js/views/knowledge-graph.4d09c537.js +0 -529
  162. package/static/v3/js/views/knowledge-graph.js +0 -529
  163. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  164. package/static/v3/js/views/marketplace.js +0 -141
  165. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  166. package/static/v3/js/views/mcp.js +0 -114
  167. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  168. package/static/v3/js/views/memory.js +0 -147
  169. package/static/v3/js/views/models.a1ffa147.js +0 -256
  170. package/static/v3/js/views/models.js +0 -256
  171. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  172. package/static/v3/js/views/my-computer.js +0 -463
  173. package/static/v3/js/views/network.52a4f181.js +0 -97
  174. package/static/v3/js/views/network.js +0 -97
  175. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  176. package/static/v3/js/views/pipeline.js +0 -157
  177. package/static/v3/js/views/planning.4876fd77.js +0 -174
  178. package/static/v3/js/views/planning.js +0 -174
  179. package/static/v3/js/views/runs.b63b2afa.js +0 -144
  180. package/static/v3/js/views/runs.js +0 -144
  181. package/static/v3/js/views/settings.b7140634.js +0 -317
  182. package/static/v3/js/views/settings.js +0 -317
  183. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  184. package/static/v3/js/views/skills.js +0 -109
  185. package/static/v3/js/views/snapshots.6f5db095.js +0 -135
  186. package/static/v3/js/views/snapshots.js +0 -135
  187. package/static/v3/js/views/tools.e4f11276.js +0 -108
  188. package/static/v3/js/views/tools.js +0 -108
  189. package/static/v3/js/views/workflows.7752225a.js +0 -213
  190. package/static/v3/js/views/workflows.js +0 -213
  191. package/static/v3/js/views/workspace-admin.c466029b.js +0 -156
  192. package/static/v3/js/views/workspace-admin.js +0 -156
@@ -1,625 +0,0 @@
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.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
- * The backend endpoint is vector-centric (status/storage/source_items/…); the
87
- * home pillars + topbar chip want a `pipelines` view keyed by
88
- * knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
89
- * index status (vectors) plus the KG stats endpoint (entities). Nothing is
90
- * fabricated: if the index endpoint is unavailable we report unavailable (so
91
- * the UI shows the honest empty state), and a missing graph-stats count yields
92
- * an "unavailable" graph pillar rather than a fake number. */
93
- async indexStatus() {
94
- const res = await raw("/api/index/status");
95
- if (!(res.ok && res.data && !res.data.raw)) {
96
- return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
97
- }
98
- const idx = res.data;
99
- let entities = null;
100
- const gs = await raw("/knowledge-graph/stats");
101
- if (gs.ok && gs.data && !gs.data.raw) {
102
- const g = gs.data;
103
- const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
104
- if (n !== undefined && n !== null) entities = Number(n) || 0;
105
- }
106
- const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
107
- const vstate = idx.status === "ready" ? "ready" : "pending";
108
- const pipelines = {
109
- knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
110
- vector_index: { state: vstate, vectors },
111
- hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
112
- };
113
- return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
114
- },
115
-
116
- /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
117
- rebuildIndex(opts = {}) {
118
- return raw("/api/index/rebuild", { method: "POST", body: { full: false, include_nodes: true, include_chunks: true, ...opts } });
119
- },
120
-
121
- /** GET /api/graph — knowledge graph (nodes + edges). Falls back through the
122
- * current /knowledge-graph/graph route before reporting unavailable. */
123
- async graph(params = {}) {
124
- const qs = new URLSearchParams(params).toString();
125
- const primary = await raw(`/api/graph${qs ? "?" + qs : ""}`);
126
- if (primary.ok && primary.data && Array.isArray(primary.data.nodes)) {
127
- return { ...primary, source: "live" };
128
- }
129
- const legacy = await raw("/knowledge-graph/graph");
130
- if (legacy.ok && legacy.data && Array.isArray(legacy.data.nodes)) {
131
- return { ...legacy, source: "live" };
132
- }
133
- return { ok: false, status: primary.status || legacy.status || 0, data: { nodes: [], edges: [] }, source: "unavailable", error: primary.error || legacy.error };
134
- },
135
-
136
- graphStats() {
137
- return withFallback("/knowledge-graph/stats", {}, EMPTY_GRAPH_STATS);
138
- },
139
-
140
- /** POST /api/search/hybrid — fused KG + vector retrieval.
141
- * The backend returns `{ matches: [...] }` where each match carries
142
- * `source_scores: { keyword, vector, graph }`. Normalize that into the flat
143
- * result shape the view renders (title/path/snippet/score + per-signal). A
144
- * legacy `results` array is also accepted defensively. */
145
- async hybridSearch(query, opts = {}) {
146
- const res = await raw("/api/search/hybrid", { method: "POST", body: { query, ...opts } });
147
- const live = res.ok && res.data
148
- ? (Array.isArray(res.data.matches) ? res.data.matches
149
- : Array.isArray(res.data.results) ? res.data.results
150
- : null)
151
- : null;
152
- if (live) {
153
- const items = live.map((m) => {
154
- const ss = m.source_scores || {};
155
- const meta = m.metadata || {};
156
- return {
157
- id: m.id || m.node_id,
158
- title: m.title || m.id || "Untitled",
159
- path: meta.path || meta.source || m.path || m.type || "",
160
- snippet: m.snippet || m.summary || "",
161
- score: typeof m.score === "number" ? m.score : 0,
162
- vector: Number(ss.vector ?? m.vector) || 0,
163
- lexical: Number(ss.keyword ?? m.lexical) || 0,
164
- graph: Number(ss.graph ?? m.graph) || 0,
165
- };
166
- });
167
- return { ok: true, status: res.status, data: items, source: "live", weights: res.data.weights || null };
168
- }
169
- return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
170
- },
171
-
172
- /* ── Existing surfaces (used where helpful, all fallback-safe) ──────── */
173
- workspaceOs() { return withFallback("/workspace/os", {}, EMPTY_WORKSPACE_OS); },
174
- async models() {
175
- const res = await raw("/models");
176
- if (res.ok && res.data && !res.data.raw) {
177
- const data = res.data;
178
- const loadedIds = Array.isArray(data.loaded) ? data.loaded : [];
179
- const recommended = Array.isArray(data.recommended) ? data.recommended.map((m) => ({
180
- ...m,
181
- name: m.name || m.display_name || m.id,
182
- family: m.family || m.modality || "local",
183
- state: loadedIds.includes(m.id) || data.current === m.id ? "loaded" : "available",
184
- })) : [];
185
- const loadedOnly = loadedIds
186
- .filter((id) => !recommended.some((m) => m.id === id))
187
- .map((id) => ({ id, name: id, family: "local", state: data.current === id ? "loaded" : "available" }));
188
- return {
189
- ok: true,
190
- status: res.status,
191
- source: "live",
192
- data: { ...data, catalog: Array.isArray(data.catalog) ? data.catalog : [...recommended, ...loadedOnly] },
193
- };
194
- }
195
- return { ok: false, status: res.status, data: { current: null, catalog: [] }, source: "unavailable", error: res.error };
196
- },
197
- loadModel(modelId, engine) {
198
- return raw("/models/load", { method: "POST", body: { model_id: modelId, engine: engine || null } });
199
- },
200
- unloadModel(modelId) {
201
- return raw(`/models/unload/${encodeURIComponent(modelId)}`, { method: "DELETE" });
202
- },
203
- sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
204
-
205
- /** POST /upload/document — manual document ingest (multipart/form-data).
206
- * Real backend path: parse → chunk → embed → knowledge-graph ingest
207
- * (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
208
- * source }; never throws. FormData must NOT carry a JSON Content-Type — the
209
- * browser sets the multipart boundary itself. */
210
- async uploadDocument(file) {
211
- const ws = store.get().workspaceId;
212
- const form = new FormData();
213
- form.append("file", file);
214
- try {
215
- const res = await fetch("/upload/document", {
216
- method: "POST",
217
- credentials: "same-origin",
218
- headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
219
- body: form,
220
- });
221
- let data = null;
222
- const text = await res.text();
223
- if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
224
- return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
225
- } catch (err) {
226
- return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
227
- }
228
- },
229
-
230
- adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
231
- adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
232
- adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
233
- adminSecurity() { return withFallback("/admin/security/overview", {}, EMPTY_ADMIN.security); },
234
- adminRoles() { return withFallback("/admin/roles", {}, EMPTY_ADMIN.roles); },
235
- adminPolicies() { return withFallback("/admin/policies", {}, EMPTY_ADMIN.policies); },
236
- vpcStatus() { return withFallback("/vpc/status", {}, EMPTY_ADMIN.vpc); },
237
-
238
- /* ── Embeddings (real backend: /api/embeddings/*) ───────────────────── */
239
- /** GET /api/embeddings/status — active provider, grade, dimensions, last index. */
240
- async embeddingsStatus() {
241
- const res = await raw("/api/embeddings/status");
242
- if (res.ok && res.data && res.data.provider) {
243
- return { ok: true, status: res.status, data: res.data, source: "live" };
244
- }
245
- // No backend → report unavailable honestly (never fabricate a provider).
246
- return {
247
- ok: false, status: res.status, source: "unavailable",
248
- data: { provider: null, active_provider: null, model: null,
249
- model_id: null, dimensions: null, grade: "unavailable",
250
- state: "unavailable", fell_back: false, health: { status: "unavailable", detail: "backend unavailable" },
251
- last_indexed_at: null },
252
- };
253
- },
254
- embeddingsProviders() { return withFallback("/api/embeddings/providers", {}, { active: "hash", providers: [] }); },
255
-
256
- /* ── Agents (real backend: AgentRuntime /agents/api/runtime/*) ───────── */
257
- /** GET /agents/api/runtime/status — roles, roster, runs, health from the runtime. */
258
- async agentRuntime() {
259
- const res = await raw("/agents/api/runtime/status");
260
- if (res.ok && res.data && res.data.runtime && Array.isArray(res.data.agents)) {
261
- return { ok: true, status: res.status, data: res.data, source: "live" };
262
- }
263
- // Fallback: unavailable roster, no fabricated run ledger.
264
- return {
265
- ok: false, status: res.status, source: "unavailable",
266
- data: { runtime: { ready: false, total_runs: 0, active_runs: 0 },
267
- health: { status: "unknown", checks: {} }, roles: [],
268
- agents: [], runs: [] },
269
- };
270
- },
271
- /** POST /agents/api/run — execute the multi-agent pipeline for a goal. */
272
- runAgent(goal, roles) { return raw("/agents/api/run", { method: "POST", body: { goal, roles: roles || [] } }); },
273
-
274
- /* ── Local computer memory (real backend: /workspace/computer-memory) ── */
275
- computerMemory() { return raw("/workspace/computer-memory"); },
276
- setComputerMemory(enabled) {
277
- return raw("/workspace/computer-memory", { method: "POST", body: { enabled, consent: { approved: !!enabled } } });
278
- },
279
-
280
- /* ── Organization workspaces (real backend: /workspace/orgs) ────────── */
281
- createOrg(name) { return raw("/workspace/orgs", { method: "POST", body: { name } }); },
282
- workspaceRegistry() { return withFallback("/workspace/registry", {}, { workspaces: [], roles: [], permissions: {} }); },
283
- activateWorkspace(workspace_id) { return raw("/workspace/activate", { method: "POST", body: { workspace_id } }); },
284
- workspaceDetail(workspace_id) { return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}`); },
285
- updateWorkspace(workspace_id, body) { return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}`, { method: "PATCH", body }); },
286
- archiveWorkspace(workspace_id) { return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}/archive`, { method: "POST", body: {} }); },
287
- addWorkspaceMember(workspace_id, user_id, role) {
288
- return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}/members`, { method: "POST", body: { user_id, role } });
289
- },
290
- updateWorkspaceMember(workspace_id, user_id, role) {
291
- return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}/members/${encodeURIComponent(user_id)}`, { method: "PATCH", body: { role } });
292
- },
293
- removeWorkspaceMember(workspace_id, user_id) {
294
- return raw(`/workspace/orgs/${encodeURIComponent(workspace_id)}/members/${encodeURIComponent(user_id)}`, { method: "DELETE" });
295
- },
296
- invitations() { return withFallback("/invitations", {}, { invitations: [] }); },
297
- createInvitation(body) { return raw("/invitations", { method: "POST", body }); },
298
- acceptInvitation(token) { return raw(`/invitations/${encodeURIComponent(token)}/accept`, { method: "POST", body: {} }); },
299
-
300
- /* ── Account / token-native auth ───────────────────────────────────── */
301
- profile() { return raw("/account/profile"); },
302
- login(email, password) { return raw("/login", { method: "POST", body: { email, password } }); },
303
- register(body) { return raw("/register", { method: "POST", body }); },
304
- logout() { return raw("/logout", { method: "POST", body: {} }); },
305
- updateProfile(body) { return raw("/account/profile", { method: "PATCH", body }); },
306
- changePassword(current_password, new_password) {
307
- return raw("/account/change-password", { method: "POST", body: { current_password, new_password } });
308
- },
309
- ssoConfig() { return withFallback("/auth/sso/config", {}, { enabled: false, providers: [] }); },
310
-
311
- /* ── Snapshots, Time Machine, realtime ─────────────────────────────── */
312
- snapshots() { return withFallback("/workspace/snapshots", {}, { snapshots: [] }); },
313
- createSnapshot(name) { return raw("/workspace/snapshots", { method: "POST", body: { name } }); },
314
- compareSnapshots(before_id, after_id) { return raw("/workspace/snapshots/compare", { method: "POST", body: { before_id, after_id } }); },
315
- snapshotExport(snapshot_id) { return raw(`/workspace/snapshots/${encodeURIComponent(snapshot_id)}/export`, { method: "POST", body: {} }); },
316
- snapshotRestore(snapshot_id) { return raw(`/workspace/snapshots/${encodeURIComponent(snapshot_id)}/restore`, { method: "POST", body: {} }); },
317
- timeMachine(limit = 100) { return withFallback(`/workspace/time-machine?limit=${encodeURIComponent(limit)}`, {}, { events: [] }); },
318
- realtimeFeed(limit = 50) { return withFallback(`/realtime/feed?limit=${encodeURIComponent(limit)}`, {}, { events: [], stats: {} }); },
319
- presence() { return withFallback("/realtime/presence", {}, { presence: [], stats: {} }); },
320
-
321
- /* ── Approval + Brain Network surfaces ─────────────────────────────── */
322
- permissionsPending() { return withFallback("/permissions/pending", {}, { pending: {}, count: 0 }); },
323
- denyPermission(token) { return raw(`/permissions/deny/${encodeURIComponent(token)}`, { method: "POST" }); },
324
- networkIdentity() { return withFallback("/network/identity", {}, {}); },
325
- networkPeers() { return withFallback("/network/peers", {}, { peers: [] }); },
326
- pairPeer(body) { return raw("/network/peers", { method: "POST", body }); },
327
- unpairPeer(peer_id) { return raw(`/network/peers/${encodeURIComponent(peer_id)}`, { method: "DELETE" }); },
328
- pushPeer(peer_id, workspace_id) { return raw(`/network/push/${encodeURIComponent(peer_id)}`, { method: "POST", body: { workspace_id } }); },
329
-
330
- /* ── Chat (real backend: SSE /chat + /history/*) ────────────────────── */
331
-
332
- /** GET /history/conversations — conversation list. */
333
- async chatHistory() {
334
- const res = await raw("/history/conversations");
335
- const list = res.ok && Array.isArray(res.data) ? res.data
336
- : res.ok && res.data && Array.isArray(res.data.conversations) ? res.data.conversations
337
- : null;
338
- if (list) return { ok: true, status: res.status, data: list, source: "live" };
339
- return { ok: false, status: res.status, data: [], source: "unavailable" };
340
- },
341
-
342
- /** GET /history/conversations/{id} — messages for one conversation. */
343
- async conversation(id) {
344
- const res = await raw(`/history/conversations/${encodeURIComponent(id)}`);
345
- if (res.ok && res.data && Array.isArray(res.data.messages)) {
346
- return { ok: true, status: res.status, data: res.data.messages, source: "live" };
347
- }
348
- return { ok: false, status: res.status, data: [], source: "unavailable" };
349
- },
350
-
351
- deleteConversation(id) {
352
- return raw(`/history/conversations/${encodeURIComponent(id)}`, { method: "DELETE" });
353
- },
354
-
355
- /**
356
- * POST /chat — streams the assistant reply over SSE.
357
- * Parses `data: {chunk, model, trace}` events (terminator `[DONE]`), calling
358
- * onChunk(delta, fullText) and onTrace(trace). If the endpoint is missing or
359
- * not an event-stream, reports that chat is unavailable (no generated answer is
360
- * invented). Resolves { source, text, trace, model, aborted }.
361
- */
362
- async streamChat(body, { onChunk, onTrace, signal } = {}) {
363
- const ws = store.get().workspaceId;
364
- let res;
365
- try {
366
- res = await fetch("/chat", {
367
- method: "POST",
368
- credentials: "same-origin",
369
- signal,
370
- headers: {
371
- "Content-Type": "application/json",
372
- "Accept": "text/event-stream",
373
- ...(ws ? { "X-Workspace-Id": ws } : {}),
374
- },
375
- body: JSON.stringify({ stream: true, max_tokens: 2048, temperature: 0.2, ...body }),
376
- });
377
- } catch (err) {
378
- if (err && err.name === "AbortError") return { source: "live", text: "", aborted: true };
379
- return simulateChat(body, { onChunk, onTrace, signal });
380
- }
381
- const ctype = res.headers.get("content-type") || "";
382
- if (!res.ok) {
383
- let data = null;
384
- try { data = await res.clone().json(); } catch {}
385
- const detail = data && (data.detail || data.message || data.error);
386
- const noModel = data && (data.error === "no_model_loaded" || /no .*model .*loaded/i.test(String(detail || "")));
387
- if (noModel) {
388
- return { source: "live", text: "", error: "no_model_loaded", errorMessage: String(detail || "No local model is loaded.") };
389
- }
390
- return simulateChat(body, { onChunk, onTrace, signal });
391
- }
392
- if (!res.body || !ctype.includes("text/event-stream")) {
393
- return simulateChat(body, { onChunk, onTrace, signal });
394
- }
395
- const reader = res.body.getReader();
396
- const decoder = new TextDecoder();
397
- let buf = "", text = "", trace = null, model = null;
398
- try {
399
- for (;;) {
400
- const { value, done } = await reader.read();
401
- if (done) break;
402
- buf += decoder.decode(value, { stream: true });
403
- const parts = buf.split("\n\n");
404
- buf = parts.pop();
405
- for (const part of parts) {
406
- const line = part.split("\n").find((l) => l.startsWith("data:"));
407
- if (!line) continue;
408
- const rawData = line.slice(5).trim();
409
- if (rawData === "[DONE]") return { source: "live", text, trace, model };
410
- let data; try { data = JSON.parse(rawData); } catch { continue; }
411
- // Standard chat streams `chunk`; the document-generation path streams
412
- // `text` (report body + footnotes). Accept both so doc requests render
413
- // instead of falsely reporting the backend as unreachable.
414
- const delta = data.chunk || data.text;
415
- if (delta) { text += delta; onChunk && onChunk(delta, text); }
416
- if (data.model) model = data.model;
417
- if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
418
- }
419
- }
420
- } catch (err) {
421
- if (err && err.name === "AbortError") return { source: "live", text, trace, model, aborted: true };
422
- if (!text) return simulateChat(body, { onChunk, onTrace, signal });
423
- }
424
- return { source: "live", text, trace, model };
425
- },
426
-
427
- /* ── v3.2 platform surfaces (all fallback-safe; never fabricate) ─────── */
428
-
429
- // Agent Registry (Part 2)
430
- agentRegistry(type) { return withFallback(`/agents/api/registry${type ? "?type=" + encodeURIComponent(type) : ""}`, {}, { agents: [], counts: {}, types: [] }); },
431
- agentCapabilities() { return withFallback("/agents/api/registry/capabilities", {}, { capabilities: {} }); },
432
- registerAgent(body) { return raw("/agents/api/registry", { method: "POST", body }); },
433
- updateAgent(id, body) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
434
- removeAgent(id) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "DELETE" }); },
435
- agentRunDetail(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}`); },
436
- agentRunReplay(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/replay`); },
437
- stopAgentRun(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
438
-
439
- // Marketplace + Templates (Parts 3, 4)
440
- templates(kind) { return withFallback(`/marketplace/templates${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { templates: [], kinds: [] }); },
441
- templateRegistry() { return withFallback("/marketplace/templates/registry", {}, { registry: [] }); },
442
- exportTemplate(kind, id) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/export`); },
443
- importTemplate(data) { return raw("/marketplace/templates/import", { method: "POST", body: { data } }); },
444
- installTemplate(data) { return raw("/marketplace/templates/install", { method: "POST", body: { data } }); },
445
- cloneTemplate(kind, id, name) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/clone`, { method: "POST", body: { name } }); },
446
- pluginsRegistry() { return withFallback("/plugins/registry", {}, { plugins: [] }); },
447
- pluginsDirectory() { return withFallback("/plugins/directory", {}, { plugins: [], categories: [] }); },
448
-
449
- // Workflow Agents (Part 5)
450
- workflowDefinitions() { return withFallback("/workflows/api/definitions", {}, { workflows: [] }); },
451
- createWorkflow(body) { return raw("/workflows/api/definitions", { method: "POST", body }); },
452
- updateWorkflow(id, body) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
453
- runWorkflow(id, body = {}) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, { method: "POST", body }); },
454
- workflowRuns() { return withFallback("/workflows/api/runs", {}, { runs: [] }); },
455
- workflowReplay(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/replay`); },
456
- stopWorkflowRun(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
457
- resumeWorkflowRun(runId, approved = true) {
458
- return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/resume`, { method: "POST", body: { approved } });
459
- },
460
- workflowTriggers() { return withFallback("/workflows/api/triggers", {}, { running: false, armed: [] }); },
461
-
462
- // Long-Term Memory + Memory Manager (Parts 7, 8)
463
- memoryManager() { return withFallback("/api/memory/manager", {}, { sources: [], tiers: [], usage: {} }); },
464
- memoryTiers() { return withFallback("/api/memory/tiers", {}, { tiers: [], workspace_kinds: [] }); },
465
- memoryInspect(source, limit = 50) { return withFallback(`/api/memory/inspect?source=${encodeURIComponent(source)}&limit=${limit}`, {}, { items: [] }); },
466
- memoryRecall(query, limit = 20) { return raw("/api/memory/recall", { method: "POST", body: { query, limit } }); },
467
- memoryPrune(body) { return raw("/api/memory/prune", { method: "POST", body }); },
468
- memoryCompact() { return raw("/api/memory/compact", { method: "POST", body: {} }); },
469
- memoryRebuild(target = "vector") { return raw("/api/memory/rebuild", { method: "POST", body: { target } }); },
470
- memoryClear(scope, confirm = true) { return raw("/api/memory/clear", { method: "POST", body: { scope, confirm } }); },
471
- workspaceMemories(kind) { return withFallback(`/workspace/memories${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { memories: [] }); },
472
-
473
- // Skills Registry (Part 9)
474
- skills() { return withFallback("/workspace/skills", {}, { skills: [] }); },
475
- skillEnable(skill) { return raw("/workspace/skills/enable", { method: "POST", body: { skill } }); },
476
- skillDisable(skill) { return raw("/workspace/skills/disable", { method: "POST", body: { skill } }); },
477
- skillInstall(skill, plugin) { return raw("/workspace/skills/install", { method: "POST", body: { skill, plugin: plugin || "" } }); },
478
- skillUninstall(skill) { return raw("/workspace/skills/uninstall", { method: "POST", body: { skill } }); },
479
- skillsMarketplace() { return withFallback("/skills/marketplace", {}, { skills: [], categories: [] }); },
480
-
481
- // Hooks Registry (Part 10)
482
- hooks(kind) { return withFallback(`/api/hooks${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { hooks: [], kinds: [], counts: {} }); },
483
- hookEnable(hook_id, enabled = true) { return raw("/api/hooks/enable", { method: "POST", body: { hook_id, enabled } }); },
484
- hookDisable(hook_id) { return raw("/api/hooks/disable", { method: "POST", body: { hook_id, enabled: false } }); },
485
- hookReorder(kind, ordered_ids) { return raw("/api/hooks/reorder", { method: "POST", body: { kind, ordered_ids } }); },
486
- hookRegister(body) { return raw("/api/hooks/register", { method: "POST", body }); },
487
- hookRemove(hook_id) { return raw(`/api/hooks/${encodeURIComponent(hook_id)}`, { method: "DELETE" }); },
488
-
489
- // Tool Registry + MCP (Parts 11, 12)
490
- toolPermissions() { return withFallback("/tools/permissions", {}, { permissions: [] }); },
491
- mcpTools() { return withFallback("/mcp/tools", {}, { tools: [], installed_mcps: [] }); },
492
- mcpInstalled() { return withFallback("/mcp/installed", {}, { installed: [] }); },
493
- mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
494
- mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
495
- mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
496
-
497
- /* ── v3.4 Platform Completion ───────────────────────────────────────────
498
- * Uploaded documents in Files, Connect Folder + Folder Watch over the real
499
- * on-device runtime, the Local Agent status, and Hooks dispatch/run-log.
500
- * All endpoints are real (latticeai/api + knowledge_graph_api); fallback-safe. */
501
-
502
- /** GET /knowledge-graph/documents — uploaded + indexed docs with index state. */
503
- async documents(limit = 200) {
504
- const res = await raw(`/knowledge-graph/documents?limit=${encodeURIComponent(limit)}`);
505
- if (res.ok && res.data && Array.isArray(res.data.documents)) {
506
- return { ok: true, status: res.status, data: res.data.documents, source: "live", total: res.data.total };
507
- }
508
- return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
509
- },
510
-
511
- // Local Agent (the on-device Lattice runtime: real GET /api/local-agent/status)
512
- async localAgent() {
513
- const res = await raw("/api/local-agent/status");
514
- if (res.ok && res.data && res.data.agent) {
515
- return { ok: true, status: res.status, data: res.data, source: "live" };
516
- }
517
- return {
518
- ok: false, status: res.status, source: "unavailable",
519
- data: { agent: { online: false }, health: {}, folders: { connected: 0, watching: 0 }, watch: { available: false, active: {} }, sources: [] },
520
- };
521
- },
522
-
523
- // Connect Folder + Folder Watch (real backend: /knowledge-graph/local/*)
524
- localRoots() { return withFallback("/knowledge-graph/local/roots", {}, { roots: [] }); },
525
- async localSources() {
526
- const res = await raw("/knowledge-graph/local/sources");
527
- if (res.ok && res.data && Array.isArray(res.data.sources)) {
528
- return { ok: true, status: res.status, data: res.data, source: "live" };
529
- }
530
- return { ok: false, status: res.status, data: { sources: [], watch: { available: false, active: {} } }, source: "unavailable" };
531
- },
532
- localWatchStatus() { return raw("/knowledge-graph/local/watch/status"); },
533
- localWatchStop(source_id) { return raw("/knowledge-graph/local/watch/stop", { method: "POST", body: { source_id } }); },
534
- approvePermission(token) { return raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" }); },
535
- indexFolder(path, opts = {}) {
536
- return raw("/knowledge-graph/local/index", { method: "POST", body: { path, ...opts } });
537
- },
538
- /** One-call Connect Folder: request → self-approve (the click is the consent)
539
- * → index (+ optional watch). Returns { ok, data, error }. */
540
- async connectFolder(path, { watch = true, includeOcr = false } = {}) {
541
- const probe = await raw("/knowledge-graph/local/index", { method: "POST", body: { path, approved: false } });
542
- const token = probe.data && probe.data.approval_token;
543
- if (!token) {
544
- const detail = (probe.data && (probe.data.detail || probe.data.error)) || "the runtime did not return an approval token";
545
- return { ok: false, error: detail, status: probe.status };
546
- }
547
- const approved = await raw(`/permissions/approve/${encodeURIComponent(token)}`, { method: "POST" });
548
- if (!approved.ok) {
549
- const detail = (approved.data && (approved.data.detail || approved.data.error)) || "approval failed";
550
- return { ok: false, error: detail, status: approved.status };
551
- }
552
- const res = await raw("/knowledge-graph/local/index", {
553
- method: "POST",
554
- body: { path, approved: true, approval_token: token, watch_enabled: watch, include_ocr: includeOcr, consent: { approved: true, source: "files-ui" } },
555
- });
556
- if (res.ok && res.data && !res.data.detail) return { ok: true, data: res.data, status: res.status };
557
- return { ok: false, error: (res.data && (res.data.detail || res.data.error)) || "indexing failed", status: res.status, data: res.data };
558
- },
559
-
560
- // Hooks dispatch (real backend: POST /api/hooks/run + GET /api/hooks/runs)
561
- hookRun(body) { return raw("/api/hooks/run", { method: "POST", body }); },
562
- hookRuns(limit = 50, kind) { return withFallback(`/api/hooks/runs?limit=${encodeURIComponent(limit)}${kind ? "&kind=" + encodeURIComponent(kind) : ""}`, {}, { runs: [], total: 0 }); },
563
-
564
- /* ── v3.6 Knowledge Graph First: ingestion provenance + portability ─────
565
- * The graph is the durable asset; these surface its health, where every node
566
- * came from, and local export/import/backup. All fallback-safe; never fake. */
567
-
568
- /** GET /api/knowledge-graph/portability — schema versions + stats + provenance counts. */
569
- async kgPortability() {
570
- const res = await raw("/api/knowledge-graph/portability");
571
- if (res.ok && res.data && res.data.available) {
572
- return { ok: true, status: res.status, data: res.data, source: "live" };
573
- }
574
- return {
575
- ok: false, status: res.status, source: "unavailable",
576
- data: { available: false, graph_schema_version: null, embed_dim: null,
577
- stats: { nodes: {}, edges: {} },
578
- provenance: { total: 0, by_source_type: {}, embedded: 0, duplicates: 0, last_ingested_at: null } },
579
- };
580
- },
581
-
582
- /** GET /api/knowledge-graph/provenance — recent ingestions (newest first). */
583
- kgProvenance(limit = 50, sourceType) {
584
- const qs = `?limit=${encodeURIComponent(limit)}${sourceType ? "&source_type=" + encodeURIComponent(sourceType) : ""}`;
585
- return withFallback(`/api/knowledge-graph/provenance${qs}`, {}, { items: [], count: 0 });
586
- },
587
-
588
- kgProvenanceCoverage() {
589
- return withFallback("/knowledge-graph/provenance/coverage", {}, { total_nodes: 0, nodes_with_provenance: 0, coverage_ratio: 0 });
590
- },
591
-
592
- /** POST /api/knowledge-graph/export — logical JSON export of the whole graph. */
593
- graphExport() { return raw("/api/knowledge-graph/export", { method: "POST", body: {} }); },
594
-
595
- /** POST /api/knowledge-graph/import — import an export artifact (merge|replace). */
596
- graphImport(artifact, mode = "merge", dryRun = false) {
597
- return raw("/api/knowledge-graph/import", { method: "POST", body: { artifact, mode, dry_run: dryRun } });
598
- },
599
-
600
- /** POST /api/knowledge-graph/backup — binary backup (sqlite + blobs) to a local zip. */
601
- graphBackup() { return raw("/api/knowledge-graph/backup", { method: "POST", body: {} }); },
602
-
603
- /** POST /api/browser/read-url — fetch a public URL locally into the graph. */
604
- browserReadUrl(url) { return raw("/api/browser/read-url", { method: "POST", body: { url } }); },
605
- };
606
-
607
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
608
-
609
- /** Transparent unavailable stream — used only when no chat backend is available. */
610
- async function simulateChat(body, { onChunk, onTrace, signal } = {}) {
611
- const q = (body && body.message) || "your question";
612
- const reply =
613
- `Chat is unavailable because the Lattice backend or active model is not reachable. ` +
614
- `Start the server, load a model, and rebuild retrieval before sending “${q}”.`;
615
- let text = "";
616
- for (const word of reply.split(" ")) {
617
- if (signal && signal.aborted) return { source: "unavailable", text, aborted: true };
618
- const delta = (text ? " " : "") + word;
619
- text += delta;
620
- onChunk && onChunk(delta, text);
621
- await sleep(16);
622
- }
623
- onTrace && onTrace(null);
624
- return { source: "unavailable", text, trace: null };
625
- }