larkup 0.1.14

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 (141) hide show
  1. package/.env +2 -0
  2. package/app/actions/vercel.ts +417 -0
  3. package/app/api/chat/route.ts +192 -0
  4. package/app/api/chat/status/route.ts +55 -0
  5. package/app/api/chat/suggestions/route.ts +115 -0
  6. package/app/api/config/route.ts +73 -0
  7. package/app/api/config/test/route.ts +67 -0
  8. package/app/api/config/test-embedding/route.ts +40 -0
  9. package/app/api/config/test-llm/route.ts +47 -0
  10. package/app/api/config/test-provider/route.ts +36 -0
  11. package/app/api/demo/route.ts +176 -0
  12. package/app/api/deploy/ssh/route.ts +340 -0
  13. package/app/api/documents/route.ts +90 -0
  14. package/app/api/firecrawl/local/route.ts +46 -0
  15. package/app/api/index/route.ts +113 -0
  16. package/app/api/jobs/[id]/route.ts +44 -0
  17. package/app/api/jobs/route.ts +116 -0
  18. package/app/api/parse-file/route.ts +39 -0
  19. package/app/api/search/google/route.ts +162 -0
  20. package/app/api/search/route.ts +29 -0
  21. package/app/api/server/download/route.ts +112 -0
  22. package/app/api/server/generate/route.ts +46 -0
  23. package/app/api/server/local/route.ts +42 -0
  24. package/app/api/servers/route.ts +137 -0
  25. package/app/api/vector-stores/install/route.ts +65 -0
  26. package/app/api/vector-stores/status/route.ts +26 -0
  27. package/app/chat/page.tsx +15 -0
  28. package/app/configure/page.tsx +53 -0
  29. package/app/data/page.tsx +15 -0
  30. package/app/demo/page.tsx +15 -0
  31. package/app/favicon.ico +0 -0
  32. package/app/globals.css +190 -0
  33. package/app/index-data/page.tsx +15 -0
  34. package/app/layout.tsx +51 -0
  35. package/app/page.tsx +34 -0
  36. package/app/server/page.tsx +22 -0
  37. package/app/simple/chat/page.tsx +17 -0
  38. package/app/simple/docs/page.tsx +21 -0
  39. package/app/simple/layout.tsx +9 -0
  40. package/app/simple/settings/page.tsx +627 -0
  41. package/app/template.tsx +16 -0
  42. package/app/themes.css +733 -0
  43. package/components/alerts/generic-alert.tsx +74 -0
  44. package/components/app-sidebar.tsx +187 -0
  45. package/components/app-topnav.tsx +195 -0
  46. package/components/chat/chat-settings-modal.tsx +209 -0
  47. package/components/chat/chat-workspace.tsx +348 -0
  48. package/components/chat/knowledge-base-result.tsx +97 -0
  49. package/components/chat/message-item.tsx +103 -0
  50. package/components/client-layout-wrapper.tsx +182 -0
  51. package/components/configure/configure-form.tsx +1683 -0
  52. package/components/configure/store-fields.tsx +152 -0
  53. package/components/data/corpus-panel.tsx +499 -0
  54. package/components/data/data-workspace.tsx +272 -0
  55. package/components/data/firecrawl-notice.tsx +236 -0
  56. package/components/data/jobs-panel.tsx +485 -0
  57. package/components/data/paste-panel.tsx +79 -0
  58. package/components/data/scrape-panel.tsx +964 -0
  59. package/components/data/upload-panel.tsx +714 -0
  60. package/components/demo/demo-workspace.tsx +419 -0
  61. package/components/global-settings.tsx +286 -0
  62. package/components/index/index-workspace.tsx +482 -0
  63. package/components/onboarding/simple-setup.tsx +702 -0
  64. package/components/onboarding/tech-setup.tsx +121 -0
  65. package/components/onboarding/welcome-screen.tsx +183 -0
  66. package/components/page-header.tsx +44 -0
  67. package/components/server/code-viewer.tsx +87 -0
  68. package/components/server/deploy-button.tsx +1390 -0
  69. package/components/server/server-workspace.tsx +392 -0
  70. package/components/simple/sdk-connect-dialog.tsx +201 -0
  71. package/components/simple/simple-index-button.tsx +83 -0
  72. package/components/simple/simple-sidebar.tsx +161 -0
  73. package/components/theme-customizer-provider.tsx +171 -0
  74. package/components/theme-switcher.tsx +445 -0
  75. package/components/ui/alert-dialog.tsx +187 -0
  76. package/components/ui/alert.tsx +76 -0
  77. package/components/ui/badge.tsx +52 -0
  78. package/components/ui/button.tsx +59 -0
  79. package/components/ui/card.tsx +103 -0
  80. package/components/ui/checkbox.tsx +28 -0
  81. package/components/ui/collapsible.tsx +21 -0
  82. package/components/ui/dialog.tsx +159 -0
  83. package/components/ui/dropdown-menu.tsx +281 -0
  84. package/components/ui/input.tsx +22 -0
  85. package/components/ui/label.tsx +20 -0
  86. package/components/ui/popover.tsx +90 -0
  87. package/components/ui/progress.tsx +83 -0
  88. package/components/ui/provider-icon.tsx +94 -0
  89. package/components/ui/scroll-area.tsx +55 -0
  90. package/components/ui/select.tsx +248 -0
  91. package/components/ui/separator.tsx +25 -0
  92. package/components/ui/sheet.tsx +138 -0
  93. package/components/ui/skeleton.tsx +13 -0
  94. package/components/ui/sonner.tsx +63 -0
  95. package/components/ui/switch.tsx +32 -0
  96. package/components/ui/table.tsx +116 -0
  97. package/components/ui/tabs.tsx +84 -0
  98. package/components/ui/textarea.tsx +18 -0
  99. package/components/ui/tooltip.tsx +66 -0
  100. package/components/workspace/delete-server-dialog.tsx +71 -0
  101. package/components/workspace/server-form-dialog.tsx +103 -0
  102. package/components/workspace/server-switcher.tsx +163 -0
  103. package/components/workspace/workspace-provider.tsx +177 -0
  104. package/components/workspace/workspace-top-bar.tsx +23 -0
  105. package/components.json +21 -0
  106. package/lib/error-formatter.ts +15 -0
  107. package/lib/utils.ts +6 -0
  108. package/next-env.d.ts +6 -0
  109. package/next.config.mjs +18 -0
  110. package/package.json +65 -0
  111. package/postcss.config.mjs +8 -0
  112. package/public/docker.png +0 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/github.svg +3 -0
  115. package/public/hetzner.svg +1 -0
  116. package/public/icons/aws.svg +38 -0
  117. package/public/icons/azure.svg +1 -0
  118. package/public/icons/chroma.png +0 -0
  119. package/public/icons/cohere.svg +30 -0
  120. package/public/icons/digital-ocean.webp +0 -0
  121. package/public/icons/firecrawl2.png +0 -0
  122. package/public/icons/gcp.svg +1 -0
  123. package/public/icons/gemini.svg +1 -0
  124. package/public/icons/jina.svg +1 -0
  125. package/public/icons/lancedb2.png +0 -0
  126. package/public/icons/mistral.svg +469 -0
  127. package/public/icons/nomic.png +0 -0
  128. package/public/icons/openai.svg +1 -0
  129. package/public/icons/pgvector2.png +0 -0
  130. package/public/icons/pinecone.png +0 -0
  131. package/public/icons/qdrant.svg +35 -0
  132. package/public/icons/supabase.png +0 -0
  133. package/public/icons/vercel copy.svg +1 -0
  134. package/public/icons/vercel.svg +1 -0
  135. package/public/icons/voyage-light.png +0 -0
  136. package/public/icons/weaviate.webp +0 -0
  137. package/public/logo-light.png +0 -0
  138. package/public/logo.png +0 -0
  139. package/public/logo9.png +0 -0
  140. package/public/vercel.svg +1 -0
  141. package/tsconfig.json +33 -0
