@askdb/studio 0.2.0-beta.1 → 0.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -10
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-B4TrMaCm.js +8 -0
- package/dist/client/assets/index-CCT1sMLb.css +2 -0
- package/dist/client/index.html +2 -2
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +219 -100
- package/dist/server.js.map +1 -1
- package/dist/shared/api.d.ts +8 -1
- package/dist/shared/api.d.ts.map +1 -1
- package/package.json +7 -6
- package/dist/client/assets/index-Bn3ZXmWq.js +0 -8
- package/dist/client/assets/index-Bsn2S5X2.css +0 -2
package/dist/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>AskDB Studio</title>
|
|
7
7
|
<link rel="icon" type="image/png" media="(prefers-color-scheme: light)" href="/assets/brand/light-icon.png" />
|
|
8
8
|
<link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="/assets/brand/dark-icon.png" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-B4TrMaCm.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CCT1sMLb.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { createPgvectorStore } from "@askdb/rag";
|
|
2
3
|
import { type Workspace } from "@askdb/enrich";
|
|
3
4
|
import type { StudioWorkspaceDto } from "./shared/api.js";
|
|
4
5
|
export type StudioOptions = {
|
|
@@ -7,6 +8,7 @@ export type StudioOptions = {
|
|
|
7
8
|
port?: number;
|
|
8
9
|
};
|
|
9
10
|
export type StudioServer = ReturnType<typeof createServer>;
|
|
11
|
+
export declare function setStudioPgvectorStoreFactoryForTests(factory: typeof createPgvectorStore | undefined): void;
|
|
10
12
|
export declare function createStudioServer(options: StudioOptions): StudioServer;
|
|
11
13
|
export declare function serializeWorkspace(workspace: Workspace): StudioWorkspaceDto;
|
|
12
14
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AAsBpF,OAAO,EAOL,mBAAmB,EAOpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAaL,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAOV,kBAAkB,EAEnB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAyD3D,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAwEvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAqC3E"}
|
package/dist/server.js
CHANGED
|
@@ -4,16 +4,17 @@ import { extname, relative, join, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateText as defaultGenerateText } from "ai";
|
|
6
6
|
import { getAskDbRuntimeConfig } from "@askdb/config";
|
|
7
|
-
import { ask, askDbAiKeyMissingMessage, createAskDbEmbeddingModel, createAskDbLanguageModelFromEnv, loadSchema, resolveAskDbAiConfig, resolveAskDbEmbeddingConfig, suggestEnrichment, } from "@askdb/core";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { createAiSdkEmbedder } from "@askdb/rag/embedders/ai-sdk";
|
|
11
|
-
import { createFileStore } from "@askdb/rag/stores/file";
|
|
12
|
-
import { buildDefaultTableBody, buildFrontmatter, buildSuggestionContext, buildSuggestionTarget, buildTableDraft, loadWorkspace, replaceH2Section, replaceTableDescription, saveTable, } from "@askdb/enrich";
|
|
7
|
+
import { ask, askDbAiKeyMissingMessage, createAskDbEmbeddingModel, createAskDbLanguageModelFromEnv, isBuiltInDialectId, loadSchema, resolveAskDbAiConfig, resolveAskDbEmbeddingConfig, suggestEnrichment, } from "@askdb/core";
|
|
8
|
+
import { buildSchemaIndex, chunkContentHash, chunkSchema, createAiSdkEmbedder, createFileStore, createMemoryStore, createPgvectorStore, createRetriever, loadChunkerSourcesFromDir, } from "@askdb/rag";
|
|
9
|
+
import { buildDefaultTableBody, buildFrontmatter, buildSuggestionContext, buildSuggestionTarget, buildTableDraft, loadWorkspace, replaceH2Section, replaceTableDescription, saveConcepts, saveTable, } from "@askdb/enrich";
|
|
13
10
|
const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
|
|
14
11
|
const STUDIO_RAG_MOCK_DIMENSIONS = 64;
|
|
15
12
|
const STUDIO_RAG_MOCK_EMBEDDER_ID = `studio:mock-lexical-${STUDIO_RAG_MOCK_DIMENSIONS}`;
|
|
16
13
|
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
14
|
+
let studioPgvectorStoreFactoryForTests;
|
|
15
|
+
export function setStudioPgvectorStoreFactoryForTests(factory) {
|
|
16
|
+
studioPgvectorStoreFactoryForTests = factory;
|
|
17
|
+
}
|
|
17
18
|
export function createStudioServer(options) {
|
|
18
19
|
const schemaDir = resolve(options.schema);
|
|
19
20
|
if (!existsSync(schemaDir)) {
|
|
@@ -42,6 +43,12 @@ export function createStudioServer(options) {
|
|
|
42
43
|
saveDraft(state, tableId, draft);
|
|
43
44
|
return writeJson(res, 200, serializeWorkspace(state.workspace));
|
|
44
45
|
}
|
|
46
|
+
if (req.method === "POST" && url.pathname === "/api/concepts") {
|
|
47
|
+
const body = await readJson(req);
|
|
48
|
+
const concepts = parseConceptsBody(body);
|
|
49
|
+
saveConceptsDraft(state, concepts);
|
|
50
|
+
return writeJson(res, 200, serializeWorkspace(state.workspace));
|
|
51
|
+
}
|
|
45
52
|
if (req.method === "POST" && url.pathname === "/api/suggest") {
|
|
46
53
|
const body = await readJson(req);
|
|
47
54
|
const source = parseSuggestSource(body);
|
|
@@ -49,22 +56,22 @@ export function createStudioServer(options) {
|
|
|
49
56
|
return writeJson(res, 200, { candidates });
|
|
50
57
|
}
|
|
51
58
|
if (req.method === "GET" && url.pathname === "/api/rag/status") {
|
|
52
|
-
return writeJson(res, 200, getRagStatus(state
|
|
59
|
+
return writeJson(res, 200, await getRagStatus(state));
|
|
53
60
|
}
|
|
54
61
|
if (req.method === "POST" && url.pathname === "/api/rag/index") {
|
|
55
|
-
const result = await indexRag(state
|
|
62
|
+
const result = await indexRag(state);
|
|
56
63
|
return writeJson(res, 200, result);
|
|
57
64
|
}
|
|
58
65
|
if (req.method === "POST" && url.pathname === "/api/rag/query") {
|
|
59
66
|
const body = await readJson(req);
|
|
60
67
|
const query = parseRagQuery(body);
|
|
61
|
-
const result = await queryRag(state
|
|
68
|
+
const result = await queryRag(state, query);
|
|
62
69
|
return writeJson(res, 200, result);
|
|
63
70
|
}
|
|
64
71
|
if (req.method === "POST" && url.pathname === "/api/ask") {
|
|
65
72
|
const body = await readJson(req);
|
|
66
73
|
const options = parseAskBody(body);
|
|
67
|
-
const result = await askSampleQuestion(state
|
|
74
|
+
const result = await askSampleQuestion(state, options);
|
|
68
75
|
return writeJson(res, 200, result);
|
|
69
76
|
}
|
|
70
77
|
if (req.method === "GET" && !url.pathname.startsWith("/api/")) {
|
|
@@ -84,9 +91,7 @@ export function serializeWorkspace(workspace) {
|
|
|
84
91
|
const rt = getAskDbRuntimeConfig();
|
|
85
92
|
const aiConfig = (() => {
|
|
86
93
|
try {
|
|
87
|
-
return resolveAskDbAiConfig(rt.ai.aiEnv
|
|
88
|
-
modelEnvVar: rt.ai.studioModel ? "ASKDB_STUDIO_MODEL" : undefined,
|
|
89
|
-
});
|
|
94
|
+
return resolveAskDbAiConfig(rt.ai.aiEnv);
|
|
90
95
|
}
|
|
91
96
|
catch {
|
|
92
97
|
// A misconfigured AI env (e.g. azure without resourceName) shouldn't crash the workspace
|
|
@@ -116,6 +121,10 @@ export function serializeWorkspace(workspace) {
|
|
|
116
121
|
concepts: workspace.concepts?.frontmatter.concepts ?? [],
|
|
117
122
|
};
|
|
118
123
|
}
|
|
124
|
+
function saveConceptsDraft(state, concepts) {
|
|
125
|
+
saveConcepts(state.workspace, { concepts });
|
|
126
|
+
state.workspace = loadWorkspace(state.schemaDir);
|
|
127
|
+
}
|
|
119
128
|
function saveDraft(state, tableId, draft) {
|
|
120
129
|
const table = state.workspace.tables.find((candidate) => candidate.physical.id === tableId);
|
|
121
130
|
if (!table)
|
|
@@ -135,43 +144,39 @@ function saveDraft(state, tableId, draft) {
|
|
|
135
144
|
}
|
|
136
145
|
async function suggestForSource(workspace, source) {
|
|
137
146
|
const rt = getAskDbRuntimeConfig();
|
|
138
|
-
const
|
|
139
|
-
? "ASKDB_STUDIO_MODEL"
|
|
140
|
-
: rt.ai.tuiModel
|
|
141
|
-
? "ASKDB_TUI_MODEL"
|
|
142
|
-
: undefined;
|
|
143
|
-
const model = await createAskDbLanguageModelFromEnv(rt.ai.aiEnv, {
|
|
144
|
-
modelEnvVar,
|
|
145
|
-
});
|
|
147
|
+
const model = await createAskDbLanguageModelFromEnv(rt.ai.aiEnv);
|
|
146
148
|
if (!model) {
|
|
147
149
|
throw new StudioHttpError(400, askDbAiKeyMissingMessage("AI enrichment suggestions"));
|
|
148
150
|
}
|
|
149
151
|
const candidates = await suggestEnrichment(buildSuggestionTarget(workspace, source), buildSuggestionContext(workspace, source.tableId), model);
|
|
150
152
|
return candidates.map((candidate) => ({ text: candidate.text }));
|
|
151
153
|
}
|
|
152
|
-
async function askSampleQuestion(
|
|
154
|
+
async function askSampleQuestion(state, options) {
|
|
153
155
|
const rt = getAskDbRuntimeConfig();
|
|
154
156
|
const mockSql = rt.dev.mockSql;
|
|
155
157
|
const aiConfig = mockSql ? undefined : resolveAskDbAiConfig(rt.ai.aiEnv);
|
|
156
158
|
if (!mockSql && !aiConfig) {
|
|
157
159
|
throw new StudioHttpError(400, `${askDbAiKeyMissingMessage("Sample NL-to-SQL generation")} Set ASKDB_MOCK_SQL to bypass the live model.`);
|
|
158
160
|
}
|
|
159
|
-
const schema = loadSchema(schemaDir);
|
|
161
|
+
const schema = loadSchema(state.schemaDir);
|
|
160
162
|
const retrievedChunks = [];
|
|
161
163
|
const usage = createRequestUsageCollector();
|
|
162
164
|
const ragIndex = options.useRag
|
|
163
|
-
? await createCurrentStudioRagIndex(
|
|
165
|
+
? await createCurrentStudioRagIndex(state, "using RAG for sample generation", usage)
|
|
164
166
|
: undefined;
|
|
165
167
|
const model = mockSql
|
|
166
168
|
? undefined
|
|
167
|
-
: (await createAskDbLanguageModelFromEnv(rt.ai.aiEnv
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
: (await createAskDbLanguageModelFromEnv(rt.ai.aiEnv));
|
|
170
|
+
const schemaProvider = "provider" in schema && typeof schema.provider === "string"
|
|
171
|
+
? schema.provider
|
|
172
|
+
: undefined;
|
|
173
|
+
const dialect = rt.nlToSql.dialect ??
|
|
174
|
+
(schemaProvider && isBuiltInDialectId(schemaProvider) ? schemaProvider : "postgres");
|
|
170
175
|
const result = await ask({
|
|
171
176
|
question: options.question,
|
|
172
177
|
schema,
|
|
173
178
|
model,
|
|
174
|
-
dialect
|
|
179
|
+
dialect,
|
|
175
180
|
explain: true,
|
|
176
181
|
...(ragIndex
|
|
177
182
|
? {
|
|
@@ -181,7 +186,7 @@ async function askSampleQuestion(schemaDir, options) {
|
|
|
181
186
|
results = await ragIndex.retriever(params);
|
|
182
187
|
}
|
|
183
188
|
catch (error) {
|
|
184
|
-
throw
|
|
189
|
+
throw formatStudioRagOperationError(error, ragIndex.config);
|
|
185
190
|
}
|
|
186
191
|
retrievedChunks.splice(0, retrievedChunks.length, ...results);
|
|
187
192
|
return results;
|
|
@@ -198,6 +203,8 @@ async function askSampleQuestion(schemaDir, options) {
|
|
|
198
203
|
: {
|
|
199
204
|
generateText: createTrackedGenerateText(usage),
|
|
200
205
|
},
|
|
206
|
+
}).finally(async () => {
|
|
207
|
+
await ragIndex?.dispose();
|
|
201
208
|
});
|
|
202
209
|
return {
|
|
203
210
|
sql: result.sql,
|
|
@@ -210,87 +217,104 @@ async function askSampleQuestion(schemaDir, options) {
|
|
|
210
217
|
usage: usage.toDto(),
|
|
211
218
|
};
|
|
212
219
|
}
|
|
213
|
-
function getRagStatus(
|
|
220
|
+
async function getRagStatus(state) {
|
|
214
221
|
const config = resolveStudioRagEmbedderConfig();
|
|
215
|
-
const sources = loadChunkerSourcesFromDir(schemaDir);
|
|
222
|
+
const sources = loadChunkerSourcesFromDir(state.schemaDir);
|
|
216
223
|
const chunkResult = chunkSchema(sources);
|
|
217
|
-
const lockPath = join(schemaDir, "schema.lock.json");
|
|
218
|
-
const embeddingsJsonPath = join(schemaDir, "schema.embeddings.json");
|
|
219
|
-
const embeddingsBinPath = join(schemaDir, "schema.embeddings.bin");
|
|
224
|
+
const lockPath = join(state.schemaDir, "schema.lock.json");
|
|
220
225
|
const lock = readOptionalJson(lockPath);
|
|
221
|
-
const embeddings = readOptionalJson(embeddingsJsonPath);
|
|
222
226
|
const currentHashes = Object.fromEntries(chunkResult.chunks.map((chunk) => [chunk.id, chunkContentHash(chunk.text)]));
|
|
223
227
|
const lockHashes = lock?.hashes ?? {};
|
|
224
228
|
const hashIds = Object.keys(currentHashes);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
229
|
+
const store = await openStudioRagStore(state, config.dimensions);
|
|
230
|
+
try {
|
|
231
|
+
const chunksIndexed = await countStudioRagStoreChunks(store, sources.schema.schemaId);
|
|
232
|
+
const stale = !lock ||
|
|
233
|
+
lock.embedderId !== config.embedderId ||
|
|
234
|
+
Object.keys(lockHashes).length !== hashIds.length ||
|
|
235
|
+
hashIds.some((id) => lockHashes[id] !== currentHashes[id]) ||
|
|
236
|
+
chunksIndexed !== hashIds.length;
|
|
237
|
+
const fileArtifacts = store.kind === "file" && store.basePath
|
|
238
|
+
? {
|
|
239
|
+
lock: existsSync(lockPath),
|
|
240
|
+
embeddingsJson: existsSync(`${store.basePath}.embeddings.json`),
|
|
241
|
+
embeddingsBin: existsSync(`${store.basePath}.embeddings.bin`),
|
|
242
|
+
}
|
|
243
|
+
: {
|
|
244
|
+
lock: existsSync(lockPath),
|
|
245
|
+
embeddingsJson: false,
|
|
246
|
+
embeddingsBin: false,
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
schemaId: sources.schema.schemaId,
|
|
250
|
+
store: {
|
|
251
|
+
kind: store.kind,
|
|
252
|
+
basePath: store.basePath ?? null,
|
|
253
|
+
table: store.table ?? null,
|
|
254
|
+
indexStrategy: store.indexStrategy ?? null,
|
|
255
|
+
},
|
|
256
|
+
embedder: {
|
|
257
|
+
kind: config.kind,
|
|
258
|
+
label: config.label,
|
|
259
|
+
configured: config.configured,
|
|
260
|
+
expectedId: config.embedderId,
|
|
261
|
+
indexedId: lock?.embedderId ?? null,
|
|
262
|
+
provider: config.kind === "ai-sdk" ? config.provider : null,
|
|
263
|
+
model: config.kind === "ai-sdk" ? config.model : null,
|
|
264
|
+
baseUrl: config.kind === "ai-sdk" ? config.baseUrl ?? null : null,
|
|
265
|
+
},
|
|
266
|
+
embedderId: lock?.embedderId ?? config.embedderId,
|
|
267
|
+
expectedEmbedderId: config.embedderId,
|
|
268
|
+
hasIndex: Boolean(lock && chunksIndexed > 0),
|
|
269
|
+
stale,
|
|
270
|
+
updatedAt: lock?.updatedAt ?? null,
|
|
271
|
+
chunksTotal: chunkResult.chunks.length,
|
|
272
|
+
chunksIndexed,
|
|
273
|
+
dimensions: config.dimensions,
|
|
274
|
+
expectedDimensions: config.dimensions,
|
|
275
|
+
sensitiveExcluded: chunkResult.stats.sensitiveExcluded,
|
|
276
|
+
sensitiveIncluded: chunkResult.stats.sensitiveIncluded,
|
|
277
|
+
files: fileArtifacts,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await store.dispose();
|
|
282
|
+
}
|
|
261
283
|
}
|
|
262
|
-
async function indexRag(
|
|
284
|
+
async function indexRag(state) {
|
|
263
285
|
const config = resolveStudioRagEmbedderConfig();
|
|
264
286
|
if (!config.configured) {
|
|
265
287
|
throw new StudioHttpError(400, studioRagAiSdkKeyMissingMessage());
|
|
266
288
|
}
|
|
267
289
|
const usage = createRequestUsageCollector();
|
|
268
|
-
clearIncompatibleRagStore(
|
|
269
|
-
const sources = loadChunkerSourcesFromDir(schemaDir);
|
|
270
|
-
const store =
|
|
290
|
+
clearIncompatibleRagStore(state, config);
|
|
291
|
+
const sources = loadChunkerSourcesFromDir(state.schemaDir);
|
|
292
|
+
const store = await openStudioRagStore(state, config.dimensions);
|
|
271
293
|
let result;
|
|
272
294
|
try {
|
|
273
295
|
result = await buildSchemaIndex({
|
|
274
296
|
schema: sources,
|
|
275
297
|
embedder: await createStudioRagEmbedder(config, usage),
|
|
276
|
-
store,
|
|
298
|
+
store: store.store,
|
|
277
299
|
embedderId: config.embedderId,
|
|
278
|
-
lockFilePath: join(schemaDir, "schema.lock.json"),
|
|
300
|
+
lockFilePath: join(state.schemaDir, "schema.lock.json"),
|
|
279
301
|
});
|
|
280
302
|
}
|
|
281
303
|
catch (error) {
|
|
282
|
-
throw
|
|
304
|
+
throw formatStudioRagOperationError(error, config);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await store.dispose();
|
|
283
308
|
}
|
|
284
|
-
store.flush();
|
|
285
309
|
return {
|
|
286
|
-
status: getRagStatus(
|
|
310
|
+
status: await getRagStatus(state),
|
|
287
311
|
stats: result.stats,
|
|
288
312
|
usage: usage.toDto(),
|
|
289
313
|
};
|
|
290
314
|
}
|
|
291
|
-
async function queryRag(
|
|
315
|
+
async function queryRag(state, query) {
|
|
292
316
|
const usage = createRequestUsageCollector();
|
|
293
|
-
const ragIndex = await createCurrentStudioRagIndex(
|
|
317
|
+
const ragIndex = await createCurrentStudioRagIndex(state, "querying chunks", usage);
|
|
294
318
|
let results;
|
|
295
319
|
try {
|
|
296
320
|
results = await ragIndex.retriever({
|
|
@@ -303,7 +327,10 @@ async function queryRag(schemaDir, query) {
|
|
|
303
327
|
});
|
|
304
328
|
}
|
|
305
329
|
catch (error) {
|
|
306
|
-
throw
|
|
330
|
+
throw formatStudioRagOperationError(error, ragIndex.config);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
await ragIndex.dispose();
|
|
307
334
|
}
|
|
308
335
|
return {
|
|
309
336
|
question: query.question,
|
|
@@ -312,12 +339,12 @@ async function queryRag(schemaDir, query) {
|
|
|
312
339
|
usage: usage.toDto(),
|
|
313
340
|
};
|
|
314
341
|
}
|
|
315
|
-
async function createCurrentStudioRagIndex(
|
|
342
|
+
async function createCurrentStudioRagIndex(state, action, usage) {
|
|
316
343
|
const config = resolveStudioRagEmbedderConfig();
|
|
317
344
|
if (!config.configured) {
|
|
318
345
|
throw new StudioHttpError(400, studioRagAiSdkKeyMissingMessage());
|
|
319
346
|
}
|
|
320
|
-
const status = getRagStatus(
|
|
347
|
+
const status = (await getRagStatus(state));
|
|
321
348
|
if (!status.hasIndex) {
|
|
322
349
|
throw new StudioHttpError(400, `Build the RAG index before ${action}.`);
|
|
323
350
|
}
|
|
@@ -327,6 +354,7 @@ async function createCurrentStudioRagIndex(schemaDir, action, usage) {
|
|
|
327
354
|
if (!status.schemaId || typeof status.chunksTotal !== "number") {
|
|
328
355
|
throw new StudioHttpError(500, "Studio RAG status is missing schema metadata.");
|
|
329
356
|
}
|
|
357
|
+
const store = await openStudioRagStore(state, config.dimensions);
|
|
330
358
|
return {
|
|
331
359
|
config,
|
|
332
360
|
status: {
|
|
@@ -335,10 +363,85 @@ async function createCurrentStudioRagIndex(schemaDir, action, usage) {
|
|
|
335
363
|
},
|
|
336
364
|
retriever: createRetriever({
|
|
337
365
|
embedder: await createStudioRagEmbedder(config, usage),
|
|
338
|
-
store:
|
|
366
|
+
store: store.store,
|
|
339
367
|
}),
|
|
368
|
+
dispose: async () => {
|
|
369
|
+
await store.dispose();
|
|
370
|
+
},
|
|
340
371
|
};
|
|
341
372
|
}
|
|
373
|
+
function resolveStudioRagStoreConfig(state) {
|
|
374
|
+
const rt = getAskDbRuntimeConfig();
|
|
375
|
+
const kind = rt.structured.rag.store;
|
|
376
|
+
if (kind === "memory")
|
|
377
|
+
return { kind };
|
|
378
|
+
if (kind === "file") {
|
|
379
|
+
const basePath = rt.structured.rag.storeConfig.file?.basePath?.trim();
|
|
380
|
+
return { kind, basePath: basePath ? resolve(basePath) : join(state.schemaDir, "schema") };
|
|
381
|
+
}
|
|
382
|
+
const connectionString = pickFlat(rt.flat, "ASKDB_PGVECTOR_URL");
|
|
383
|
+
return {
|
|
384
|
+
kind,
|
|
385
|
+
connectionString,
|
|
386
|
+
table: rt.structured.rag.storeConfig.pgvector?.table?.trim() || undefined,
|
|
387
|
+
indexStrategy: pickFlat(rt.flat, "ASKDB_PGVECTOR_INDEX_STRATEGY"),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async function openStudioRagStore(state, dimensions) {
|
|
391
|
+
const config = resolveStudioRagStoreConfig(state);
|
|
392
|
+
if (config.kind === "memory") {
|
|
393
|
+
state.ragMemoryStore ??= createMemoryStore();
|
|
394
|
+
return {
|
|
395
|
+
kind: "memory",
|
|
396
|
+
store: state.ragMemoryStore,
|
|
397
|
+
dispose: async () => { },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (config.kind === "file") {
|
|
401
|
+
const store = createFileStore({ basePath: config.basePath });
|
|
402
|
+
return {
|
|
403
|
+
kind: "file",
|
|
404
|
+
store,
|
|
405
|
+
basePath: config.basePath,
|
|
406
|
+
dispose: async () => {
|
|
407
|
+
store.flush();
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (!config.connectionString) {
|
|
412
|
+
throw new StudioHttpError(400, 'Studio pgvector RAG requires `ASKDB_PGVECTOR_URL` via `askdb.config.ts`.');
|
|
413
|
+
}
|
|
414
|
+
const pgvectorFactory = studioPgvectorStoreFactoryForTests ?? createPgvectorStore;
|
|
415
|
+
const store = pgvectorFactory({
|
|
416
|
+
connectionString: config.connectionString,
|
|
417
|
+
table: config.table,
|
|
418
|
+
dimensions,
|
|
419
|
+
...(config.indexStrategy ? { indexStrategy: config.indexStrategy } : {}),
|
|
420
|
+
});
|
|
421
|
+
await store.ensureSchema();
|
|
422
|
+
return {
|
|
423
|
+
kind: "pgvector",
|
|
424
|
+
store,
|
|
425
|
+
table: config.table,
|
|
426
|
+
indexStrategy: config.indexStrategy,
|
|
427
|
+
dispose: async () => {
|
|
428
|
+
await store.close();
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
async function countStudioRagStoreChunks(store, schemaId) {
|
|
433
|
+
if (typeof store.store.count === "function") {
|
|
434
|
+
return store.store.count({ schemaId });
|
|
435
|
+
}
|
|
436
|
+
if (typeof store.store.size === "function") {
|
|
437
|
+
return store.store.size();
|
|
438
|
+
}
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
function pickFlat(flat, key) {
|
|
442
|
+
const value = flat[key];
|
|
443
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
444
|
+
}
|
|
342
445
|
function pickEnv(env, key) {
|
|
343
446
|
const v = env[key];
|
|
344
447
|
return typeof v === "string" && v.trim() !== "" ? v.trim() : undefined;
|
|
@@ -346,7 +449,7 @@ function pickEnv(env, key) {
|
|
|
346
449
|
function resolveStudioRagEmbedderConfig() {
|
|
347
450
|
const rt = getAskDbRuntimeConfig();
|
|
348
451
|
const base = rt.ai.aiEnv;
|
|
349
|
-
const explicitKind = pickEnv(base, "ASKDB_RAG_EMBEDDER")
|
|
452
|
+
const explicitKind = pickEnv(base, "ASKDB_RAG_EMBEDDER");
|
|
350
453
|
const kind = explicitKind?.toLowerCase();
|
|
351
454
|
if (kind === "mock") {
|
|
352
455
|
return {
|
|
@@ -362,7 +465,7 @@ function resolveStudioRagEmbedderConfig() {
|
|
|
362
465
|
}
|
|
363
466
|
const env = buildStudioRagEmbeddingEnv(kind, base);
|
|
364
467
|
const aiConfig = resolveAskDbEmbeddingConfig(env, {
|
|
365
|
-
modelEnvVar: "
|
|
468
|
+
modelEnvVar: "ASKDB_RAG_EMBEDDER_MODEL",
|
|
366
469
|
modelDefault: DEFAULT_EMBEDDING_MODEL,
|
|
367
470
|
});
|
|
368
471
|
if (!aiConfig && kind === undefined) {
|
|
@@ -376,8 +479,7 @@ function resolveStudioRagEmbedderConfig() {
|
|
|
376
479
|
}
|
|
377
480
|
const provider = aiConfig?.provider ?? fallbackStudioRagProvider(kind, base);
|
|
378
481
|
const model = aiConfig?.model ?? DEFAULT_EMBEDDING_MODEL;
|
|
379
|
-
const dimensionOverride = readPositiveIntegerEnv(pickEnv(base, "ASKDB_RAG_EMBEDDER_DIMENSIONS")
|
|
380
|
-
pickEnv(base, "ASKDB_STUDIO_RAG_EMBEDDER_DIMENSIONS"));
|
|
482
|
+
const dimensionOverride = readPositiveIntegerEnv(pickEnv(base, "ASKDB_RAG_EMBEDDER_DIMENSIONS"));
|
|
381
483
|
const dimensions = dimensionOverride ?? defaultEmbeddingDimensions(model);
|
|
382
484
|
return {
|
|
383
485
|
kind: "ai-sdk",
|
|
@@ -393,14 +495,12 @@ function resolveStudioRagEmbedderConfig() {
|
|
|
393
495
|
};
|
|
394
496
|
}
|
|
395
497
|
function buildStudioRagEmbeddingEnv(kind, base) {
|
|
396
|
-
const apiKeyOverride = pickEnv(base, "ASKDB_RAG_EMBEDDER_API_KEY")
|
|
397
|
-
const
|
|
398
|
-
const baseUrlOverride = pickEnv(base, "ASKDB_RAG_EMBEDDER_BASE_URL") ?? pickEnv(base, "ASKDB_STUDIO_RAG_EMBEDDER_BASE_URL");
|
|
498
|
+
const apiKeyOverride = pickEnv(base, "ASKDB_RAG_EMBEDDER_API_KEY");
|
|
499
|
+
const baseUrlOverride = pickEnv(base, "ASKDB_RAG_EMBEDDER_BASE_URL");
|
|
399
500
|
return {
|
|
400
501
|
...base,
|
|
401
502
|
...(kind === "openai" ? { ASKDB_AI_PROVIDER: "openai" } : {}),
|
|
402
503
|
...(apiKeyOverride ? { ASKDB_AI_API_KEY: apiKeyOverride } : {}),
|
|
403
|
-
...(modelOverride ? { ASKDB_STUDIO_RAG_EMBEDDER_MODEL: modelOverride } : {}),
|
|
404
504
|
...(baseUrlOverride ? { ASKDB_AI_BASE_URL: baseUrlOverride } : {}),
|
|
405
505
|
};
|
|
406
506
|
}
|
|
@@ -437,11 +537,16 @@ async function createStudioRagEmbedder(config, usage) {
|
|
|
437
537
|
},
|
|
438
538
|
});
|
|
439
539
|
}
|
|
440
|
-
function
|
|
540
|
+
function formatStudioRagOperationError(error, config) {
|
|
541
|
+
if (error instanceof StudioHttpError)
|
|
542
|
+
return error;
|
|
441
543
|
if (config.kind === "mock") {
|
|
442
544
|
return new StudioHttpError(500, error instanceof Error ? error.message : String(error));
|
|
443
545
|
}
|
|
444
546
|
const apiError = findApiCallError(error);
|
|
547
|
+
if (!apiError) {
|
|
548
|
+
return new StudioHttpError(500, error instanceof Error ? error.message : String(error));
|
|
549
|
+
}
|
|
445
550
|
const parts = [
|
|
446
551
|
`Studio RAG embedding request failed for provider ${config.provider}, model ${config.model}.`,
|
|
447
552
|
];
|
|
@@ -484,15 +589,18 @@ function truncateForMessage(value) {
|
|
|
484
589
|
return undefined;
|
|
485
590
|
return trimmed.length > 500 ? `${trimmed.slice(0, 500)}...` : trimmed;
|
|
486
591
|
}
|
|
487
|
-
function clearIncompatibleRagStore(
|
|
488
|
-
const
|
|
592
|
+
function clearIncompatibleRagStore(state, config) {
|
|
593
|
+
const store = resolveStudioRagStoreConfig(state);
|
|
594
|
+
if (store.kind !== "file")
|
|
595
|
+
return;
|
|
596
|
+
const embeddingsJsonPath = `${store.basePath}.embeddings.json`;
|
|
489
597
|
const embeddings = readOptionalJson(embeddingsJsonPath);
|
|
490
598
|
if (embeddings?.dimensions === undefined || embeddings.dimensions === config.dimensions)
|
|
491
599
|
return;
|
|
492
600
|
for (const path of [
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
join(schemaDir, "schema.lock.json"),
|
|
601
|
+
`${store.basePath}.embeddings.json`,
|
|
602
|
+
`${store.basePath}.embeddings.bin`,
|
|
603
|
+
join(state.schemaDir, "schema.lock.json"),
|
|
496
604
|
]) {
|
|
497
605
|
rmSync(path, { force: true });
|
|
498
606
|
}
|
|
@@ -552,6 +660,17 @@ function readOptionalJson(path) {
|
|
|
552
660
|
return undefined;
|
|
553
661
|
}
|
|
554
662
|
}
|
|
663
|
+
function parseConceptsBody(body) {
|
|
664
|
+
if (!isRecord(body) || !Array.isArray(body.concepts)) {
|
|
665
|
+
throw new StudioHttpError(400, "`concepts` array is required.");
|
|
666
|
+
}
|
|
667
|
+
for (const concept of body.concepts) {
|
|
668
|
+
if (!isRecord(concept) || typeof concept.id !== "string" || typeof concept.label !== "string") {
|
|
669
|
+
throw new StudioHttpError(400, "Each concept requires a string `id` and `label`.");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return body.concepts;
|
|
673
|
+
}
|
|
555
674
|
function parseTableDraftBody(body) {
|
|
556
675
|
if (!isRecord(body) || !isRecord(body.draft)) {
|
|
557
676
|
throw new StudioHttpError(400, "`draft` is required.");
|