@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.
@@ -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-Bn3ZXmWq.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-Bsn2S5X2.css">
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
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AAgCpF,OAAO,EAYL,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;AAyC3D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAkEvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAuC3E"}
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 { postgresDialect } from "@askdb/postgres";
9
- import { buildSchemaIndex, chunkContentHash, chunkSchema, createRetriever, loadChunkerSourcesFromDir, } from "@askdb/rag";
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.schemaDir));
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.schemaDir);
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.schemaDir, query);
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.schemaDir, options);
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 modelEnvVar = rt.ai.studioModel
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(schemaDir, options) {
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(schemaDir, "using RAG for sample generation", usage)
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
- modelEnvVar: rt.ai.studioModel ? "ASKDB_STUDIO_MODEL" : undefined,
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: postgresDialect,
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 formatStudioRagEmbeddingError(error, ragIndex.config);
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(schemaDir) {
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 stale = !lock ||
226
- !existsSync(embeddingsJsonPath) ||
227
- !existsSync(embeddingsBinPath) ||
228
- lock.embedderId !== config.embedderId ||
229
- embeddings?.dimensions !== config.dimensions ||
230
- Object.keys(lockHashes).length !== hashIds.length ||
231
- hashIds.some((id) => lockHashes[id] !== currentHashes[id]);
232
- return {
233
- schemaId: sources.schema.schemaId,
234
- embedder: {
235
- kind: config.kind,
236
- label: config.label,
237
- configured: config.configured,
238
- expectedId: config.embedderId,
239
- indexedId: lock?.embedderId ?? null,
240
- provider: config.kind === "ai-sdk" ? config.provider : null,
241
- model: config.kind === "ai-sdk" ? config.model : null,
242
- baseUrl: config.kind === "ai-sdk" ? config.baseUrl ?? null : null,
243
- },
244
- embedderId: lock?.embedderId ?? config.embedderId,
245
- expectedEmbedderId: config.embedderId,
246
- hasIndex: Boolean(lock && existsSync(embeddingsJsonPath) && existsSync(embeddingsBinPath)),
247
- stale,
248
- updatedAt: lock?.updatedAt ?? null,
249
- chunksTotal: chunkResult.chunks.length,
250
- chunksIndexed: Array.isArray(embeddings?.records) ? embeddings.records.length : 0,
251
- dimensions: typeof embeddings?.dimensions === "number" ? embeddings.dimensions : config.dimensions,
252
- expectedDimensions: config.dimensions,
253
- sensitiveExcluded: chunkResult.stats.sensitiveExcluded,
254
- sensitiveIncluded: chunkResult.stats.sensitiveIncluded,
255
- files: {
256
- lock: existsSync(lockPath),
257
- embeddingsJson: existsSync(embeddingsJsonPath),
258
- embeddingsBin: existsSync(embeddingsBinPath),
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(schemaDir) {
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(schemaDir, config);
269
- const sources = loadChunkerSourcesFromDir(schemaDir);
270
- const store = createFileStore({ basePath: join(schemaDir, "schema") });
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 formatStudioRagEmbeddingError(error, config);
304
+ throw formatStudioRagOperationError(error, config);
305
+ }
306
+ finally {
307
+ await store.dispose();
283
308
  }
284
- store.flush();
285
309
  return {
286
- status: getRagStatus(schemaDir),
310
+ status: await getRagStatus(state),
287
311
  stats: result.stats,
288
312
  usage: usage.toDto(),
289
313
  };
290
314
  }
291
- async function queryRag(schemaDir, query) {
315
+ async function queryRag(state, query) {
292
316
  const usage = createRequestUsageCollector();
293
- const ragIndex = await createCurrentStudioRagIndex(schemaDir, "querying chunks", usage);
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 formatStudioRagEmbeddingError(error, ragIndex.config);
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(schemaDir, action, usage) {
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(schemaDir);
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: createFileStore({ basePath: join(schemaDir, "schema") }),
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") ?? pickEnv(base, "ASKDB_STUDIO_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: "ASKDB_STUDIO_RAG_EMBEDDER_MODEL",
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") ?? pickEnv(base, "ASKDB_STUDIO_RAG_EMBEDDER_API_KEY");
397
- const modelOverride = pickEnv(base, "ASKDB_RAG_EMBEDDER_MODEL") ?? pickEnv(base, "ASKDB_STUDIO_RAG_EMBEDDER_MODEL");
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 formatStudioRagEmbeddingError(error, config) {
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(schemaDir, config) {
488
- const embeddingsJsonPath = join(schemaDir, "schema.embeddings.json");
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
- join(schemaDir, "schema.embeddings.json"),
494
- join(schemaDir, "schema.embeddings.bin"),
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.");