package/.env ADDED
@@ -0,0 +1,2 @@
1
+ SERPER_API_KEY="15cd177b8da4d70f262f0c116cf1164f40ca32c4"
2
+ AI_GATEWAY_API_KEY="vck_7qkeRibvoKg3fbVJNOVY1OjSDuixxryKYeeK9jsyaJDn65UrWD4PnzNa"
@@ -0,0 +1,417 @@
1
+ "use server";
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { getActiveServer } from "@larkup/core/workspace";
6
+
7
+ const VERCEL_API = "https://api.vercel.com";
8
+
9
+ interface VercelProject {
10
+ id: string;
11
+ name: string;
12
+ link?: {
13
+ type: string;
14
+ repo?: string;
15
+ repoId?: number;
16
+ org?: string;
17
+ gitCredentialId?: string;
18
+ productionBranch?: string;
19
+ };
20
+ }
21
+
22
+ interface VercelDeployment {
23
+ id: string;
24
+ uid: string;
25
+ name: string;
26
+ url: string;
27
+ readyState: string;
28
+ target?: string;
29
+ }
30
+
31
+ async function vercelFetch(
32
+ path: string,
33
+ token: string,
34
+ options: RequestInit = {},
35
+ ) {
36
+ const res = await fetch(`${VERCEL_API}${path}`, {
37
+ ...options,
38
+ headers: {
39
+ Authorization: `Bearer ${token}`,
40
+ "Content-Type": "application/json",
41
+ ...(options.headers || {}),
42
+ },
43
+ });
44
+
45
+ const text = await res.text();
46
+ let body: any;
47
+ try {
48
+ body = JSON.parse(text);
49
+ } catch {
50
+ body = { rawText: text };
51
+ }
52
+
53
+ if (!res.ok) {
54
+ const msg =
55
+ body?.error?.message ||
56
+ body?.message ||
57
+ `API error occurred: Status ${res.status} Content-Type "${res.headers.get("content-type")}". Body: ${text}`;
58
+ throw new Error(msg);
59
+ }
60
+
61
+ return body;
62
+ }
63
+
64
+ /** Look up a project by name or ID. Returns null if not found. */
65
+ async function getProject(
66
+ token: string,
67
+ nameOrId: string,
68
+ ): Promise<VercelProject | null> {
69
+ try {
70
+ const data = await vercelFetch(
71
+ `/v9/projects/${encodeURIComponent(nameOrId)}`,
72
+ token,
73
+ );
74
+ return data as VercelProject;
75
+ } catch (e: any) {
76
+ if (
77
+ e.message?.toLowerCase().includes("not found") ||
78
+ e.message?.toLowerCase().includes("project not found")
79
+ ) {
80
+ return null;
81
+ }
82
+ throw e;
83
+ }
84
+ }
85
+
86
+ /** Create a new blank Vercel project. */
87
+ async function createProject(
88
+ token: string,
89
+ name: string,
90
+ ): Promise<VercelProject> {
91
+ const data = await vercelFetch(`/v9/projects`, token, {
92
+ method: "POST",
93
+ body: JSON.stringify({ name, framework: null }),
94
+ });
95
+ return data as VercelProject;
96
+ }
97
+
98
+ /** Set an environment variable for the project */
99
+ async function setProjectEnv(
100
+ token: string,
101
+ projectId: string,
102
+ key: string,
103
+ value: string,
104
+ ) {
105
+ // Try to set it. We use the /v10/projects/:id/env endpoint.
106
+ // First, check if it exists so we can patch or post
107
+ const envData = await vercelFetch(`/v9/projects/${projectId}/env`, token);
108
+ const existing = envData.envs?.find((e: any) => e.key === key);
109
+
110
+ if (existing) {
111
+ await vercelFetch(`/v9/projects/${projectId}/env/${existing.id}`, token, {
112
+ method: "PATCH",
113
+ body: JSON.stringify({
114
+ value,
115
+ target: ["production", "preview", "development"],
116
+ }),
117
+ });
118
+ } else {
119
+ await vercelFetch(`/v10/projects/${projectId}/env`, token, {
120
+ method: "POST",
121
+ body: JSON.stringify([
122
+ {
123
+ key,
124
+ value,
125
+ target: ["production", "preview", "development"],
126
+ type: "plain",
127
+ },
128
+ ]),
129
+ });
130
+ }
131
+ }
132
+
133
+ function getFilesRecursively(dir: string, baseDir: string): any[] {
134
+ let results: any[] = [];
135
+ if (!fs.existsSync(dir)) return results;
136
+ const list = fs.readdirSync(dir);
137
+ for (const file of list) {
138
+ if (file === "node_modules" || file === ".git" || file === "server.log")
139
+ continue;
140
+ const filePath = path.join(dir, file);
141
+ const stat = fs.statSync(filePath);
142
+ if (stat && stat.isDirectory()) {
143
+ results = results.concat(getFilesRecursively(filePath, baseDir));
144
+ } else {
145
+ const data = fs.readFileSync(filePath);
146
+ const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
147
+ results.push({
148
+ file: relativePath,
149
+ data: data.toString("base64"),
150
+ encoding: "base64",
151
+ });
152
+ }
153
+ }
154
+ return results;
155
+ }
156
+
157
+ /** Deploy by sending files directly. */
158
+ async function triggerDeploymentWithFiles(
159
+ token: string,
160
+ project: VercelProject,
161
+ files: any[],
162
+ ): Promise<VercelDeployment> {
163
+ const body: Record<string, any> = {
164
+ name: project.name,
165
+ target: "production",
166
+ files,
167
+ };
168
+
169
+ const data = await vercelFetch(`/v13/deployments`, token, {
170
+ method: "POST",
171
+ body: JSON.stringify(body),
172
+ });
173
+
174
+ return data as VercelDeployment;
175
+ }
176
+
177
+ export async function getServerEnvRequirements(serverId: string) {
178
+ let activeId = serverId;
179
+ if (activeId === "default") {
180
+ const server = await getActiveServer();
181
+ if (server) activeId = server.id;
182
+ }
183
+
184
+ const cwd = process.cwd();
185
+
186
+ // ── Read the LIVE config (not the stale .env.example on disk).
187
+ // This ensures that switching vector stores in the UI is immediately
188
+ // reflected in the env-vars sheet without requiring a server regeneration.
189
+ let vectorStore: string = "lancedb";
190
+ let storeConfig: Record<string, string> = {};
191
+ const configCandidates = [
192
+ path.join(cwd, ".larkup", "servers", activeId, "config.json"),
193
+ path.join(cwd, ".larkup", "config.json"),
194
+ ];
195
+ for (const cfgPath of configCandidates) {
196
+ if (fs.existsSync(cfgPath)) {
197
+ try {
198
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
199
+ vectorStore = cfg.vectorStore ?? "lancedb";
200
+ storeConfig = cfg.storeConfig ?? {};
201
+ break;
202
+ } catch {
203
+ // continue to next candidate
204
+ }
205
+ }
206
+ }
207
+
208
+ // ── Map storeConfig fields → env var names.
209
+ // The configure page stores credentials as storeConfig.apiKey, storeConfig.indexName, etc.
210
+ // Mirror the same mapping used in server-runtime.ts so configured values are
211
+ // automatically pre-filled as defaults in the env-vars sheet.
212
+ const storeDefaults: Record<string, string> = {};
213
+ if (vectorStore === "pinecone") {
214
+ if (storeConfig.apiKey) storeDefaults["PINECONE_API_KEY"] = storeConfig.apiKey;
215
+ if (storeConfig.indexName) storeDefaults["PINECONE_INDEX"] = storeConfig.indexName;
216
+ if (storeConfig.namespace) storeDefaults["PINECONE_NAMESPACE"] = storeConfig.namespace;
217
+ if (storeConfig.sparseModel) storeDefaults["PINECONE_SPARSE_MODEL"] = storeConfig.sparseModel;
218
+ if (storeConfig.sparseIndexName) storeDefaults["PINECONE_SPARSE_INDEX"] = storeConfig.sparseIndexName;
219
+ } else {
220
+ if (storeConfig.tableName) storeDefaults["LANCEDB_TABLE"] = storeConfig.tableName;
221
+ if (storeConfig.uri) storeDefaults["LANCEDB_URI"] = storeConfig.uri;
222
+ if (storeConfig.apiKey) storeDefaults["LANCEDB_API_KEY"] = storeConfig.apiKey;
223
+ }
224
+
225
+ // ── Canonical env-var list — mirrors generateServer() in generate-server.ts.
226
+ // SERVER_API_KEY, PORT, LANCEDB_PATH, LANCEDB_MODE are handled internally
227
+ // and intentionally omitted here.
228
+ const envVarDefs: { key: string; required: boolean; help: string }[] = [
229
+ {
230
+ key: "EMBEDDING_API_KEY",
231
+ required: true,
232
+ help: "API key used to embed incoming queries.",
233
+ },
234
+ ];
235
+
236
+ if (vectorStore === "pinecone") {
237
+ envVarDefs.push(
238
+ { key: "PINECONE_API_KEY", required: true, help: "Pinecone API key." },
239
+ { key: "PINECONE_INDEX", required: true, help: "Pinecone index name to query." },
240
+ { key: "PINECONE_NAMESPACE", required: false, help: "Pinecone namespace (default 'default')." },
241
+ { key: "PINECONE_SPARSE_MODEL", required: false, help: "Pinecone sparse model (for hybrid search)." },
242
+ { key: "PINECONE_SPARSE_INDEX", required: false, help: "Pinecone sparse index name (for hybrid search)." },
243
+ );
244
+ } else {
245
+ // lancedb
246
+ envVarDefs.push(
247
+ { key: "LANCEDB_TABLE", required: false, help: "Table name holding the embedded chunks (default 'documents')." },
248
+ { key: "LANCEDB_URI", required: false, help: "LanceDB Cloud database URI (cloud mode)." },
249
+ { key: "LANCEDB_API_KEY", required: false, help: "LanceDB Cloud API key (cloud mode)." },
250
+ );
251
+ }
252
+
253
+ // ── Load .env file defaults (lowest priority).
254
+ const rootEnvPath = path.join(cwd, ".env");
255
+ const monoRootEnvPath = path.join(cwd, "../..", ".env");
256
+ const envFileDefaults: Record<string, string> = {};
257
+ for (const envFile of [monoRootEnvPath, rootEnvPath]) {
258
+ if (fs.existsSync(envFile)) {
259
+ const content = fs.readFileSync(envFile, "utf-8");
260
+ for (const line of content.split("\n")) {
261
+ const trimmed = line.trim();
262
+ if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
263
+ const [key, ...rest] = trimmed.split("=");
264
+ let val = rest.join("=").trim();
265
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
266
+ else if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
267
+ envFileDefaults[key.trim()] = val;
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // Priority: config > storeConfig (configure page) > .env file > empty
274
+ return envVarDefs.map((e) => {
275
+ let defaultValue = storeDefaults[e.key] || envFileDefaults[e.key] || "";
276
+ if (e.key === "EMBEDDING_API_KEY" && !defaultValue) {
277
+ // try to read from config directly
278
+ const cwd = process.cwd();
279
+ const configCandidates = [
280
+ path.join(cwd, ".larkup", "servers", activeId, "config.json"),
281
+ path.join(cwd, ".larkup", "config.json"),
282
+ ];
283
+ for (const cfgPath of configCandidates) {
284
+ if (fs.existsSync(cfgPath)) {
285
+ try {
286
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
287
+ if (cfg.embeddingApiKey) defaultValue = cfg.embeddingApiKey;
288
+ break;
289
+ } catch {}
290
+ }
291
+ }
292
+ }
293
+ return {
294
+ key: e.key,
295
+ help: e.help,
296
+ required: e.required,
297
+ defaultValue,
298
+ };
299
+ });
300
+ }
301
+
302
+
303
+ export async function deployToVercel(
304
+ token: string,
305
+ projectIdOrName: string,
306
+ serverId: string,
307
+ envVars: Record<string, string> = {}
308
+ ) {
309
+ let activeId = serverId;
310
+ if (activeId === "default") {
311
+ const server = await getActiveServer();
312
+ if (server) activeId = server.id;
313
+ }
314
+
315
+ if (!token || !projectIdOrName) {
316
+ return {
317
+ success: false,
318
+ error: "Vercel token and project ID/name are required.",
319
+ };
320
+ }
321
+
322
+ if (!activeId) {
323
+ return {
324
+ success: false,
325
+ error: "Server ID is required to read deployment files.",
326
+ };
327
+ }
328
+
329
+ try {
330
+ // 1. Find existing project or create one
331
+ let project = await getProject(token, projectIdOrName);
332
+ const wasNew = !project;
333
+
334
+ if (!project) {
335
+ console.log(`Project "${projectIdOrName}" not found. Creating it...`);
336
+ project = await createProject(token, projectIdOrName);
337
+ console.log(`Created project: ${project.id} (${project.name})`);
338
+ }
339
+
340
+ // 2. Set environment variables
341
+ for (const [key, value] of Object.entries(envVars)) {
342
+ if (value) {
343
+ await setProjectEnv(token, project.id, key, value);
344
+ }
345
+ }
346
+
347
+ // 3. Read the LIVE config to ensure we deploy the latest configuration
348
+ const cwd = process.cwd();
349
+ let config: any = null;
350
+ const configCandidates = [
351
+ path.join(cwd, ".larkup", "servers", activeId, "config.json"),
352
+ path.join(cwd, ".larkup", "config.json"),
353
+ ];
354
+ for (const cfgPath of configCandidates) {
355
+ if (fs.existsSync(cfgPath)) {
356
+ try {
357
+ config = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
358
+ break;
359
+ } catch {}
360
+ }
361
+ }
362
+
363
+ if (!config) {
364
+ return { success: false, error: "Configuration not found. Please save your settings first." };
365
+ }
366
+
367
+ // 4. Generate the server files in memory based on current config
368
+ const { generateServer } = await import("@larkup/core/generator/generate-server");
369
+ const generated = generateServer(config);
370
+
371
+ const files = generated.files.map((f) => ({
372
+ file: f.path,
373
+ data: f.encoding === "base64" ? f.contents : Buffer.from(f.contents).toString("base64"),
374
+ encoding: "base64",
375
+ }));
376
+
377
+ // 5. Attach LanceDB local data folder ONLY if LanceDB local is actually configured
378
+ const isLanceLocal = config.vectorStore === "lancedb" && config.storeConfig?.mode !== "cloud";
379
+ if (isLanceLocal) {
380
+ await setProjectEnv(token, project.id, "LANCEDB_PATH", "./lancedb");
381
+ const lancedbDir = path.join(cwd, ".larkup", "servers", activeId, "lancedb");
382
+ if (fs.existsSync(lancedbDir)) {
383
+ const lancedbFiles = getFilesRecursively(
384
+ lancedbDir,
385
+ path.join(cwd, ".larkup", "servers", activeId)
386
+ );
387
+ files.push(...lancedbFiles);
388
+ }
389
+ }
390
+
391
+ if (files.length === 0) {
392
+ return {
393
+ success: false,
394
+ error: "No files found to deploy in the server directory.",
395
+ };
396
+ }
397
+
398
+ // 4. Create new deployment with files
399
+ const deployRes = await triggerDeploymentWithFiles(token, project, files);
400
+
401
+ // 5. Build canonical production URL
402
+ const url = `https://${project.name}.vercel.app`;
403
+
404
+ return {
405
+ success: true,
406
+ url,
407
+ deploymentId: deployRes?.id,
408
+ projectCreated: wasNew,
409
+ };
410
+ } catch (error: any) {
411
+ console.error("Vercel Deploy Error:", error);
412
+ return {
413
+ success: false,
414
+ error: error.message || "Failed to trigger deployment on Vercel.",
415
+ };
416
+ }
417
+ }
@@ -0,0 +1,192 @@
1
+ import {
2
+ streamText,
3
+ convertToModelMessages,
4
+ tool,
5
+ stepCountIs,
6
+ type UIMessage,
7
+ } from "ai";
8
+ import { z } from "zod";
9
+ import { readConfig } from "@larkup/core/config-store";
10
+ import { readRun } from "@larkup/core/index-store";
11
+ import { refreshServerStatus } from "@larkup/core/generator/server-runtime";
12
+ import { createAdapter } from "@larkup/vector-stores/factory";
13
+ import { embedQuery } from "@larkup/core/indexing/embedder";
14
+ import { runWithServer } from "@larkup/core/workspace";
15
+ import {
16
+ getDefaultChatModel,
17
+ getChatModel,
18
+ } from "@larkup/core/chat-models/registry";
19
+
20
+ import { createOpenAI } from "@ai-sdk/openai";
21
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
22
+ import { createCohere } from "@ai-sdk/cohere";
23
+ import { createMistral } from "@ai-sdk/mistral";
24
+ import { createDeepSeek } from "@ai-sdk/deepseek";
25
+ import { createGateway } from "@ai-sdk/gateway";
26
+
27
+ export const maxDuration = 60;
28
+
29
+ const DEFAULT_SYSTEM_PROMPT = `You are a helpful research assistant powered by a knowledge base.
30
+
31
+ You have one tool:
32
+ - "searchKnowledgeBase" — searches a private RAG knowledge base.
33
+
34
+ Guidelines:
35
+ - ONLY skip the tool if the user's message is a basic greeting (e.g., "hi", "hello") or simple conversational filler.
36
+ - For ALL OTHER messages (especially questions about facts, personal preferences, instructions, or specific topics), you MUST call the searchKnowledgeBase tool FIRST before answering. Do not assume you cannot answer a question (like "what do I like?") without checking the knowledge base first!
37
+ - Synthesize a clear, well-structured answer based on the retrieved documents.
38
+ - Cite sources inline using markdown links to their URLs when available.
39
+ - Be concise and accurate. Never fabricate sources or facts.
40
+ - Don't reply to the user with a question — try to give the answer unless you truly need clarification.
41
+ - IMPORTANT: If the searchKnowledgeBase tool returns empty results, inform the user that you currently have no knowledge in your database for their specific question. However, you may still attempt to answer general questions using your general knowledge.
42
+ `;
43
+
44
+ /**
45
+ * Creates an AI SDK language model instance based on the provider and model ID.
46
+ */
47
+ function createChatModel(provider: string, modelId: string, apiKey?: string) {
48
+ const modelName = modelId.includes("/")
49
+ ? modelId.split("/").slice(1).join("/")
50
+ : modelId;
51
+ const key = apiKey;
52
+
53
+ switch (provider) {
54
+ case "google":
55
+ return createGoogleGenerativeAI({ apiKey: key })(modelName);
56
+ case "cohere":
57
+ return createCohere({ apiKey: key })(modelName);
58
+ case "mistral":
59
+ return createMistral({ apiKey: key })(modelName);
60
+ case "deepseek":
61
+ return createDeepSeek({ apiKey: key })(modelName);
62
+ case "vercel_ai_gateway":
63
+ return createGateway({ apiKey: key })(modelId);
64
+ case "openai":
65
+ default:
66
+ return createOpenAI({ apiKey: key })(modelName);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Retrieves documents from the knowledge base — either via the running
72
+ * generated server or directly from the local vector store.
73
+ */
74
+ async function queryKnowledgeBase(
75
+ query: string,
76
+ topK: number,
77
+ serverId: string | null,
78
+ ) {
79
+ const doRetrieve = async () => {
80
+ const config = await readConfig();
81
+
82
+ // 1) Try running generated server first
83
+ const server = await refreshServerStatus();
84
+ if (server.running) {
85
+ try {
86
+ const res = await fetch(`${server.endpoint}/query`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ query, topK }),
90
+ signal: AbortSignal.timeout(15_000),
91
+ });
92
+ const data = await res.json();
93
+ if (res.ok && data.hits) {
94
+ return {
95
+ query,
96
+ hits: (data.hits as any[]).map((h: any) => ({
97
+ title: h.title ?? "Untitled",
98
+ url: h.url ?? "",
99
+ score: Number((h.score ?? 0).toFixed(3)),
100
+ text: (h.text ?? "").slice(0, 1200),
101
+ })),
102
+ };
103
+ }
104
+ } catch {
105
+ // Fall through to direct retrieval
106
+ }
107
+ }
108
+
109
+ // 2) Direct retrieval from local vector store
110
+ const run = await readRun();
111
+ if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
112
+ return { query, hits: [] };
113
+ }
114
+
115
+ const vector = await embedQuery(config, query);
116
+ const adapter = await createAdapter(config);
117
+ const hits = await adapter.query(vector, topK, query);
118
+
119
+ return {
120
+ query,
121
+ hits: hits.map((h) => ({
122
+ title: h.title ?? "Untitled",
123
+ url: h.url ?? "",
124
+ score: Number((h.score ?? 0).toFixed(3)),
125
+ text: (h.text ?? "").slice(0, 1200),
126
+ })),
127
+ };
128
+ };
129
+
130
+ return serverId ? runWithServer(serverId, doRetrieve) : doRetrieve();
131
+ }
132
+
133
+ export async function POST(req: Request) {
134
+ const {
135
+ messages,
136
+ serverId,
137
+ chatModelId: requestedModelId,
138
+ }: {
139
+ messages: UIMessage[];
140
+ serverId?: string;
141
+ chatModelId?: string;
142
+ } = await req.json();
143
+
144
+ const config = await readConfig();
145
+ const provider = config.chatProvider || config.embeddingProvider;
146
+
147
+ // Resolve chat model: explicit request > config > default for provider
148
+ const chatModelId =
149
+ requestedModelId ||
150
+ config.chatModelId ||
151
+ getDefaultChatModel(provider)?.id ||
152
+ "openai/gpt-4o-mini";
153
+
154
+ const chatModelDescriptor = getChatModel(chatModelId);
155
+ const resolvedProvider =
156
+ provider === "vercel_ai_gateway"
157
+ ? "vercel_ai_gateway"
158
+ : chatModelDescriptor?.provider || provider;
159
+
160
+ const apiKey = config.chatApiKey || config.embeddingApiKey || undefined;
161
+ console.log("Using API Key for chat:", apiKey ? `${apiKey.substring(0, 10)}...` : "NONE", "Provider:", resolvedProvider);
162
+ const model = createChatModel(resolvedProvider, chatModelId, apiKey);
163
+
164
+ const result = streamText({
165
+ model,
166
+ system: config.systemPrompt || DEFAULT_SYSTEM_PROMPT,
167
+ messages: await convertToModelMessages(messages),
168
+ stopWhen: stepCountIs(5),
169
+ tools: {
170
+ searchKnowledgeBase: tool({
171
+ description:
172
+ "Search the private RAG knowledge base for relevant documents. Use this for factual questions about the indexed content.",
173
+ inputSchema: z.object({
174
+ query: z
175
+ .string()
176
+ .describe("The search query for the knowledge base."),
177
+ }),
178
+ execute: async ({ query }) => {
179
+ return queryKnowledgeBase(query, 5, serverId ?? null);
180
+ },
181
+ }),
182
+ },
183
+ });
184
+
185
+ return result.toUIMessageStreamResponse({
186
+ onError: (error) => {
187
+ console.error("[chat] stream error:", error);
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ return message || "Something went wrong while generating a response.";
190
+ },
191
+ });
192
+ }
@@ -0,0 +1,55 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readConfig } from "@larkup/core/config-store";
3
+ import { readRun } from "@larkup/core/index-store";
4
+ import { runWithServer } from "@larkup/core/workspace";
5
+ import {
6
+ getChatModelsForProvider,
7
+ getDefaultChatModel,
8
+ } from "@larkup/core/chat-models/registry";
9
+
10
+ export const dynamic = "force-dynamic";
11
+
12
+ function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
13
+ return serverId ? runWithServer(serverId, fn) : fn();
14
+ }
15
+
16
+ /**
17
+ * GET /api/chat/status — readiness snapshot for the Chat stage.
18
+ *
19
+ * Reports whether there's an index ready to chat against, plus
20
+ * the available chat models for the user's selected provider.
21
+ */
22
+ export async function GET(req: Request) {
23
+ const serverId = new URL(req.url).searchParams.get("serverId");
24
+ return withServer(serverId, async () => {
25
+ const config = await readConfig();
26
+ const run = await readRun();
27
+
28
+ const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
29
+ const hasApiKey = !!(config.chatApiKey || config.embeddingApiKey);
30
+
31
+ const blockers: string[] = [];
32
+ if (!hasApiKey) {
33
+ blockers.push(
34
+ "Set an API Key in Settings.",
35
+ );
36
+ }
37
+
38
+ const provider = config.chatProvider || config.embeddingProvider;
39
+ const models = getChatModelsForProvider(provider);
40
+ const defaultModel = getDefaultChatModel(provider);
41
+ const chatModelId =
42
+ config.chatModelId || defaultModel?.id || "openai/gpt-4o-mini";
43
+
44
+ return NextResponse.json({
45
+ ready: hasApiKey,
46
+ indexed,
47
+ blockers,
48
+ provider,
49
+ chatModelId,
50
+ availableModels: models.map((m) => ({ id: m.id, label: m.label, provider: m.provider })),
51
+ suggestions: config.chatSuggestions || [],
52
+ });
53
+ });
54
+ }
55
+