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.
- package/.env +2 -0
- package/app/actions/vercel.ts +417 -0
- package/app/api/chat/route.ts +192 -0
- package/app/api/chat/status/route.ts +55 -0
- package/app/api/chat/suggestions/route.ts +115 -0
- package/app/api/config/route.ts +73 -0
- package/app/api/config/test/route.ts +67 -0
- package/app/api/config/test-embedding/route.ts +40 -0
- package/app/api/config/test-llm/route.ts +47 -0
- package/app/api/config/test-provider/route.ts +36 -0
- package/app/api/demo/route.ts +176 -0
- package/app/api/deploy/ssh/route.ts +340 -0
- package/app/api/documents/route.ts +90 -0
- package/app/api/firecrawl/local/route.ts +46 -0
- package/app/api/index/route.ts +113 -0
- package/app/api/jobs/[id]/route.ts +44 -0
- package/app/api/jobs/route.ts +116 -0
- package/app/api/parse-file/route.ts +39 -0
- package/app/api/search/google/route.ts +162 -0
- package/app/api/search/route.ts +29 -0
- package/app/api/server/download/route.ts +112 -0
- package/app/api/server/generate/route.ts +46 -0
- package/app/api/server/local/route.ts +42 -0
- package/app/api/servers/route.ts +137 -0
- package/app/api/vector-stores/install/route.ts +65 -0
- package/app/api/vector-stores/status/route.ts +26 -0
- package/app/chat/page.tsx +15 -0
- package/app/configure/page.tsx +53 -0
- package/app/data/page.tsx +15 -0
- package/app/demo/page.tsx +15 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +190 -0
- package/app/index-data/page.tsx +15 -0
- package/app/layout.tsx +51 -0
- package/app/page.tsx +34 -0
- package/app/server/page.tsx +22 -0
- package/app/simple/chat/page.tsx +17 -0
- package/app/simple/docs/page.tsx +21 -0
- package/app/simple/layout.tsx +9 -0
- package/app/simple/settings/page.tsx +627 -0
- package/app/template.tsx +16 -0
- package/app/themes.css +733 -0
- package/components/alerts/generic-alert.tsx +74 -0
- package/components/app-sidebar.tsx +187 -0
- package/components/app-topnav.tsx +195 -0
- package/components/chat/chat-settings-modal.tsx +209 -0
- package/components/chat/chat-workspace.tsx +348 -0
- package/components/chat/knowledge-base-result.tsx +97 -0
- package/components/chat/message-item.tsx +103 -0
- package/components/client-layout-wrapper.tsx +182 -0
- package/components/configure/configure-form.tsx +1683 -0
- package/components/configure/store-fields.tsx +152 -0
- package/components/data/corpus-panel.tsx +499 -0
- package/components/data/data-workspace.tsx +272 -0
- package/components/data/firecrawl-notice.tsx +236 -0
- package/components/data/jobs-panel.tsx +485 -0
- package/components/data/paste-panel.tsx +79 -0
- package/components/data/scrape-panel.tsx +964 -0
- package/components/data/upload-panel.tsx +714 -0
- package/components/demo/demo-workspace.tsx +419 -0
- package/components/global-settings.tsx +286 -0
- package/components/index/index-workspace.tsx +482 -0
- package/components/onboarding/simple-setup.tsx +702 -0
- package/components/onboarding/tech-setup.tsx +121 -0
- package/components/onboarding/welcome-screen.tsx +183 -0
- package/components/page-header.tsx +44 -0
- package/components/server/code-viewer.tsx +87 -0
- package/components/server/deploy-button.tsx +1390 -0
- package/components/server/server-workspace.tsx +392 -0
- package/components/simple/sdk-connect-dialog.tsx +201 -0
- package/components/simple/simple-index-button.tsx +83 -0
- package/components/simple/simple-sidebar.tsx +161 -0
- package/components/theme-customizer-provider.tsx +171 -0
- package/components/theme-switcher.tsx +445 -0
- package/components/ui/alert-dialog.tsx +187 -0
- package/components/ui/alert.tsx +76 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/checkbox.tsx +28 -0
- package/components/ui/collapsible.tsx +21 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +281 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +20 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/progress.tsx +83 -0
- package/components/ui/provider-icon.tsx +94 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +248 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/sonner.tsx +63 -0
- package/components/ui/switch.tsx +32 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +84 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/workspace/delete-server-dialog.tsx +71 -0
- package/components/workspace/server-form-dialog.tsx +103 -0
- package/components/workspace/server-switcher.tsx +163 -0
- package/components/workspace/workspace-provider.tsx +177 -0
- package/components/workspace/workspace-top-bar.tsx +23 -0
- package/components.json +21 -0
- package/lib/error-formatter.ts +15 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +18 -0
- package/package.json +65 -0
- package/postcss.config.mjs +8 -0
- package/public/docker.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/github.svg +3 -0
- package/public/hetzner.svg +1 -0
- package/public/icons/aws.svg +38 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/chroma.png +0 -0
- package/public/icons/cohere.svg +30 -0
- package/public/icons/digital-ocean.webp +0 -0
- package/public/icons/firecrawl2.png +0 -0
- package/public/icons/gcp.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/jina.svg +1 -0
- package/public/icons/lancedb2.png +0 -0
- package/public/icons/mistral.svg +469 -0
- package/public/icons/nomic.png +0 -0
- package/public/icons/openai.svg +1 -0
- package/public/icons/pgvector2.png +0 -0
- package/public/icons/pinecone.png +0 -0
- package/public/icons/qdrant.svg +35 -0
- package/public/icons/supabase.png +0 -0
- package/public/icons/vercel copy.svg +1 -0
- package/public/icons/vercel.svg +1 -0
- package/public/icons/voyage-light.png +0 -0
- package/public/icons/weaviate.webp +0 -0
- package/public/logo-light.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo9.png +0 -0
- package/public/vercel.svg +1 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readConfig, writeConfig } from "@larkup/core/config-store";
|
|
3
|
+
import { readRun } from "@larkup/core/index-store";
|
|
4
|
+
import { runWithServer } from "@larkup/core/workspace";
|
|
5
|
+
import { createAdapter } from "@larkup/vector-stores/factory";
|
|
6
|
+
import { embedQuery } from "@larkup/core/indexing/embedder";
|
|
7
|
+
import { generateObject } from "ai";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { getChatModel } from "@larkup/core/chat-models/registry";
|
|
10
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
11
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
12
|
+
import { createCohere } from "@ai-sdk/cohere";
|
|
13
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
14
|
+
import { createDeepSeek } from "@ai-sdk/deepseek";
|
|
15
|
+
import { createGateway } from "@ai-sdk/gateway";
|
|
16
|
+
|
|
17
|
+
export const dynamic = "force-dynamic";
|
|
18
|
+
|
|
19
|
+
function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
|
|
20
|
+
return serverId ? runWithServer(serverId, fn) : fn();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createChatModel(provider: string, modelId: string, apiKey?: string) {
|
|
24
|
+
const modelName = modelId.includes("/")
|
|
25
|
+
? modelId.split("/").slice(1).join("/")
|
|
26
|
+
: modelId;
|
|
27
|
+
const key = apiKey;
|
|
28
|
+
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case "google":
|
|
31
|
+
return createGoogleGenerativeAI({ apiKey: key })(modelName);
|
|
32
|
+
case "cohere":
|
|
33
|
+
return createCohere({ apiKey: key })(modelName);
|
|
34
|
+
case "mistral":
|
|
35
|
+
return createMistral({ apiKey: key })(modelName);
|
|
36
|
+
case "deepseek":
|
|
37
|
+
return createDeepSeek({ apiKey: key })(modelName);
|
|
38
|
+
case "vercel_ai_gateway":
|
|
39
|
+
return createGateway({ apiKey: key })(modelId);
|
|
40
|
+
case "openai":
|
|
41
|
+
default:
|
|
42
|
+
return createOpenAI({ apiKey: key })(modelName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function POST(req: Request) {
|
|
47
|
+
const serverId = new URL(req.url).searchParams.get("serverId");
|
|
48
|
+
return withServer(serverId, async () => {
|
|
49
|
+
const config = await readConfig();
|
|
50
|
+
const run = await readRun();
|
|
51
|
+
|
|
52
|
+
if (config.chatSuggestions && config.chatSuggestions.length > 0) {
|
|
53
|
+
return NextResponse.json({ suggestions: config.chatSuggestions });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
|
|
57
|
+
if (!indexed) {
|
|
58
|
+
return NextResponse.json({
|
|
59
|
+
suggestions: [
|
|
60
|
+
"What is this corpus about?",
|
|
61
|
+
"Summarize the key concepts",
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Pull a dummy vector (just embedded "summary" or similar) to get some chunks
|
|
68
|
+
const vector = await embedQuery(config, "overview summary");
|
|
69
|
+
const adapter = await createAdapter(config);
|
|
70
|
+
const hits = await adapter.query(vector, 5, "overview summary");
|
|
71
|
+
|
|
72
|
+
const context = hits
|
|
73
|
+
?.map((h) => h.text)
|
|
74
|
+
?.join("\n\n")
|
|
75
|
+
?.slice(0, 3000);
|
|
76
|
+
|
|
77
|
+
const provider = config.chatProvider || config.embeddingProvider;
|
|
78
|
+
const chatModelId = config.chatModelId || "openai/gpt-4o-mini";
|
|
79
|
+
const descriptor = getChatModel(chatModelId);
|
|
80
|
+
const resolvedProvider =
|
|
81
|
+
provider === "vercel_ai_gateway"
|
|
82
|
+
? "vercel_ai_gateway"
|
|
83
|
+
: descriptor?.provider || provider;
|
|
84
|
+
|
|
85
|
+
const aiModel = createChatModel(
|
|
86
|
+
resolvedProvider,
|
|
87
|
+
chatModelId,
|
|
88
|
+
config.chatApiKey || config.embeddingApiKey,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const { object } = await generateObject({
|
|
92
|
+
model: aiModel,
|
|
93
|
+
system:
|
|
94
|
+
"You are an AI that suggests 3 very short and concise questions a user could ask based on the provided text.",
|
|
95
|
+
prompt: `Text:\n${context}`,
|
|
96
|
+
schema: z.object({
|
|
97
|
+
suggestions: z.array(z.string()).length(3),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
config.chatSuggestions = object.suggestions;
|
|
102
|
+
await writeConfig(config);
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({ suggestions: object.suggestions });
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
console.error("Failed to generate suggestions:", e.message);
|
|
107
|
+
return NextResponse.json({
|
|
108
|
+
suggestions: [
|
|
109
|
+
"What is this corpus about?",
|
|
110
|
+
"Summarize the key concepts",
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { readConfig, writeConfig } from "@larkup/core/config-store"
|
|
3
|
+
import { getVectorStore, validateStoreConfig } from "@larkup/vector-stores/registry"
|
|
4
|
+
import { getEmbeddingModel } from "@larkup/core/embeddings/registry"
|
|
5
|
+
import { runWithServer } from "@larkup/core/workspace"
|
|
6
|
+
import type { RagConfig } from "@larkup/core/types"
|
|
7
|
+
|
|
8
|
+
// Uses node:fs — must run on the Node.js runtime, not edge.
|
|
9
|
+
export const runtime = "nodejs"
|
|
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
|
+
export async function GET(request: Request) {
|
|
17
|
+
const serverId = new URL(request.url).searchParams.get("serverId")
|
|
18
|
+
return withServer(serverId, async () => {
|
|
19
|
+
const config = await readConfig()
|
|
20
|
+
return NextResponse.json({ config })
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function PUT(request: Request) {
|
|
25
|
+
const serverId = new URL(request.url).searchParams.get("serverId")
|
|
26
|
+
let body: RagConfig
|
|
27
|
+
try {
|
|
28
|
+
body = (await request.json()) as RagConfig
|
|
29
|
+
} catch {
|
|
30
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate the embedding model exists.
|
|
34
|
+
const isCustom = body.embeddingModelId.startsWith("custom:")
|
|
35
|
+
if (!isCustom && !getEmbeddingModel(body.embeddingModelId)) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: `Unknown embedding model: ${body.embeddingModelId}` },
|
|
38
|
+
{ status: 400 },
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (isCustom) {
|
|
42
|
+
const modelName = body.embeddingModelId.slice("custom:".length)
|
|
43
|
+
const found = body.customEmbeddings?.find((m) => m.modelName === modelName)
|
|
44
|
+
if (!found) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: `Custom embedding model "${modelName}" not found in customEmbeddings` },
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate the store + its dynamic, store-specific fields.
|
|
53
|
+
const store = getVectorStore(body.vectorStore)
|
|
54
|
+
if (!store) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: `Unknown vector store: ${body.vectorStore}` },
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
const fieldErrors = validateStoreConfig(store, body.storeConfig ?? {}, body.indexType)
|
|
61
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: "Missing required vector store fields", fieldErrors },
|
|
64
|
+
{ status: 422 },
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return withServer(serverId, async () => {
|
|
69
|
+
const saved = await writeConfig(body)
|
|
70
|
+
return NextResponse.json({ config: saved })
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { getVectorStore, validateStoreConfig } from "@larkup/vector-stores/registry"
|
|
3
|
+
import { createAdapter } from "@larkup/vector-stores/factory"
|
|
4
|
+
import type { RagConfig } from "@larkup/core/types"
|
|
5
|
+
|
|
6
|
+
// Uses node:fs — must run on the Node.js runtime, not edge.
|
|
7
|
+
export const runtime = "nodejs"
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
let body: RagConfig
|
|
11
|
+
try {
|
|
12
|
+
body = (await request.json()) as RagConfig
|
|
13
|
+
} catch {
|
|
14
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Validate the store + its dynamic, store-specific fields.
|
|
18
|
+
const store = getVectorStore(body.vectorStore)
|
|
19
|
+
if (!store) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: `Unknown vector store: ${body.vectorStore}` },
|
|
22
|
+
{ status: 400 },
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fieldErrors = validateStoreConfig(store, body.storeConfig ?? {}, body.indexType)
|
|
27
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: "Missing required vector store fields", fieldErrors },
|
|
30
|
+
{ status: 422 },
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let dimensions = 1536
|
|
35
|
+
const isCustom = body.embeddingModelId.startsWith("custom:")
|
|
36
|
+
if (isCustom) {
|
|
37
|
+
const modelName = body.embeddingModelId.slice("custom:".length)
|
|
38
|
+
const found = body.customEmbeddings?.find((m) => m.modelName === modelName)
|
|
39
|
+
if (!found) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: `Custom embedding model "${modelName}" not found in customEmbeddings` },
|
|
42
|
+
{ status: 400 },
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
dimensions = found.dimensions
|
|
46
|
+
} else {
|
|
47
|
+
const embeddingModel = await import("@larkup/core/embeddings/registry").then(m => m.getEmbeddingModel(body.embeddingModelId))
|
|
48
|
+
if (!embeddingModel) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: `Unknown embedding model: ${body.embeddingModelId}` },
|
|
51
|
+
{ status: 400 },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
dimensions = embeddingModel.dimensions
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const adapter = await createAdapter(body)
|
|
59
|
+
await adapter.testConnection(dimensions)
|
|
60
|
+
return NextResponse.json({ success: true })
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: error instanceof Error ? error.message : "Connection failed" },
|
|
64
|
+
{ status: 400 },
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { createOpenAI } from "@ai-sdk/openai"
|
|
3
|
+
import { embed } from "ai"
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs"
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
let body: { baseUrl: string; apiKey?: string; modelName: string }
|
|
9
|
+
try {
|
|
10
|
+
body = await request.json()
|
|
11
|
+
} catch {
|
|
12
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { baseUrl, apiKey, modelName } = body
|
|
16
|
+
|
|
17
|
+
if (!baseUrl || !modelName) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "Missing required fields: baseUrl and modelName" },
|
|
20
|
+
{ status: 400 },
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const customProvider = createOpenAI({
|
|
26
|
+
baseURL: baseUrl,
|
|
27
|
+
apiKey: apiKey || "empty", // some backends require any string
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const model = customProvider.embedding(modelName)
|
|
31
|
+
const { embedding } = await embed({ model, value: "test connection" })
|
|
32
|
+
|
|
33
|
+
return NextResponse.json({ success: true, dimensions: embedding.length })
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{ error: error instanceof Error ? error.message : "Embedding connection failed" },
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
|
+
import { generateText } from "ai";
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
let body: { baseUrl: string; apiKey?: string; modelName: string };
|
|
9
|
+
try {
|
|
10
|
+
body = await request.json();
|
|
11
|
+
} catch {
|
|
12
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { baseUrl, apiKey, modelName } = body;
|
|
16
|
+
|
|
17
|
+
if (!baseUrl || !modelName) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "Missing required fields: baseUrl and modelName" },
|
|
20
|
+
{ status: 400 },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const customProvider = createOpenAI({
|
|
26
|
+
baseURL: baseUrl,
|
|
27
|
+
apiKey: apiKey || "empty", // some backends require any string
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const model = customProvider.chat(modelName);
|
|
31
|
+
// We just need a simple prompt to verify connection
|
|
32
|
+
const { text } = await generateText({
|
|
33
|
+
model,
|
|
34
|
+
prompt: "Reply with the word 'OK'",
|
|
35
|
+
maxOutputTokens: 5,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({ success: true, text });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{
|
|
42
|
+
error: error instanceof Error ? error.message : "LLM connection failed",
|
|
43
|
+
},
|
|
44
|
+
{ status: 400 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { embed } from "ai"
|
|
3
|
+
import { getAIModel } from "@larkup/core/embeddings/providers"
|
|
4
|
+
import type { RagConfig } from "@larkup/core/types"
|
|
5
|
+
|
|
6
|
+
export const runtime = "nodejs"
|
|
7
|
+
|
|
8
|
+
export async function POST(request: Request) {
|
|
9
|
+
let body: Partial<RagConfig>
|
|
10
|
+
try {
|
|
11
|
+
body = await request.json()
|
|
12
|
+
} catch {
|
|
13
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { embeddingProvider, embeddingModelId } = body
|
|
17
|
+
|
|
18
|
+
if (!embeddingProvider || !embeddingModelId) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: "Missing required fields: embeddingProvider or embeddingModelId" },
|
|
21
|
+
{ status: 400 },
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const model = getAIModel(body as RagConfig)
|
|
27
|
+
const { embedding } = await embed({ model, value: "test connection" })
|
|
28
|
+
|
|
29
|
+
return NextResponse.json({ success: true, dimensions: embedding.length })
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: error instanceof Error ? error.message : "Connection failed. Please check your API key." },
|
|
33
|
+
{ status: 400 },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readConfig } from "@larkup/core/config-store";
|
|
3
|
+
import { readRun } from "@larkup/core/index-store";
|
|
4
|
+
import { refreshServerStatus } from "@larkup/core/generator/server-runtime";
|
|
5
|
+
import { createAdapter } from "@larkup/vector-stores/factory";
|
|
6
|
+
import { getEmbeddingModel } from "@larkup/core/embeddings/registry";
|
|
7
|
+
import { getVectorStore } from "@larkup/vector-stores/registry";
|
|
8
|
+
import type { QueryHit } from "@larkup/vector-stores/adapter";
|
|
9
|
+
import { runWithServer } from "@larkup/core/workspace";
|
|
10
|
+
import { embedQuery } from "@larkup/core/indexing/embedder";
|
|
11
|
+
|
|
12
|
+
export const dynamic = "force-dynamic";
|
|
13
|
+
|
|
14
|
+
/** Run a handler against a specific server when `serverId` is provided. */
|
|
15
|
+
function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
|
|
16
|
+
return serverId ? runWithServer(serverId, fn) : fn();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/demo — readiness snapshot for the Demo stage.
|
|
21
|
+
*
|
|
22
|
+
* Reports whether there's a built index to query and whether a generated
|
|
23
|
+
* server is live, so the UI can label which path a query will take. Accepts
|
|
24
|
+
* `?serverId=` to inspect a specific server instead of the active one.
|
|
25
|
+
*/
|
|
26
|
+
export async function GET(req: Request) {
|
|
27
|
+
const serverId = new URL(req.url).searchParams.get("serverId");
|
|
28
|
+
return withServer(serverId, () => snapshot());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function snapshot() {
|
|
32
|
+
const config = await readConfig();
|
|
33
|
+
const run = await readRun();
|
|
34
|
+
const server = await refreshServerStatus();
|
|
35
|
+
|
|
36
|
+
const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
|
|
37
|
+
const ready = indexed || server.running;
|
|
38
|
+
|
|
39
|
+
const blockers: string[] = [];
|
|
40
|
+
if (!ready) {
|
|
41
|
+
blockers.push(
|
|
42
|
+
"Build a larkup index (or launch a generated server) before running a demo query.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
ready,
|
|
48
|
+
blockers,
|
|
49
|
+
indexed,
|
|
50
|
+
server: { running: server.running, endpoint: server.endpoint },
|
|
51
|
+
config: {
|
|
52
|
+
projectName: config.projectName,
|
|
53
|
+
embeddingModelId: config.embeddingModelId,
|
|
54
|
+
vectorStore: config.vectorStore,
|
|
55
|
+
indexType: config.indexType,
|
|
56
|
+
topK: config.topK,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DemoResult {
|
|
62
|
+
query: string;
|
|
63
|
+
hits: QueryHit[];
|
|
64
|
+
source: "server" | "direct";
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
tookMs: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* POST /api/demo — run a retrieval query.
|
|
71
|
+
*
|
|
72
|
+
* Prefers a running generated server (the artifact you'd deploy), so the demo
|
|
73
|
+
* exercises the real serving path. Falls back to in-process retrieval against
|
|
74
|
+
* the toolkit's own store when no server is running.
|
|
75
|
+
*/
|
|
76
|
+
export async function POST(req: Request) {
|
|
77
|
+
const started = Date.now();
|
|
78
|
+
|
|
79
|
+
let body: { query?: string; topK?: number; serverId?: string };
|
|
80
|
+
try {
|
|
81
|
+
body = await req.json();
|
|
82
|
+
} catch {
|
|
83
|
+
return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const query = body.query?.trim();
|
|
87
|
+
if (!query) {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: "A non-empty 'query' is required." },
|
|
90
|
+
{ status: 400 },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return withServer(body.serverId ?? null, () =>
|
|
95
|
+
runQuery(query, body.topK, started),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runQuery(
|
|
100
|
+
query: string,
|
|
101
|
+
requestedTopK: number | undefined,
|
|
102
|
+
started: number,
|
|
103
|
+
) {
|
|
104
|
+
const config = await readConfig();
|
|
105
|
+
const topK = Math.min(Math.max(Number(requestedTopK) || config.topK, 1), 20);
|
|
106
|
+
|
|
107
|
+
// 1) Prefer the running generated server — the real serving path.
|
|
108
|
+
const server = await refreshServerStatus();
|
|
109
|
+
if (server.running) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${server.endpoint}/query`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ query, topK }),
|
|
115
|
+
signal: AbortSignal.timeout(30_000),
|
|
116
|
+
});
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
if (res.ok) {
|
|
119
|
+
const result: DemoResult = {
|
|
120
|
+
query,
|
|
121
|
+
hits: data.hits ?? [],
|
|
122
|
+
source: "server",
|
|
123
|
+
endpoint: server.endpoint,
|
|
124
|
+
tookMs: Date.now() - started,
|
|
125
|
+
};
|
|
126
|
+
return NextResponse.json(result);
|
|
127
|
+
}
|
|
128
|
+
// Fall through to direct retrieval if the server errored.
|
|
129
|
+
} catch {
|
|
130
|
+
// Server unreachable — fall back to direct retrieval below.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2) Fallback: in-process retrieval against the toolkit's own store.
|
|
135
|
+
const run = await readRun();
|
|
136
|
+
if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{
|
|
139
|
+
error:
|
|
140
|
+
"No index to query yet. Build an index in the Index stage, or launch a server first.",
|
|
141
|
+
},
|
|
142
|
+
{ status: 409 },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!getEmbeddingModel(config.embeddingModelId)) {
|
|
147
|
+
return NextResponse.json(
|
|
148
|
+
{ error: `Unknown embedding model "${config.embeddingModelId}".` },
|
|
149
|
+
{ status: 400 },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (!getVectorStore(config.vectorStore)) {
|
|
153
|
+
return NextResponse.json(
|
|
154
|
+
{ error: `Unknown vector store "${config.vectorStore}".` },
|
|
155
|
+
{ status: 400 },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const vector = await embedQuery(config, query);
|
|
161
|
+
const adapter = await createAdapter(config);
|
|
162
|
+
const hits = await adapter.query(vector, topK, query);
|
|
163
|
+
|
|
164
|
+
const result: DemoResult = {
|
|
165
|
+
query,
|
|
166
|
+
hits,
|
|
167
|
+
source: "direct",
|
|
168
|
+
tookMs: Date.now() - started,
|
|
169
|
+
};
|
|
170
|
+
return NextResponse.json(result);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const message =
|
|
173
|
+
err instanceof Error ? err.message : "Retrieval failed unexpectedly.";
|
|
174
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
175
|
+
}
|
|
176
|
+
}
|