@deepagents/text2sql 0.11.0 → 0.12.1
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/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1334 -491
- package/dist/index.js.map +4 -4
- package/dist/lib/adapters/groundings/index.js +2034 -50
- package/dist/lib/adapters/groundings/index.js.map +4 -4
- package/dist/lib/adapters/groundings/report.grounding.d.ts.map +1 -1
- package/dist/lib/adapters/mysql/index.js +2034 -50
- package/dist/lib/adapters/mysql/index.js.map +4 -4
- package/dist/lib/adapters/postgres/index.js +2034 -50
- package/dist/lib/adapters/postgres/index.js.map +4 -4
- package/dist/lib/adapters/spreadsheet/index.js +35 -49
- package/dist/lib/adapters/spreadsheet/index.js.map +4 -4
- package/dist/lib/adapters/sqlite/index.js +2034 -50
- package/dist/lib/adapters/sqlite/index.js.map +4 -4
- package/dist/lib/adapters/sqlserver/column-stats.sqlserver.grounding.d.ts.map +1 -1
- package/dist/lib/adapters/sqlserver/index.js +2035 -53
- package/dist/lib/adapters/sqlserver/index.js.map +4 -4
- package/dist/lib/agents/developer.agent.d.ts.map +1 -1
- package/dist/lib/agents/explainer.agent.d.ts +4 -5
- package/dist/lib/agents/explainer.agent.d.ts.map +1 -1
- package/dist/lib/agents/question.agent.d.ts.map +1 -1
- package/dist/lib/agents/result-tools.d.ts +37 -0
- package/dist/lib/agents/result-tools.d.ts.map +1 -0
- package/dist/lib/agents/sql.agent.d.ts +1 -1
- package/dist/lib/agents/sql.agent.d.ts.map +1 -1
- package/dist/lib/agents/teachables.agent.d.ts.map +1 -1
- package/dist/lib/agents/text2sql.agent.d.ts +0 -21
- package/dist/lib/agents/text2sql.agent.d.ts.map +1 -1
- package/dist/lib/checkpoint.d.ts +1 -1
- package/dist/lib/checkpoint.d.ts.map +1 -1
- package/dist/lib/instructions.d.ts +9 -28
- package/dist/lib/instructions.d.ts.map +1 -1
- package/dist/lib/sql.d.ts +1 -1
- package/dist/lib/sql.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts +6 -7
- package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/last-query-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/segmented-context-extractor.d.ts +0 -6
- package/dist/lib/synthesis/extractors/segmented-context-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/sql-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/index.js +2394 -2132
- package/dist/lib/synthesis/index.js.map +4 -4
- package/dist/lib/synthesis/synthesizers/breadth-evolver.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts +3 -3
- package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/persona-generator.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts +1 -1
- package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts.map +1 -1
- package/package.json +9 -15
- package/dist/lib/instructions.js +0 -432
- package/dist/lib/instructions.js.map +0 -7
- package/dist/lib/teach/teachings.d.ts +0 -11
- package/dist/lib/teach/teachings.d.ts.map +0 -1
|
@@ -165,7 +165,7 @@ import {
|
|
|
165
165
|
} from "ai";
|
|
166
166
|
|
|
167
167
|
// packages/text2sql/src/lib/synthesis/extractors/base-contextual-extractor.ts
|
|
168
|
-
import { groq } from "@ai-sdk/groq";
|
|
168
|
+
import { groq as groq2 } from "@ai-sdk/groq";
|
|
169
169
|
import {
|
|
170
170
|
getToolOrDynamicToolName,
|
|
171
171
|
isTextUIPart,
|
|
@@ -173,1643 +173,1185 @@ import {
|
|
|
173
173
|
} from "ai";
|
|
174
174
|
import dedent from "dedent";
|
|
175
175
|
import z from "zod";
|
|
176
|
-
import { agent, generate, user } from "@deepagents/agent";
|
|
177
|
-
var contextResolverAgent = agent({
|
|
178
|
-
name: "context_resolver",
|
|
179
|
-
model: groq("openai/gpt-oss-20b"),
|
|
180
|
-
output: z.object({
|
|
181
|
-
question: z.string().describe(
|
|
182
|
-
"A standalone natural language question that the SQL query answers"
|
|
183
|
-
)
|
|
184
|
-
}),
|
|
185
|
-
prompt: (state) => dedent`
|
|
186
|
-
<identity>
|
|
187
|
-
You are an expert at understanding conversational context and generating clear,
|
|
188
|
-
standalone questions from multi-turn conversations.
|
|
189
|
-
</identity>
|
|
190
|
-
|
|
191
|
-
${state?.introspection ? `<schema>
|
|
192
|
-
${state.introspection}
|
|
193
|
-
</schema>` : ""}
|
|
194
|
-
|
|
195
|
-
<conversation>
|
|
196
|
-
${state?.conversation}
|
|
197
|
-
</conversation>
|
|
198
|
-
|
|
199
|
-
<sql>
|
|
200
|
-
${state?.sql}
|
|
201
|
-
</sql>
|
|
202
|
-
|
|
203
|
-
<task>
|
|
204
|
-
Given the conversation above and the SQL query that was executed,
|
|
205
|
-
generate a single, standalone natural language question that:
|
|
206
|
-
1. Fully captures the user's intent without needing prior context
|
|
207
|
-
2. Uses natural business language (not SQL terminology)
|
|
208
|
-
3. Could be asked by someone who hasn't seen the conversation
|
|
209
|
-
4. Accurately represents what the SQL query answers
|
|
210
|
-
</task>
|
|
211
|
-
|
|
212
|
-
<examples>
|
|
213
|
-
Conversation: "Show me customers" → "Filter to NY" → "Sort by revenue"
|
|
214
|
-
SQL: SELECT * FROM customers WHERE region = 'NY' ORDER BY revenue DESC
|
|
215
|
-
Question: "Show me customers in the NY region sorted by revenue"
|
|
216
176
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
177
|
+
// packages/context/dist/index.js
|
|
178
|
+
import { encode } from "gpt-tokenizer";
|
|
179
|
+
import { generateId } from "ai";
|
|
180
|
+
import pluralize from "pluralize";
|
|
181
|
+
import { titlecase } from "stringcase";
|
|
182
|
+
import chalk from "chalk";
|
|
183
|
+
import { defineCommand } from "just-bash";
|
|
184
|
+
import spawn from "nano-spawn";
|
|
185
|
+
import "bash-tool";
|
|
186
|
+
import spawn2 from "nano-spawn";
|
|
187
|
+
import {
|
|
188
|
+
createBashTool
|
|
189
|
+
} from "bash-tool";
|
|
190
|
+
import YAML from "yaml";
|
|
191
|
+
import { DatabaseSync } from "node:sqlite";
|
|
192
|
+
import { groq } from "@ai-sdk/groq";
|
|
193
|
+
import {
|
|
194
|
+
NoSuchToolError,
|
|
195
|
+
Output,
|
|
196
|
+
convertToModelMessages,
|
|
197
|
+
createUIMessageStream,
|
|
198
|
+
generateId as generateId2,
|
|
199
|
+
generateText,
|
|
200
|
+
smoothStream,
|
|
201
|
+
stepCountIs,
|
|
202
|
+
streamText
|
|
203
|
+
} from "ai";
|
|
204
|
+
import chalk2 from "chalk";
|
|
205
|
+
import "zod";
|
|
206
|
+
import "@deepagents/agent";
|
|
207
|
+
var defaultTokenizer = {
|
|
208
|
+
encode(text) {
|
|
209
|
+
return encode(text);
|
|
210
|
+
},
|
|
211
|
+
count(text) {
|
|
212
|
+
return encode(text).length;
|
|
241
213
|
}
|
|
214
|
+
};
|
|
215
|
+
var ModelsRegistry = class {
|
|
216
|
+
#cache = /* @__PURE__ */ new Map();
|
|
217
|
+
#loaded = false;
|
|
218
|
+
#tokenizers = /* @__PURE__ */ new Map();
|
|
219
|
+
#defaultTokenizer = defaultTokenizer;
|
|
242
220
|
/**
|
|
243
|
-
*
|
|
244
|
-
* Subclasses customize behavior via hooks, not by overriding this method.
|
|
221
|
+
* Load models data from models.dev API
|
|
245
222
|
*/
|
|
246
|
-
async
|
|
247
|
-
this
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (this.results.length === 0) {
|
|
252
|
-
return;
|
|
223
|
+
async load() {
|
|
224
|
+
if (this.#loaded) return;
|
|
225
|
+
const response = await fetch("https://models.dev/api.json");
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
|
253
228
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
229
|
+
const data = await response.json();
|
|
230
|
+
for (const [providerId, provider] of Object.entries(data)) {
|
|
231
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
232
|
+
const info = {
|
|
233
|
+
id: model.id,
|
|
234
|
+
name: model.name,
|
|
235
|
+
family: model.family,
|
|
236
|
+
cost: model.cost,
|
|
237
|
+
limit: model.limit,
|
|
238
|
+
provider: providerId
|
|
239
|
+
};
|
|
240
|
+
this.#cache.set(`${providerId}:${modelId}`, info);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
this.#loaded = true;
|
|
256
244
|
}
|
|
257
245
|
/**
|
|
258
|
-
*
|
|
246
|
+
* Get model info by ID
|
|
247
|
+
* @param modelId - Model ID (e.g., "openai:gpt-4o")
|
|
259
248
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (message2.role === "user") {
|
|
263
|
-
const text = getMessageText(message2);
|
|
264
|
-
if (text) {
|
|
265
|
-
await this.onUserMessage(text);
|
|
266
|
-
}
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (message2.role === "assistant") {
|
|
270
|
-
await this.extractFromAssistant(message2, toolName, includeFailures);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
249
|
+
get(modelId) {
|
|
250
|
+
return this.#cache.get(modelId);
|
|
273
251
|
}
|
|
274
252
|
/**
|
|
275
|
-
*
|
|
253
|
+
* Check if a model exists in the registry
|
|
276
254
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!isToolOrDynamicToolUIPart(part)) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (getToolOrDynamicToolName(part) !== toolName) {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
const toolInput = "input" in part ? part.input : void 0;
|
|
286
|
-
if (!toolInput?.sql) {
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
const success = part.state === "output-available";
|
|
290
|
-
const failed = part.state === "output-error";
|
|
291
|
-
if (failed && !includeFailures) {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (!success && !failed) {
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
const snapshot = this.getContextSnapshot();
|
|
298
|
-
if (snapshot.length === 0) {
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
this.results.push({
|
|
302
|
-
sql: toolInput.sql,
|
|
303
|
-
success,
|
|
304
|
-
conversationContext: snapshot
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
const assistantText = getMessageText(message2);
|
|
308
|
-
if (assistantText) {
|
|
309
|
-
this.context.push(`Assistant: ${assistantText}`);
|
|
310
|
-
}
|
|
255
|
+
has(modelId) {
|
|
256
|
+
return this.#cache.has(modelId);
|
|
311
257
|
}
|
|
312
258
|
/**
|
|
313
|
-
*
|
|
259
|
+
* List all available model IDs
|
|
314
260
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const { experimental_output } = await generate(
|
|
318
|
-
contextResolverAgent,
|
|
319
|
-
[user("Generate a standalone question for this SQL query.")],
|
|
320
|
-
{
|
|
321
|
-
conversation: formatConversation(item.conversationContext),
|
|
322
|
-
sql: item.sql,
|
|
323
|
-
introspection
|
|
324
|
-
}
|
|
325
|
-
);
|
|
326
|
-
yield [
|
|
327
|
-
{
|
|
328
|
-
question: experimental_output.question,
|
|
329
|
-
sql: item.sql,
|
|
330
|
-
context: item.conversationContext,
|
|
331
|
-
success: item.success
|
|
332
|
-
}
|
|
333
|
-
];
|
|
334
|
-
}
|
|
261
|
+
list() {
|
|
262
|
+
return [...this.#cache.keys()];
|
|
335
263
|
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
// packages/text2sql/src/lib/synthesis/extractors/message-extractor.ts
|
|
339
|
-
var MessageExtractor = class extends PairProducer {
|
|
340
|
-
#messages;
|
|
341
|
-
#options;
|
|
342
264
|
/**
|
|
343
|
-
*
|
|
344
|
-
* @param
|
|
265
|
+
* Register a custom tokenizer for specific model families
|
|
266
|
+
* @param family - Model family name (e.g., "llama", "claude")
|
|
267
|
+
* @param tokenizer - Tokenizer implementation
|
|
345
268
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
this.#messages = messages;
|
|
349
|
-
this.#options = options;
|
|
269
|
+
registerTokenizer(family, tokenizer) {
|
|
270
|
+
this.#tokenizers.set(family, tokenizer);
|
|
350
271
|
}
|
|
351
272
|
/**
|
|
352
|
-
*
|
|
353
|
-
* @returns Pairs extracted from db_query tool invocations
|
|
273
|
+
* Set the default tokenizer used when no family-specific tokenizer is registered
|
|
354
274
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
if (getToolOrDynamicToolName2(part) !== toolName) {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const toolInput = "input" in part ? part.input : void 0;
|
|
372
|
-
if (!toolInput?.sql) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const success = part.state === "output-available";
|
|
376
|
-
const failed = part.state === "output-error";
|
|
377
|
-
if (failed && !includeFailures) {
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (!success && !failed) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const question = getMessageText(lastUserMessage);
|
|
384
|
-
if (!question) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
yield [
|
|
388
|
-
{
|
|
389
|
-
question,
|
|
390
|
-
sql: toolInput.sql,
|
|
391
|
-
success
|
|
392
|
-
}
|
|
393
|
-
];
|
|
394
|
-
}
|
|
275
|
+
setDefaultTokenizer(tokenizer) {
|
|
276
|
+
this.#defaultTokenizer = tokenizer;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the appropriate tokenizer for a model
|
|
280
|
+
*/
|
|
281
|
+
getTokenizer(modelId) {
|
|
282
|
+
const model = this.get(modelId);
|
|
283
|
+
if (model) {
|
|
284
|
+
const familyTokenizer = this.#tokenizers.get(model.family);
|
|
285
|
+
if (familyTokenizer) {
|
|
286
|
+
return familyTokenizer;
|
|
395
287
|
}
|
|
396
288
|
}
|
|
397
|
-
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// packages/text2sql/src/lib/synthesis/extractors/sql-extractor.ts
|
|
401
|
-
import { groq as groq2 } from "@ai-sdk/groq";
|
|
402
|
-
import dedent2 from "dedent";
|
|
403
|
-
import z2 from "zod";
|
|
404
|
-
import { agent as agent2, generate as generate2, user as user2 } from "@deepagents/agent";
|
|
405
|
-
var sqlToQuestionAgent = agent2({
|
|
406
|
-
name: "sql_to_question",
|
|
407
|
-
model: groq2("llama-3.3-70b-versatile"),
|
|
408
|
-
output: z2.object({
|
|
409
|
-
question: z2.string().describe("A natural language question that the SQL query answers")
|
|
410
|
-
}),
|
|
411
|
-
prompt: (state) => dedent2`
|
|
412
|
-
<identity>
|
|
413
|
-
You are an expert at understanding SQL queries and generating clear,
|
|
414
|
-
natural language questions that describe what the query retrieves.
|
|
415
|
-
</identity>
|
|
416
|
-
|
|
417
|
-
<schema>
|
|
418
|
-
${state?.introspection}
|
|
419
|
-
</schema>
|
|
420
|
-
|
|
421
|
-
<sql>
|
|
422
|
-
${state?.sql}
|
|
423
|
-
</sql>
|
|
424
|
-
|
|
425
|
-
<task>
|
|
426
|
-
Given the database schema and the SQL query above, generate a single
|
|
427
|
-
natural language question that:
|
|
428
|
-
1. Accurately describes what information the query retrieves
|
|
429
|
-
2. Uses natural business language (not SQL terminology)
|
|
430
|
-
3. Could be asked by a non-technical user
|
|
431
|
-
4. Is concise but complete
|
|
432
|
-
</task>
|
|
433
|
-
|
|
434
|
-
<examples>
|
|
435
|
-
SQL: SELECT COUNT(*) FROM customers WHERE region = 'NY'
|
|
436
|
-
Question: "How many customers do we have in New York?"
|
|
437
|
-
|
|
438
|
-
SQL: SELECT product_name, SUM(quantity) as total FROM orders GROUP BY product_name ORDER BY total DESC LIMIT 10
|
|
439
|
-
Question: "What are our top 10 products by quantity sold?"
|
|
440
|
-
|
|
441
|
-
SQL: SELECT c.name, COUNT(o.id) FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.id HAVING COUNT(o.id) = 0
|
|
442
|
-
Question: "Which customers have never placed an order?"
|
|
443
|
-
</examples>
|
|
444
|
-
`
|
|
445
|
-
});
|
|
446
|
-
var SqlExtractor = class extends PairProducer {
|
|
447
|
-
#sqls;
|
|
448
|
-
#adapter;
|
|
449
|
-
#options;
|
|
450
|
-
/**
|
|
451
|
-
* @param sql - SQL query or queries to generate questions for
|
|
452
|
-
* @param adapter - Database adapter for validation and schema introspection
|
|
453
|
-
* @param options - Extraction configuration
|
|
454
|
-
*/
|
|
455
|
-
constructor(sql, adapter, options = {}) {
|
|
456
|
-
super();
|
|
457
|
-
this.#sqls = Array.isArray(sql) ? sql : [sql];
|
|
458
|
-
this.#adapter = adapter;
|
|
459
|
-
this.#options = options;
|
|
289
|
+
return this.#defaultTokenizer;
|
|
460
290
|
}
|
|
461
291
|
/**
|
|
462
|
-
*
|
|
463
|
-
* @
|
|
292
|
+
* Estimate token count and cost for given text and model
|
|
293
|
+
* @param modelId - Model ID to use for pricing (e.g., "openai:gpt-4o")
|
|
294
|
+
* @param input - Input text (prompt)
|
|
464
295
|
*/
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (validateSql) {
|
|
471
|
-
const error = await this.#adapter.validate(sql);
|
|
472
|
-
isValid = error === void 0 || error === null;
|
|
473
|
-
if (!isValid && skipInvalid) {
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const { experimental_output } = await generate2(
|
|
478
|
-
sqlToQuestionAgent,
|
|
479
|
-
[user2("Generate a natural language question for this SQL query.")],
|
|
480
|
-
{
|
|
481
|
-
sql,
|
|
482
|
-
introspection
|
|
483
|
-
}
|
|
296
|
+
estimate(modelId, input) {
|
|
297
|
+
const model = this.get(modelId);
|
|
298
|
+
if (!model) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
484
301
|
);
|
|
485
|
-
yield [
|
|
486
|
-
{
|
|
487
|
-
question: experimental_output.question,
|
|
488
|
-
sql,
|
|
489
|
-
success: isValid
|
|
490
|
-
}
|
|
491
|
-
];
|
|
492
302
|
}
|
|
303
|
+
const tokenizer = this.getTokenizer(modelId);
|
|
304
|
+
const tokens = tokenizer.count(input);
|
|
305
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
306
|
+
return {
|
|
307
|
+
model: model.id,
|
|
308
|
+
provider: model.provider,
|
|
309
|
+
tokens,
|
|
310
|
+
cost,
|
|
311
|
+
limits: {
|
|
312
|
+
context: model.limit.context,
|
|
313
|
+
output: model.limit.output,
|
|
314
|
+
exceedsContext: tokens > model.limit.context
|
|
315
|
+
},
|
|
316
|
+
fragments: []
|
|
317
|
+
};
|
|
493
318
|
}
|
|
494
319
|
};
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
super(messages, adapter, options);
|
|
320
|
+
var _registry = null;
|
|
321
|
+
function getModelsRegistry() {
|
|
322
|
+
if (!_registry) {
|
|
323
|
+
_registry = new ModelsRegistry();
|
|
500
324
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
325
|
+
return _registry;
|
|
326
|
+
}
|
|
327
|
+
function isFragment(data) {
|
|
328
|
+
return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
|
|
329
|
+
}
|
|
330
|
+
function isFragmentObject(data) {
|
|
331
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
|
|
332
|
+
}
|
|
333
|
+
function isMessageFragment(fragment2) {
|
|
334
|
+
return fragment2.type === "message";
|
|
335
|
+
}
|
|
336
|
+
function fragment(name, ...children) {
|
|
337
|
+
return {
|
|
338
|
+
name,
|
|
339
|
+
data: children
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function user(content) {
|
|
343
|
+
const message2 = typeof content === "string" ? {
|
|
344
|
+
id: generateId(),
|
|
345
|
+
role: "user",
|
|
346
|
+
parts: [{ type: "text", text: content }]
|
|
347
|
+
} : content;
|
|
348
|
+
return {
|
|
349
|
+
id: message2.id,
|
|
350
|
+
name: "user",
|
|
351
|
+
data: "content",
|
|
352
|
+
type: "message",
|
|
353
|
+
persist: true,
|
|
354
|
+
codec: {
|
|
355
|
+
decode() {
|
|
356
|
+
return message2;
|
|
357
|
+
},
|
|
358
|
+
encode() {
|
|
359
|
+
return message2;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function assistant(message2) {
|
|
365
|
+
return {
|
|
366
|
+
id: message2.id,
|
|
367
|
+
name: "assistant",
|
|
368
|
+
data: "content",
|
|
369
|
+
type: "message",
|
|
370
|
+
persist: true,
|
|
371
|
+
codec: {
|
|
372
|
+
decode() {
|
|
373
|
+
return message2;
|
|
374
|
+
},
|
|
375
|
+
encode() {
|
|
376
|
+
return message2;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function message(content) {
|
|
382
|
+
const message2 = typeof content === "string" ? {
|
|
383
|
+
id: generateId(),
|
|
384
|
+
role: "user",
|
|
385
|
+
parts: [{ type: "text", text: content }]
|
|
386
|
+
} : content;
|
|
387
|
+
return {
|
|
388
|
+
id: message2.id,
|
|
389
|
+
name: message2.role,
|
|
390
|
+
data: "content",
|
|
391
|
+
type: "message",
|
|
392
|
+
persist: true,
|
|
393
|
+
codec: {
|
|
394
|
+
decode() {
|
|
395
|
+
return message2;
|
|
396
|
+
},
|
|
397
|
+
encode() {
|
|
398
|
+
return message2;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function assistantText(content, options) {
|
|
404
|
+
const id = options?.id ?? crypto.randomUUID();
|
|
405
|
+
return assistant({
|
|
406
|
+
id,
|
|
407
|
+
role: "assistant",
|
|
408
|
+
parts: [{ type: "text", text: content }]
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
var LAZY_ID = Symbol("lazy-id");
|
|
412
|
+
function isLazyFragment(fragment2) {
|
|
413
|
+
return LAZY_ID in fragment2;
|
|
414
|
+
}
|
|
415
|
+
var ContextRenderer = class {
|
|
416
|
+
options;
|
|
417
|
+
constructor(options = {}) {
|
|
418
|
+
this.options = options;
|
|
506
419
|
}
|
|
507
420
|
/**
|
|
508
|
-
*
|
|
421
|
+
* Check if data is a primitive (string, number, boolean).
|
|
509
422
|
*/
|
|
510
|
-
|
|
511
|
-
return
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
// packages/text2sql/src/lib/synthesis/extractors/windowed-context-extractor.ts
|
|
516
|
-
var WindowedContextExtractor = class extends BaseContextualExtractor {
|
|
517
|
-
windowSize;
|
|
518
|
-
constructor(messages, adapter, options) {
|
|
519
|
-
super(messages, adapter, options);
|
|
520
|
-
this.windowSize = options.windowSize;
|
|
423
|
+
isPrimitive(data) {
|
|
424
|
+
return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
|
|
521
425
|
}
|
|
522
426
|
/**
|
|
523
|
-
*
|
|
427
|
+
* Group fragments by name for groupFragments option.
|
|
524
428
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
429
|
+
groupByName(fragments) {
|
|
430
|
+
const groups = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const fragment2 of fragments) {
|
|
432
|
+
const existing = groups.get(fragment2.name) ?? [];
|
|
433
|
+
existing.push(fragment2);
|
|
434
|
+
groups.set(fragment2.name, existing);
|
|
435
|
+
}
|
|
436
|
+
return groups;
|
|
527
437
|
}
|
|
528
438
|
/**
|
|
529
|
-
*
|
|
439
|
+
* Remove null/undefined from fragments and fragment data recursively.
|
|
440
|
+
* This protects renderers from nullish values and ensures they are ignored
|
|
441
|
+
* consistently across all output formats.
|
|
530
442
|
*/
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
443
|
+
sanitizeFragments(fragments) {
|
|
444
|
+
const sanitized = [];
|
|
445
|
+
for (const fragment2 of fragments) {
|
|
446
|
+
const cleaned = this.sanitizeFragment(fragment2, /* @__PURE__ */ new WeakSet());
|
|
447
|
+
if (cleaned) {
|
|
448
|
+
sanitized.push(cleaned);
|
|
449
|
+
}
|
|
534
450
|
}
|
|
535
|
-
return
|
|
451
|
+
return sanitized;
|
|
536
452
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
model: groq3("openai/gpt-oss-20b"),
|
|
547
|
-
output: z3.object({
|
|
548
|
-
isTopicChange: z3.boolean().describe("Whether the new message represents a topic change"),
|
|
549
|
-
reason: z3.string().describe("Brief explanation for the decision")
|
|
550
|
-
}),
|
|
551
|
-
prompt: (state) => dedent3`
|
|
552
|
-
<identity>
|
|
553
|
-
You are an expert at understanding conversational flow and detecting topic changes.
|
|
554
|
-
</identity>
|
|
555
|
-
|
|
556
|
-
<conversation_context>
|
|
557
|
-
${state?.context || "(no prior context)"}
|
|
558
|
-
</conversation_context>
|
|
559
|
-
|
|
560
|
-
<new_message>
|
|
561
|
-
${state?.newMessage}
|
|
562
|
-
</new_message>
|
|
563
|
-
|
|
564
|
-
<task>
|
|
565
|
-
Determine if the new message represents a significant topic change from the
|
|
566
|
-
prior conversation context. A topic change occurs when:
|
|
567
|
-
1. The user asks about a completely different entity/table/domain
|
|
568
|
-
2. The user starts a new analytical question unrelated to prior discussion
|
|
569
|
-
3. There's a clear shift in what data or metrics are being discussed
|
|
570
|
-
|
|
571
|
-
NOT a topic change:
|
|
572
|
-
- Follow-up questions refining the same query ("filter by...", "sort by...")
|
|
573
|
-
- Questions about the same entities with different conditions
|
|
574
|
-
- Requests for more details on the same topic
|
|
575
|
-
</task>
|
|
576
|
-
|
|
577
|
-
<examples>
|
|
578
|
-
Context: "Show me customers in NY" → "Sort by revenue"
|
|
579
|
-
New: "Filter to those with orders over $1000"
|
|
580
|
-
Decision: NOT a topic change (still refining customer query)
|
|
581
|
-
|
|
582
|
-
Context: "Show me customers in NY" → "Sort by revenue"
|
|
583
|
-
New: "What were our total sales last quarter?"
|
|
584
|
-
Decision: Topic change (shifted from customers to sales metrics)
|
|
585
|
-
|
|
586
|
-
Context: "List all products"
|
|
587
|
-
New: "How many orders did we have last month?"
|
|
588
|
-
Decision: Topic change (products → orders/sales)
|
|
589
|
-
</examples>
|
|
590
|
-
`
|
|
591
|
-
});
|
|
592
|
-
var SegmentedContextExtractor = class extends BaseContextualExtractor {
|
|
593
|
-
constructor(messages, adapter, options = {}) {
|
|
594
|
-
super(messages, adapter, options);
|
|
453
|
+
sanitizeFragment(fragment2, seen) {
|
|
454
|
+
const data = this.sanitizeData(fragment2.data, seen);
|
|
455
|
+
if (data == null) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
...fragment2,
|
|
460
|
+
data
|
|
461
|
+
};
|
|
595
462
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
463
|
+
sanitizeData(data, seen) {
|
|
464
|
+
if (data == null) {
|
|
465
|
+
return void 0;
|
|
466
|
+
}
|
|
467
|
+
if (isFragment(data)) {
|
|
468
|
+
return this.sanitizeFragment(data, seen) ?? void 0;
|
|
469
|
+
}
|
|
470
|
+
if (Array.isArray(data)) {
|
|
471
|
+
if (seen.has(data)) {
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
seen.add(data);
|
|
475
|
+
const cleaned = [];
|
|
476
|
+
for (const item of data) {
|
|
477
|
+
const sanitizedItem = this.sanitizeData(item, seen);
|
|
478
|
+
if (sanitizedItem != null) {
|
|
479
|
+
cleaned.push(sanitizedItem);
|
|
480
|
+
}
|
|
611
481
|
}
|
|
482
|
+
return cleaned;
|
|
612
483
|
}
|
|
613
|
-
|
|
484
|
+
if (isFragmentObject(data)) {
|
|
485
|
+
if (seen.has(data)) {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
seen.add(data);
|
|
489
|
+
const cleaned = {};
|
|
490
|
+
for (const [key, value] of Object.entries(data)) {
|
|
491
|
+
const sanitizedValue = this.sanitizeData(value, seen);
|
|
492
|
+
if (sanitizedValue != null) {
|
|
493
|
+
cleaned[key] = sanitizedValue;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return cleaned;
|
|
497
|
+
}
|
|
498
|
+
return data;
|
|
614
499
|
}
|
|
615
500
|
/**
|
|
616
|
-
*
|
|
501
|
+
* Template method - dispatches value to appropriate handler.
|
|
617
502
|
*/
|
|
618
|
-
|
|
619
|
-
|
|
503
|
+
renderValue(key, value, ctx) {
|
|
504
|
+
if (value == null) {
|
|
505
|
+
return "";
|
|
506
|
+
}
|
|
507
|
+
if (isFragment(value)) {
|
|
508
|
+
return this.renderFragment(value, ctx);
|
|
509
|
+
}
|
|
510
|
+
if (Array.isArray(value)) {
|
|
511
|
+
return this.renderArray(key, value, ctx);
|
|
512
|
+
}
|
|
513
|
+
if (isFragmentObject(value)) {
|
|
514
|
+
return this.renderObject(key, value, ctx);
|
|
515
|
+
}
|
|
516
|
+
return this.renderPrimitive(key, String(value), ctx);
|
|
620
517
|
}
|
|
621
518
|
/**
|
|
622
|
-
*
|
|
623
|
-
* @param newMessage - The new user message to check
|
|
624
|
-
* @param contextSnapshot - Snapshot of context captured before this async call
|
|
519
|
+
* Render all entries of an object.
|
|
625
520
|
*/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
521
|
+
renderEntries(data, ctx) {
|
|
522
|
+
return Object.entries(data).map(([key, value]) => this.renderValue(key, value, ctx)).filter(Boolean);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var XmlRenderer = class extends ContextRenderer {
|
|
526
|
+
render(fragments) {
|
|
527
|
+
const sanitized = this.sanitizeFragments(fragments);
|
|
528
|
+
return sanitized.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
|
|
529
|
+
}
|
|
530
|
+
#renderTopLevel(fragment2) {
|
|
531
|
+
if (this.isPrimitive(fragment2.data)) {
|
|
532
|
+
return this.#leafRoot(fragment2.name, String(fragment2.data));
|
|
533
|
+
}
|
|
534
|
+
if (Array.isArray(fragment2.data)) {
|
|
535
|
+
return this.#renderArray(fragment2.name, fragment2.data, 0);
|
|
536
|
+
}
|
|
537
|
+
if (isFragment(fragment2.data)) {
|
|
538
|
+
const child = this.renderFragment(fragment2.data, { depth: 1, path: [] });
|
|
539
|
+
return this.#wrap(fragment2.name, [child]);
|
|
540
|
+
}
|
|
541
|
+
if (isFragmentObject(fragment2.data)) {
|
|
542
|
+
return this.#wrap(
|
|
543
|
+
fragment2.name,
|
|
544
|
+
this.renderEntries(fragment2.data, { depth: 1, path: [] })
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return "";
|
|
548
|
+
}
|
|
549
|
+
#renderArray(name, items, depth) {
|
|
550
|
+
const fragmentItems = items.filter(isFragment);
|
|
551
|
+
const nonFragmentItems = items.filter((item) => !isFragment(item));
|
|
552
|
+
const children = [];
|
|
553
|
+
for (const item of nonFragmentItems) {
|
|
554
|
+
if (item != null) {
|
|
555
|
+
if (isFragmentObject(item)) {
|
|
556
|
+
children.push(
|
|
557
|
+
this.#wrapIndented(
|
|
558
|
+
pluralize.singular(name),
|
|
559
|
+
this.renderEntries(item, { depth: depth + 2, path: [] }),
|
|
560
|
+
depth + 1
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
children.push(
|
|
565
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
633
568
|
}
|
|
634
|
-
|
|
635
|
-
|
|
569
|
+
}
|
|
570
|
+
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
571
|
+
const groups = this.groupByName(fragmentItems);
|
|
572
|
+
for (const [groupName, groupFragments] of groups) {
|
|
573
|
+
const groupChildren = groupFragments.map(
|
|
574
|
+
(frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
|
|
575
|
+
);
|
|
576
|
+
const pluralName = pluralize.plural(groupName);
|
|
577
|
+
children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
for (const frag of fragmentItems) {
|
|
581
|
+
children.push(
|
|
582
|
+
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return this.#wrap(name, children);
|
|
636
587
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
588
|
+
#leafRoot(tag, value) {
|
|
589
|
+
const safe = this.#escape(value);
|
|
590
|
+
if (safe.includes("\n")) {
|
|
591
|
+
return `<${tag}>
|
|
592
|
+
${this.#indent(safe, 2)}
|
|
593
|
+
</${tag}>`;
|
|
594
|
+
}
|
|
595
|
+
return `<${tag}>${safe}</${tag}>`;
|
|
596
|
+
}
|
|
597
|
+
renderFragment(fragment2, ctx) {
|
|
598
|
+
const { name, data } = fragment2;
|
|
599
|
+
if (this.isPrimitive(data)) {
|
|
600
|
+
return this.#leaf(name, String(data), ctx.depth);
|
|
601
|
+
}
|
|
602
|
+
if (isFragment(data)) {
|
|
603
|
+
const child = this.renderFragment(data, { ...ctx, depth: ctx.depth + 1 });
|
|
604
|
+
return this.#wrapIndented(name, [child], ctx.depth);
|
|
605
|
+
}
|
|
606
|
+
if (Array.isArray(data)) {
|
|
607
|
+
return this.#renderArrayIndented(name, data, ctx.depth);
|
|
608
|
+
}
|
|
609
|
+
if (isFragmentObject(data)) {
|
|
610
|
+
const children = this.renderEntries(data, {
|
|
611
|
+
...ctx,
|
|
612
|
+
depth: ctx.depth + 1
|
|
613
|
+
});
|
|
614
|
+
return this.#wrapIndented(name, children, ctx.depth);
|
|
615
|
+
}
|
|
616
|
+
return "";
|
|
617
|
+
}
|
|
618
|
+
#renderArrayIndented(name, items, depth) {
|
|
619
|
+
const fragmentItems = items.filter(isFragment);
|
|
620
|
+
const nonFragmentItems = items.filter((item) => !isFragment(item));
|
|
621
|
+
const children = [];
|
|
622
|
+
for (const item of nonFragmentItems) {
|
|
623
|
+
if (item != null) {
|
|
624
|
+
if (isFragmentObject(item)) {
|
|
625
|
+
children.push(
|
|
626
|
+
this.#wrapIndented(
|
|
627
|
+
pluralize.singular(name),
|
|
628
|
+
this.renderEntries(item, { depth: depth + 2, path: [] }),
|
|
629
|
+
depth + 1
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
} else {
|
|
633
|
+
children.push(
|
|
634
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
652
637
|
}
|
|
653
|
-
|
|
654
|
-
|
|
638
|
+
}
|
|
639
|
+
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
640
|
+
const groups = this.groupByName(fragmentItems);
|
|
641
|
+
for (const [groupName, groupFragments] of groups) {
|
|
642
|
+
const groupChildren = groupFragments.map(
|
|
643
|
+
(frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
|
|
644
|
+
);
|
|
645
|
+
const pluralName = pluralize.plural(groupName);
|
|
646
|
+
children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
for (const frag of fragmentItems) {
|
|
650
|
+
children.push(
|
|
651
|
+
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return this.#wrapIndented(name, children, depth);
|
|
656
|
+
}
|
|
657
|
+
renderPrimitive(key, value, ctx) {
|
|
658
|
+
return this.#leaf(key, value, ctx.depth);
|
|
659
|
+
}
|
|
660
|
+
renderArray(key, items, ctx) {
|
|
661
|
+
if (!items.length) {
|
|
662
|
+
return "";
|
|
663
|
+
}
|
|
664
|
+
const itemTag = pluralize.singular(key);
|
|
665
|
+
const children = items.filter((item) => item != null).map((item) => {
|
|
666
|
+
if (isFragment(item)) {
|
|
667
|
+
return this.renderFragment(item, { ...ctx, depth: ctx.depth + 1 });
|
|
668
|
+
}
|
|
669
|
+
if (isFragmentObject(item)) {
|
|
670
|
+
return this.#wrapIndented(
|
|
671
|
+
itemTag,
|
|
672
|
+
this.renderEntries(item, { ...ctx, depth: ctx.depth + 2 }),
|
|
673
|
+
ctx.depth + 1
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return this.#leaf(itemTag, String(item), ctx.depth + 1);
|
|
677
|
+
});
|
|
678
|
+
return this.#wrapIndented(key, children, ctx.depth);
|
|
679
|
+
}
|
|
680
|
+
renderObject(key, obj, ctx) {
|
|
681
|
+
const children = this.renderEntries(obj, { ...ctx, depth: ctx.depth + 1 });
|
|
682
|
+
return this.#wrapIndented(key, children, ctx.depth);
|
|
683
|
+
}
|
|
684
|
+
#escape(value) {
|
|
685
|
+
if (value == null) {
|
|
686
|
+
return "";
|
|
687
|
+
}
|
|
688
|
+
return value.replaceAll(/&/g, "&").replaceAll(/</g, "<").replaceAll(/>/g, ">").replaceAll(/"/g, """).replaceAll(/'/g, "'");
|
|
689
|
+
}
|
|
690
|
+
#indent(text, spaces) {
|
|
691
|
+
if (!text.trim()) {
|
|
692
|
+
return "";
|
|
693
|
+
}
|
|
694
|
+
const padding = " ".repeat(spaces);
|
|
695
|
+
return text.split("\n").map((line) => line.length ? padding + line : padding).join("\n");
|
|
696
|
+
}
|
|
697
|
+
#leaf(tag, value, depth) {
|
|
698
|
+
const safe = this.#escape(value);
|
|
699
|
+
const pad = " ".repeat(depth);
|
|
700
|
+
if (safe.includes("\n")) {
|
|
701
|
+
return `${pad}<${tag}>
|
|
702
|
+
${this.#indent(safe, (depth + 1) * 2)}
|
|
703
|
+
${pad}</${tag}>`;
|
|
704
|
+
}
|
|
705
|
+
return `${pad}<${tag}>${safe}</${tag}>`;
|
|
706
|
+
}
|
|
707
|
+
#wrap(tag, children) {
|
|
708
|
+
const content = children.filter(Boolean).join("\n");
|
|
709
|
+
if (!content) {
|
|
710
|
+
return "";
|
|
711
|
+
}
|
|
712
|
+
return `<${tag}>
|
|
713
|
+
${content}
|
|
714
|
+
</${tag}>`;
|
|
715
|
+
}
|
|
716
|
+
#wrapIndented(tag, children, depth) {
|
|
717
|
+
const content = children.filter(Boolean).join("\n");
|
|
718
|
+
if (!content) {
|
|
719
|
+
return "";
|
|
720
|
+
}
|
|
721
|
+
const pad = " ".repeat(depth);
|
|
722
|
+
return `${pad}<${tag}>
|
|
723
|
+
${content}
|
|
724
|
+
${pad}</${tag}>`;
|
|
655
725
|
}
|
|
656
726
|
};
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
727
|
+
var ContextStore = class {
|
|
728
|
+
};
|
|
729
|
+
var ContextEngine = class {
|
|
730
|
+
/** Non-message fragments (role, hints, etc.) - not persisted in graph */
|
|
731
|
+
#fragments = [];
|
|
732
|
+
/** Pending message fragments to be added to graph */
|
|
733
|
+
#pendingMessages = [];
|
|
734
|
+
#store;
|
|
735
|
+
#chatId;
|
|
736
|
+
#userId;
|
|
737
|
+
#branchName;
|
|
738
|
+
#branch = null;
|
|
739
|
+
#chatData = null;
|
|
740
|
+
#initialized = false;
|
|
741
|
+
constructor(options) {
|
|
742
|
+
if (!options.chatId) {
|
|
743
|
+
throw new Error("chatId is required");
|
|
744
|
+
}
|
|
745
|
+
if (!options.userId) {
|
|
746
|
+
throw new Error("userId is required");
|
|
747
|
+
}
|
|
748
|
+
this.#store = options.store;
|
|
749
|
+
this.#chatId = options.chatId;
|
|
750
|
+
this.#userId = options.userId;
|
|
751
|
+
this.#branchName = "main";
|
|
663
752
|
}
|
|
664
753
|
/**
|
|
665
|
-
*
|
|
754
|
+
* Initialize the chat and branch if they don't exist.
|
|
666
755
|
*/
|
|
667
|
-
async
|
|
668
|
-
this
|
|
756
|
+
async #ensureInitialized() {
|
|
757
|
+
if (this.#initialized) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.#chatData = await this.#store.upsertChat({
|
|
761
|
+
id: this.#chatId,
|
|
762
|
+
userId: this.#userId
|
|
763
|
+
});
|
|
764
|
+
this.#branch = await this.#store.getActiveBranch(this.#chatId);
|
|
765
|
+
this.#initialized = true;
|
|
669
766
|
}
|
|
670
767
|
/**
|
|
671
|
-
*
|
|
768
|
+
* Create a new branch from a specific message.
|
|
769
|
+
* Shared logic between rewind() and btw().
|
|
672
770
|
*/
|
|
673
|
-
|
|
674
|
-
|
|
771
|
+
async #createBranchFrom(messageId, switchTo) {
|
|
772
|
+
const branches = await this.#store.listBranches(this.#chatId);
|
|
773
|
+
const samePrefix = branches.filter(
|
|
774
|
+
(b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
|
|
775
|
+
);
|
|
776
|
+
const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
|
|
777
|
+
const newBranch = {
|
|
778
|
+
id: crypto.randomUUID(),
|
|
779
|
+
chatId: this.#chatId,
|
|
780
|
+
name: newBranchName,
|
|
781
|
+
headMessageId: messageId,
|
|
782
|
+
isActive: false,
|
|
783
|
+
createdAt: Date.now()
|
|
784
|
+
};
|
|
785
|
+
await this.#store.createBranch(newBranch);
|
|
786
|
+
if (switchTo) {
|
|
787
|
+
await this.#store.setActiveBranch(this.#chatId, newBranch.id);
|
|
788
|
+
this.#branch = { ...newBranch, isActive: true };
|
|
789
|
+
this.#branchName = newBranchName;
|
|
790
|
+
this.#pendingMessages = [];
|
|
791
|
+
}
|
|
792
|
+
const chain = await this.#store.getMessageChain(messageId);
|
|
793
|
+
return {
|
|
794
|
+
id: newBranch.id,
|
|
795
|
+
name: newBranch.name,
|
|
796
|
+
headMessageId: newBranch.headMessageId,
|
|
797
|
+
isActive: switchTo,
|
|
798
|
+
messageCount: chain.length,
|
|
799
|
+
createdAt: newBranch.createdAt
|
|
800
|
+
};
|
|
675
801
|
}
|
|
676
802
|
/**
|
|
677
|
-
*
|
|
803
|
+
* Get the current chat ID.
|
|
678
804
|
*/
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
const last = this.results.at(-1);
|
|
684
|
-
const { experimental_output } = await generate4(
|
|
685
|
-
contextResolverAgent,
|
|
686
|
-
[user4("Generate a standalone question for this SQL query.")],
|
|
687
|
-
{
|
|
688
|
-
conversation: formatConversation(last.conversationContext),
|
|
689
|
-
sql: last.sql,
|
|
690
|
-
introspection
|
|
691
|
-
}
|
|
692
|
-
);
|
|
693
|
-
yield [
|
|
694
|
-
{
|
|
695
|
-
question: experimental_output.question,
|
|
696
|
-
sql: last.sql,
|
|
697
|
-
context: last.conversationContext,
|
|
698
|
-
success: last.success
|
|
699
|
-
}
|
|
700
|
-
];
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
// packages/text2sql/src/lib/synthesis/synthesizers/schema-synthesizer.ts
|
|
705
|
-
import pLimit from "p-limit";
|
|
706
|
-
|
|
707
|
-
// packages/text2sql/src/lib/agents/question.agent.ts
|
|
708
|
-
import { groq as groq4 } from "@ai-sdk/groq";
|
|
709
|
-
import { defaultSettingsMiddleware, wrapLanguageModel } from "ai";
|
|
710
|
-
import dedent4 from "dedent";
|
|
711
|
-
import z4 from "zod";
|
|
712
|
-
import { agent as agent4, generate as generate5, user as user5 } from "@deepagents/agent";
|
|
713
|
-
var complexityInstructions = {
|
|
714
|
-
simple: dedent4`
|
|
715
|
-
Generate simple questions that require:
|
|
716
|
-
- Basic SELECT with single table
|
|
717
|
-
- Simple WHERE clauses with one condition
|
|
718
|
-
- COUNT(*) or basic aggregations
|
|
719
|
-
- No joins required
|
|
720
|
-
Examples: "How many customers do we have?", "List all products", "What is the total revenue?"
|
|
721
|
-
`,
|
|
722
|
-
moderate: dedent4`
|
|
723
|
-
Generate moderate questions that require:
|
|
724
|
-
- JOINs between 2-3 tables
|
|
725
|
-
- Multiple WHERE conditions (AND/OR)
|
|
726
|
-
- GROUP BY with HAVING clauses
|
|
727
|
-
- ORDER BY with LIMIT
|
|
728
|
-
- Basic subqueries
|
|
729
|
-
Examples: "What are the top 5 customers by total orders?", "Which products have never been ordered?"
|
|
730
|
-
`,
|
|
731
|
-
complex: dedent4`
|
|
732
|
-
Generate complex questions that require:
|
|
733
|
-
- Multiple JOINs (3+ tables)
|
|
734
|
-
- Nested subqueries or CTEs
|
|
735
|
-
- Complex aggregations with multiple GROUP BY columns
|
|
736
|
-
- CASE expressions
|
|
737
|
-
- Date/time calculations
|
|
738
|
-
Examples: "What is the month-over-month growth rate?", "Which customers have increased spending compared to last year?"
|
|
739
|
-
`,
|
|
740
|
-
"high complex": dedent4`
|
|
741
|
-
Generate highly complex questions that require advanced SQL features:
|
|
742
|
-
- Window functions (ROW_NUMBER, RANK, DENSE_RANK)
|
|
743
|
-
- LAG, LEAD for comparisons
|
|
744
|
-
- Running totals (SUM OVER)
|
|
745
|
-
- Moving averages
|
|
746
|
-
- PARTITION BY clauses
|
|
747
|
-
- Complex CTEs with multiple levels
|
|
748
|
-
Examples: "What is the running total of sales per month?", "Rank customers by their purchase frequency within each region"
|
|
749
|
-
`
|
|
750
|
-
};
|
|
751
|
-
var questionGeneratorAgent = agent4({
|
|
752
|
-
name: "question_generator",
|
|
753
|
-
model: wrapLanguageModel({
|
|
754
|
-
model: groq4("openai/gpt-oss-20b"),
|
|
755
|
-
middleware: defaultSettingsMiddleware({
|
|
756
|
-
settings: { temperature: 0.8, topP: 0.95 }
|
|
757
|
-
})
|
|
758
|
-
}),
|
|
759
|
-
handoffDescription: "Generates natural language questions that users might ask about the database schema.",
|
|
760
|
-
output: z4.object({
|
|
761
|
-
questions: z4.array(z4.string().describe("A natural language question about the data")).min(1).describe("List of natural language questions a user might ask")
|
|
762
|
-
}),
|
|
763
|
-
prompt: (state) => {
|
|
764
|
-
const count = state?.count;
|
|
765
|
-
const complexity = state?.complexity ?? "moderate";
|
|
766
|
-
return dedent4`
|
|
767
|
-
<identity>
|
|
768
|
-
You are a synthetic data generator specializing in creating realistic natural language questions
|
|
769
|
-
that users might ask about a database. You understand database schemas and can generate diverse,
|
|
770
|
-
practical questions that would require SQL queries to answer.
|
|
771
|
-
</identity>
|
|
772
|
-
|
|
773
|
-
${state?.introspection || ""}
|
|
774
|
-
|
|
775
|
-
<complexity level="${complexity}">
|
|
776
|
-
${complexityInstructions[complexity]}
|
|
777
|
-
</complexity>
|
|
778
|
-
|
|
779
|
-
<task>
|
|
780
|
-
Generate exactly ${count} natural language questions at the "${complexity}" complexity level.
|
|
781
|
-
The questions should:
|
|
782
|
-
1. Match the complexity requirements above
|
|
783
|
-
2. Use natural business language, not technical SQL terms
|
|
784
|
-
3. Be realistic questions a non-technical user would actually ask
|
|
785
|
-
4. Cover different tables and relationships when possible
|
|
786
|
-
</task>
|
|
787
|
-
|
|
788
|
-
<guardrails>
|
|
789
|
-
- Questions MUST ONLY reference tables and columns that exist in the schema above
|
|
790
|
-
- Before generating each question, verify that ALL entities (tables, columns, relationships) you reference are explicitly listed in the schema
|
|
791
|
-
- DO NOT invent or assume tables/columns that aren't explicitly shown in the schema
|
|
792
|
-
- Use natural language without SQL keywords like SELECT, WHERE, etc.
|
|
793
|
-
- All questions must match the specified complexity level
|
|
794
|
-
</guardrails>
|
|
795
|
-
`;
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
async function generateQuestions(params) {
|
|
799
|
-
const { introspection, complexity, count, prompt, model } = params;
|
|
800
|
-
const agentInstance = model ? questionGeneratorAgent.clone({ model }) : questionGeneratorAgent;
|
|
801
|
-
const userPrompt = prompt ?? `Generate ${count} questions at ${complexity} complexity given db schema.`;
|
|
802
|
-
const { experimental_output } = await generate5(
|
|
803
|
-
agentInstance,
|
|
804
|
-
[user5(userPrompt)],
|
|
805
|
-
{
|
|
806
|
-
introspection,
|
|
807
|
-
complexity,
|
|
808
|
-
count
|
|
809
|
-
}
|
|
810
|
-
);
|
|
811
|
-
return { questions: experimental_output.questions };
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// packages/text2sql/src/lib/agents/sql.agent.ts
|
|
815
|
-
import { groq as groq5 } from "@ai-sdk/groq";
|
|
816
|
-
import {
|
|
817
|
-
APICallError,
|
|
818
|
-
JSONParseError,
|
|
819
|
-
NoContentGeneratedError,
|
|
820
|
-
NoObjectGeneratedError,
|
|
821
|
-
NoOutputGeneratedError,
|
|
822
|
-
TypeValidationError,
|
|
823
|
-
defaultSettingsMiddleware as defaultSettingsMiddleware2,
|
|
824
|
-
wrapLanguageModel as wrapLanguageModel2
|
|
825
|
-
} from "ai";
|
|
826
|
-
import { Console } from "node:console";
|
|
827
|
-
import { createWriteStream } from "node:fs";
|
|
828
|
-
import pRetry from "p-retry";
|
|
829
|
-
import z5 from "zod";
|
|
830
|
-
import "@deepagents/agent";
|
|
831
|
-
|
|
832
|
-
// packages/context/dist/index.js
|
|
833
|
-
import { encode } from "gpt-tokenizer";
|
|
834
|
-
import { generateId } from "ai";
|
|
835
|
-
import pluralize from "pluralize";
|
|
836
|
-
import { titlecase } from "stringcase";
|
|
837
|
-
import { defineCommand } from "just-bash";
|
|
838
|
-
import spawn from "nano-spawn";
|
|
839
|
-
import "bash-tool";
|
|
840
|
-
import spawn2 from "nano-spawn";
|
|
841
|
-
import {
|
|
842
|
-
createBashTool
|
|
843
|
-
} from "bash-tool";
|
|
844
|
-
import YAML from "yaml";
|
|
845
|
-
import { DatabaseSync } from "node:sqlite";
|
|
846
|
-
import {
|
|
847
|
-
Output,
|
|
848
|
-
convertToModelMessages,
|
|
849
|
-
createUIMessageStream,
|
|
850
|
-
generateId as generateId2,
|
|
851
|
-
generateText,
|
|
852
|
-
smoothStream,
|
|
853
|
-
stepCountIs,
|
|
854
|
-
streamText
|
|
855
|
-
} from "ai";
|
|
856
|
-
import chalk from "chalk";
|
|
857
|
-
import "zod";
|
|
858
|
-
import "@deepagents/agent";
|
|
859
|
-
var defaultTokenizer = {
|
|
860
|
-
encode(text) {
|
|
861
|
-
return encode(text);
|
|
862
|
-
},
|
|
863
|
-
count(text) {
|
|
864
|
-
return encode(text).length;
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
var ModelsRegistry = class {
|
|
868
|
-
#cache = /* @__PURE__ */ new Map();
|
|
869
|
-
#loaded = false;
|
|
870
|
-
#tokenizers = /* @__PURE__ */ new Map();
|
|
871
|
-
#defaultTokenizer = defaultTokenizer;
|
|
872
|
-
/**
|
|
873
|
-
* Load models data from models.dev API
|
|
874
|
-
*/
|
|
875
|
-
async load() {
|
|
876
|
-
if (this.#loaded) return;
|
|
877
|
-
const response = await fetch("https://models.dev/api.json");
|
|
878
|
-
if (!response.ok) {
|
|
879
|
-
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
|
880
|
-
}
|
|
881
|
-
const data = await response.json();
|
|
882
|
-
for (const [providerId, provider] of Object.entries(data)) {
|
|
883
|
-
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
884
|
-
const info = {
|
|
885
|
-
id: model.id,
|
|
886
|
-
name: model.name,
|
|
887
|
-
family: model.family,
|
|
888
|
-
cost: model.cost,
|
|
889
|
-
limit: model.limit,
|
|
890
|
-
provider: providerId
|
|
891
|
-
};
|
|
892
|
-
this.#cache.set(`${providerId}:${modelId}`, info);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
this.#loaded = true;
|
|
805
|
+
get chatId() {
|
|
806
|
+
return this.#chatId;
|
|
896
807
|
}
|
|
897
808
|
/**
|
|
898
|
-
* Get
|
|
899
|
-
* @param modelId - Model ID (e.g., "openai:gpt-4o")
|
|
809
|
+
* Get the current branch name.
|
|
900
810
|
*/
|
|
901
|
-
get(
|
|
902
|
-
return this.#
|
|
811
|
+
get branch() {
|
|
812
|
+
return this.#branchName;
|
|
903
813
|
}
|
|
904
814
|
/**
|
|
905
|
-
*
|
|
815
|
+
* Get metadata for the current chat.
|
|
816
|
+
* Returns null if the chat hasn't been initialized yet.
|
|
906
817
|
*/
|
|
907
|
-
|
|
908
|
-
|
|
818
|
+
get chat() {
|
|
819
|
+
if (!this.#chatData) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
id: this.#chatData.id,
|
|
824
|
+
userId: this.#chatData.userId,
|
|
825
|
+
createdAt: this.#chatData.createdAt,
|
|
826
|
+
updatedAt: this.#chatData.updatedAt,
|
|
827
|
+
title: this.#chatData.title,
|
|
828
|
+
metadata: this.#chatData.metadata
|
|
829
|
+
};
|
|
909
830
|
}
|
|
910
831
|
/**
|
|
911
|
-
*
|
|
832
|
+
* Add fragments to the context.
|
|
833
|
+
*
|
|
834
|
+
* - Message fragments (user/assistant) are queued for persistence
|
|
835
|
+
* - Non-message fragments (role/hint) are kept in memory for system prompt
|
|
912
836
|
*/
|
|
913
|
-
|
|
914
|
-
|
|
837
|
+
set(...fragments) {
|
|
838
|
+
for (const fragment2 of fragments) {
|
|
839
|
+
if (isMessageFragment(fragment2)) {
|
|
840
|
+
this.#pendingMessages.push(fragment2);
|
|
841
|
+
} else {
|
|
842
|
+
this.#fragments.push(fragment2);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return this;
|
|
915
846
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
* @param family - Model family name (e.g., "llama", "claude")
|
|
919
|
-
* @param tokenizer - Tokenizer implementation
|
|
920
|
-
*/
|
|
921
|
-
registerTokenizer(family, tokenizer) {
|
|
922
|
-
this.#tokenizers.set(family, tokenizer);
|
|
847
|
+
// Unset a fragment by ID (not implemented yet)
|
|
848
|
+
unset(fragmentId) {
|
|
923
849
|
}
|
|
924
850
|
/**
|
|
925
|
-
*
|
|
851
|
+
* Render all fragments using the provided renderer.
|
|
852
|
+
* @internal Use resolve() instead for public API.
|
|
926
853
|
*/
|
|
927
|
-
|
|
928
|
-
this.#
|
|
854
|
+
render(renderer) {
|
|
855
|
+
return renderer.render(this.#fragments);
|
|
929
856
|
}
|
|
930
857
|
/**
|
|
931
|
-
*
|
|
858
|
+
* Resolve context into AI SDK-ready format.
|
|
859
|
+
*
|
|
860
|
+
* - Initializes chat and branch if needed
|
|
861
|
+
* - Loads message history from the graph (walking parent chain)
|
|
862
|
+
* - Separates context fragments for system prompt
|
|
863
|
+
* - Combines with pending messages
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```ts
|
|
867
|
+
* const context = new ContextEngine({ store, chatId: 'chat-1', userId: 'user-1' })
|
|
868
|
+
* .set(role('You are helpful'), user('Hello'));
|
|
869
|
+
*
|
|
870
|
+
* const { systemPrompt, messages } = await context.resolve();
|
|
871
|
+
* await generateText({ system: systemPrompt, messages });
|
|
872
|
+
* ```
|
|
932
873
|
*/
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
874
|
+
async resolve(options) {
|
|
875
|
+
await this.#ensureInitialized();
|
|
876
|
+
const systemPrompt = options.renderer.render(this.#fragments);
|
|
877
|
+
const messages = [];
|
|
878
|
+
if (this.#branch?.headMessageId) {
|
|
879
|
+
const chain = await this.#store.getMessageChain(
|
|
880
|
+
this.#branch.headMessageId
|
|
881
|
+
);
|
|
882
|
+
for (const msg of chain) {
|
|
883
|
+
messages.push(message(msg.data).codec?.decode());
|
|
939
884
|
}
|
|
940
885
|
}
|
|
941
|
-
|
|
886
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
887
|
+
const decoded = fragment2.codec.decode();
|
|
888
|
+
messages.push(decoded);
|
|
889
|
+
}
|
|
890
|
+
return { systemPrompt, messages };
|
|
942
891
|
}
|
|
943
892
|
/**
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
893
|
+
* Save pending messages to the graph.
|
|
894
|
+
*
|
|
895
|
+
* Each message is added as a node with parentId pointing to the previous message.
|
|
896
|
+
* The branch head is updated to point to the last message.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* ```ts
|
|
900
|
+
* context.set(user('Hello'));
|
|
901
|
+
* // AI responds...
|
|
902
|
+
* context.set(assistant('Hi there!'));
|
|
903
|
+
* await context.save(); // Persist to graph
|
|
904
|
+
* ```
|
|
947
905
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
953
|
-
);
|
|
906
|
+
async save() {
|
|
907
|
+
await this.#ensureInitialized();
|
|
908
|
+
if (this.#pendingMessages.length === 0) {
|
|
909
|
+
return;
|
|
954
910
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
model: model.id,
|
|
960
|
-
provider: model.provider,
|
|
961
|
-
tokens,
|
|
962
|
-
cost,
|
|
963
|
-
limits: {
|
|
964
|
-
context: model.limit.context,
|
|
965
|
-
output: model.limit.output,
|
|
966
|
-
exceedsContext: tokens > model.limit.context
|
|
967
|
-
},
|
|
968
|
-
fragments: []
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
var _registry = null;
|
|
973
|
-
function getModelsRegistry() {
|
|
974
|
-
if (!_registry) {
|
|
975
|
-
_registry = new ModelsRegistry();
|
|
976
|
-
}
|
|
977
|
-
return _registry;
|
|
978
|
-
}
|
|
979
|
-
function isFragment(data) {
|
|
980
|
-
return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
|
|
981
|
-
}
|
|
982
|
-
function isFragmentObject(data) {
|
|
983
|
-
return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
|
|
984
|
-
}
|
|
985
|
-
function isMessageFragment(fragment2) {
|
|
986
|
-
return fragment2.type === "message";
|
|
987
|
-
}
|
|
988
|
-
function role(content) {
|
|
989
|
-
return {
|
|
990
|
-
name: "role",
|
|
991
|
-
data: content
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
function user6(content) {
|
|
995
|
-
const message2 = typeof content === "string" ? {
|
|
996
|
-
id: generateId(),
|
|
997
|
-
role: "user",
|
|
998
|
-
parts: [{ type: "text", text: content }]
|
|
999
|
-
} : content;
|
|
1000
|
-
return {
|
|
1001
|
-
id: message2.id,
|
|
1002
|
-
name: "user",
|
|
1003
|
-
data: "content",
|
|
1004
|
-
type: "message",
|
|
1005
|
-
persist: true,
|
|
1006
|
-
codec: {
|
|
1007
|
-
decode() {
|
|
1008
|
-
return message2;
|
|
1009
|
-
},
|
|
1010
|
-
encode() {
|
|
1011
|
-
return message2;
|
|
911
|
+
for (let i = 0; i < this.#pendingMessages.length; i++) {
|
|
912
|
+
const fragment2 = this.#pendingMessages[i];
|
|
913
|
+
if (isLazyFragment(fragment2)) {
|
|
914
|
+
this.#pendingMessages[i] = await this.#resolveLazyFragment(fragment2);
|
|
1012
915
|
}
|
|
1013
916
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
codec: {
|
|
1029
|
-
decode() {
|
|
1030
|
-
return message2;
|
|
1031
|
-
},
|
|
1032
|
-
encode() {
|
|
1033
|
-
return message2;
|
|
1034
|
-
}
|
|
917
|
+
let parentId = this.#branch.headMessageId;
|
|
918
|
+
const now = Date.now();
|
|
919
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
920
|
+
const messageData = {
|
|
921
|
+
id: fragment2.id ?? crypto.randomUUID(),
|
|
922
|
+
chatId: this.#chatId,
|
|
923
|
+
parentId,
|
|
924
|
+
name: fragment2.name,
|
|
925
|
+
type: fragment2.type,
|
|
926
|
+
data: fragment2.codec.encode(),
|
|
927
|
+
createdAt: now
|
|
928
|
+
};
|
|
929
|
+
await this.#store.addMessage(messageData);
|
|
930
|
+
parentId = messageData.id;
|
|
1035
931
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
options;
|
|
1040
|
-
constructor(options = {}) {
|
|
1041
|
-
this.options = options;
|
|
932
|
+
await this.#store.updateBranchHead(this.#branch.id, parentId);
|
|
933
|
+
this.#branch.headMessageId = parentId;
|
|
934
|
+
this.#pendingMessages = [];
|
|
1042
935
|
}
|
|
1043
936
|
/**
|
|
1044
|
-
*
|
|
937
|
+
* Resolve a lazy fragment by finding the appropriate ID.
|
|
1045
938
|
*/
|
|
1046
|
-
|
|
1047
|
-
|
|
939
|
+
async #resolveLazyFragment(fragment2) {
|
|
940
|
+
const lazy = fragment2[LAZY_ID];
|
|
941
|
+
if (lazy.type === "last-assistant") {
|
|
942
|
+
const lastId = await this.#getLastAssistantId();
|
|
943
|
+
return assistantText(lazy.content, { id: lastId ?? crypto.randomUUID() });
|
|
944
|
+
}
|
|
945
|
+
throw new Error(`Unknown lazy fragment type: ${lazy.type}`);
|
|
1048
946
|
}
|
|
1049
947
|
/**
|
|
1050
|
-
*
|
|
948
|
+
* Find the most recent assistant message ID (pending or persisted).
|
|
1051
949
|
*/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
950
|
+
async #getLastAssistantId() {
|
|
951
|
+
for (let i = this.#pendingMessages.length - 1; i >= 0; i--) {
|
|
952
|
+
const msg = this.#pendingMessages[i];
|
|
953
|
+
if (msg.name === "assistant" && !isLazyFragment(msg)) {
|
|
954
|
+
return msg.id;
|
|
955
|
+
}
|
|
1058
956
|
}
|
|
1059
|
-
|
|
957
|
+
if (this.#branch?.headMessageId) {
|
|
958
|
+
const chain = await this.#store.getMessageChain(
|
|
959
|
+
this.#branch.headMessageId
|
|
960
|
+
);
|
|
961
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
962
|
+
if (chain[i].name === "assistant") {
|
|
963
|
+
return chain[i].id;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return void 0;
|
|
1060
968
|
}
|
|
1061
969
|
/**
|
|
1062
|
-
*
|
|
970
|
+
* Estimate token count and cost for the full context.
|
|
971
|
+
*
|
|
972
|
+
* Includes:
|
|
973
|
+
* - System prompt fragments (role, hints, etc.)
|
|
974
|
+
* - Persisted chat messages (from store)
|
|
975
|
+
* - Pending messages (not yet saved)
|
|
976
|
+
*
|
|
977
|
+
* @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
|
|
978
|
+
* @param options - Optional settings
|
|
979
|
+
* @returns Estimate result with token counts, costs, and per-fragment breakdown
|
|
1063
980
|
*/
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
981
|
+
async estimate(modelId, options = {}) {
|
|
982
|
+
await this.#ensureInitialized();
|
|
983
|
+
const renderer = options.renderer ?? new XmlRenderer();
|
|
984
|
+
const registry = getModelsRegistry();
|
|
985
|
+
await registry.load();
|
|
986
|
+
const model = registry.get(modelId);
|
|
987
|
+
if (!model) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
990
|
+
);
|
|
1067
991
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
992
|
+
const tokenizer = registry.getTokenizer(modelId);
|
|
993
|
+
const fragmentEstimates = [];
|
|
994
|
+
for (const fragment2 of this.#fragments) {
|
|
995
|
+
const rendered = renderer.render([fragment2]);
|
|
996
|
+
const tokens = tokenizer.count(rendered);
|
|
997
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
998
|
+
fragmentEstimates.push({
|
|
999
|
+
id: fragment2.id,
|
|
1000
|
+
name: fragment2.name,
|
|
1001
|
+
tokens,
|
|
1002
|
+
cost
|
|
1003
|
+
});
|
|
1070
1004
|
}
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1005
|
+
if (this.#branch?.headMessageId) {
|
|
1006
|
+
const chain = await this.#store.getMessageChain(
|
|
1007
|
+
this.#branch.headMessageId
|
|
1008
|
+
);
|
|
1009
|
+
for (const msg of chain) {
|
|
1010
|
+
const content = String(msg.data);
|
|
1011
|
+
const tokens = tokenizer.count(content);
|
|
1012
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
1013
|
+
fragmentEstimates.push({
|
|
1014
|
+
name: msg.name,
|
|
1015
|
+
id: msg.id,
|
|
1016
|
+
tokens,
|
|
1017
|
+
cost
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1073
1020
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1021
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1022
|
+
const content = String(fragment2.data);
|
|
1023
|
+
const tokens = tokenizer.count(content);
|
|
1024
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
1025
|
+
fragmentEstimates.push({
|
|
1026
|
+
name: fragment2.name,
|
|
1027
|
+
id: fragment2.id,
|
|
1028
|
+
tokens,
|
|
1029
|
+
cost
|
|
1030
|
+
});
|
|
1076
1031
|
}
|
|
1077
|
-
|
|
1032
|
+
const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
|
|
1033
|
+
const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
|
|
1034
|
+
return {
|
|
1035
|
+
model: model.id,
|
|
1036
|
+
provider: model.provider,
|
|
1037
|
+
tokens: totalTokens,
|
|
1038
|
+
cost: totalCost,
|
|
1039
|
+
limits: {
|
|
1040
|
+
context: model.limit.context,
|
|
1041
|
+
output: model.limit.output,
|
|
1042
|
+
exceedsContext: totalTokens > model.limit.context
|
|
1043
|
+
},
|
|
1044
|
+
fragments: fragmentEstimates
|
|
1045
|
+
};
|
|
1078
1046
|
}
|
|
1079
1047
|
/**
|
|
1080
|
-
*
|
|
1048
|
+
* Rewind to a specific message by ID.
|
|
1049
|
+
*
|
|
1050
|
+
* Creates a new branch from that message, preserving the original branch.
|
|
1051
|
+
* The new branch becomes active.
|
|
1052
|
+
*
|
|
1053
|
+
* @param messageId - The message ID to rewind to
|
|
1054
|
+
* @returns The new branch info
|
|
1055
|
+
*
|
|
1056
|
+
* @example
|
|
1057
|
+
* ```ts
|
|
1058
|
+
* context.set(user('What is 2 + 2?', { id: 'q1' }));
|
|
1059
|
+
* context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
|
|
1060
|
+
* await context.save();
|
|
1061
|
+
*
|
|
1062
|
+
* // Rewind to the question, creates new branch
|
|
1063
|
+
* const newBranch = await context.rewind('q1');
|
|
1064
|
+
*
|
|
1065
|
+
* // Now add correct answer on new branch
|
|
1066
|
+
* context.set(assistant('The answer is 4.'));
|
|
1067
|
+
* await context.save();
|
|
1068
|
+
* ```
|
|
1081
1069
|
*/
|
|
1082
|
-
|
|
1083
|
-
|
|
1070
|
+
async rewind(messageId) {
|
|
1071
|
+
await this.#ensureInitialized();
|
|
1072
|
+
const message2 = await this.#store.getMessage(messageId);
|
|
1073
|
+
if (!message2) {
|
|
1074
|
+
throw new Error(`Message "${messageId}" not found`);
|
|
1075
|
+
}
|
|
1076
|
+
if (message2.chatId !== this.#chatId) {
|
|
1077
|
+
throw new Error(`Message "${messageId}" belongs to a different chat`);
|
|
1078
|
+
}
|
|
1079
|
+
return this.#createBranchFrom(messageId, true);
|
|
1084
1080
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1081
|
+
/**
|
|
1082
|
+
* Create a checkpoint at the current position.
|
|
1083
|
+
*
|
|
1084
|
+
* A checkpoint is a named pointer to the current branch head.
|
|
1085
|
+
* Use restore() to return to this point later.
|
|
1086
|
+
*
|
|
1087
|
+
* @param name - Name for the checkpoint
|
|
1088
|
+
* @returns The checkpoint info
|
|
1089
|
+
*
|
|
1090
|
+
* @example
|
|
1091
|
+
* ```ts
|
|
1092
|
+
* context.set(user('I want to learn a new skill.'));
|
|
1093
|
+
* context.set(assistant('Would you like coding or cooking?'));
|
|
1094
|
+
* await context.save();
|
|
1095
|
+
*
|
|
1096
|
+
* // Save checkpoint before user's choice
|
|
1097
|
+
* const cp = await context.checkpoint('before-choice');
|
|
1098
|
+
* ```
|
|
1099
|
+
*/
|
|
1100
|
+
async checkpoint(name) {
|
|
1101
|
+
await this.#ensureInitialized();
|
|
1102
|
+
if (!this.#branch?.headMessageId) {
|
|
1103
|
+
throw new Error("Cannot create checkpoint: no messages in conversation");
|
|
1100
1104
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
+
const checkpoint = {
|
|
1106
|
+
id: crypto.randomUUID(),
|
|
1107
|
+
chatId: this.#chatId,
|
|
1108
|
+
name,
|
|
1109
|
+
messageId: this.#branch.headMessageId,
|
|
1110
|
+
createdAt: Date.now()
|
|
1111
|
+
};
|
|
1112
|
+
await this.#store.createCheckpoint(checkpoint);
|
|
1113
|
+
return {
|
|
1114
|
+
id: checkpoint.id,
|
|
1115
|
+
name: checkpoint.name,
|
|
1116
|
+
messageId: checkpoint.messageId,
|
|
1117
|
+
createdAt: checkpoint.createdAt
|
|
1118
|
+
};
|
|
1105
1119
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
1130
|
-
);
|
|
1131
|
-
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Restore to a checkpoint by creating a new branch from that point.
|
|
1122
|
+
*
|
|
1123
|
+
* @param name - Name of the checkpoint to restore
|
|
1124
|
+
* @returns The new branch info
|
|
1125
|
+
*
|
|
1126
|
+
* @example
|
|
1127
|
+
* ```ts
|
|
1128
|
+
* // User chose cooking, but wants to try coding path
|
|
1129
|
+
* await context.restore('before-choice');
|
|
1130
|
+
*
|
|
1131
|
+
* context.set(user('I want to learn coding.'));
|
|
1132
|
+
* context.set(assistant('Python is a great starting language!'));
|
|
1133
|
+
* await context.save();
|
|
1134
|
+
* ```
|
|
1135
|
+
*/
|
|
1136
|
+
async restore(name) {
|
|
1137
|
+
await this.#ensureInitialized();
|
|
1138
|
+
const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
|
|
1139
|
+
if (!checkpoint) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`Checkpoint "${name}" not found in chat "${this.#chatId}"`
|
|
1142
|
+
);
|
|
1132
1143
|
}
|
|
1133
|
-
return this
|
|
1144
|
+
return this.rewind(checkpoint.messageId);
|
|
1134
1145
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Switch to a different branch by name.
|
|
1148
|
+
*
|
|
1149
|
+
* @param name - Branch name to switch to
|
|
1150
|
+
*
|
|
1151
|
+
* @example
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* // List branches (via store)
|
|
1154
|
+
* const branches = await store.listBranches(context.chatId);
|
|
1155
|
+
* console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
|
|
1156
|
+
*
|
|
1157
|
+
* // Switch to original branch
|
|
1158
|
+
* await context.switchBranch('main');
|
|
1159
|
+
* ```
|
|
1160
|
+
*/
|
|
1161
|
+
async switchBranch(name) {
|
|
1162
|
+
await this.#ensureInitialized();
|
|
1163
|
+
const branch = await this.#store.getBranch(this.#chatId, name);
|
|
1164
|
+
if (!branch) {
|
|
1165
|
+
throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
|
|
1141
1166
|
}
|
|
1142
|
-
|
|
1167
|
+
await this.#store.setActiveBranch(this.#chatId, branch.id);
|
|
1168
|
+
this.#branch = { ...branch, isActive: true };
|
|
1169
|
+
this.#branchName = name;
|
|
1170
|
+
this.#pendingMessages = [];
|
|
1143
1171
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1172
|
+
/**
|
|
1173
|
+
* Create a parallel branch from the current position ("by the way").
|
|
1174
|
+
*
|
|
1175
|
+
* Use this when you want to fork the conversation without leaving
|
|
1176
|
+
* the current branch. Common use case: user wants to ask another
|
|
1177
|
+
* question while waiting for the model to respond.
|
|
1178
|
+
*
|
|
1179
|
+
* Unlike rewind(), this method:
|
|
1180
|
+
* - Uses the current HEAD (no messageId needed)
|
|
1181
|
+
* - Does NOT switch to the new branch
|
|
1182
|
+
* - Keeps pending messages intact
|
|
1183
|
+
*
|
|
1184
|
+
* @returns The new branch info (does not switch to it)
|
|
1185
|
+
* @throws Error if no messages exist in the conversation
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```ts
|
|
1189
|
+
* // User asked a question, model is generating...
|
|
1190
|
+
* context.set(user('What is the weather?'));
|
|
1191
|
+
* await context.save();
|
|
1192
|
+
*
|
|
1193
|
+
* // User wants to ask something else without waiting
|
|
1194
|
+
* const newBranch = await context.btw();
|
|
1195
|
+
* // newBranch = { name: 'main-v2', ... }
|
|
1196
|
+
*
|
|
1197
|
+
* // Later, switch to the new branch and add the question
|
|
1198
|
+
* await context.switchBranch(newBranch.name);
|
|
1199
|
+
* context.set(user('Also, what time is it?'));
|
|
1200
|
+
* await context.save();
|
|
1201
|
+
* ```
|
|
1202
|
+
*/
|
|
1203
|
+
async btw() {
|
|
1204
|
+
await this.#ensureInitialized();
|
|
1205
|
+
if (!this.#branch?.headMessageId) {
|
|
1206
|
+
throw new Error("Cannot create btw branch: no messages in conversation");
|
|
1155
1207
|
}
|
|
1156
|
-
|
|
1157
|
-
return this.#wrapIndented(name, children, ctx.depth);
|
|
1208
|
+
return this.#createBranchFrom(this.#branch.headMessageId, false);
|
|
1158
1209
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1210
|
+
/**
|
|
1211
|
+
* Update metadata for the current chat.
|
|
1212
|
+
*
|
|
1213
|
+
* @param updates - Partial metadata to merge (title, metadata)
|
|
1214
|
+
*
|
|
1215
|
+
* @example
|
|
1216
|
+
* ```ts
|
|
1217
|
+
* await context.updateChat({
|
|
1218
|
+
* title: 'Coding Help Session',
|
|
1219
|
+
* metadata: { tags: ['python', 'debugging'] }
|
|
1220
|
+
* });
|
|
1221
|
+
* ```
|
|
1222
|
+
*/
|
|
1223
|
+
async updateChat(updates) {
|
|
1224
|
+
await this.#ensureInitialized();
|
|
1225
|
+
const storeUpdates = {};
|
|
1226
|
+
if (updates.title !== void 0) {
|
|
1227
|
+
storeUpdates.title = updates.title;
|
|
1169
1228
|
}
|
|
1170
|
-
if (
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
);
|
|
1176
|
-
const pluralName = pluralize.plural(groupName);
|
|
1177
|
-
children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
|
|
1178
|
-
}
|
|
1179
|
-
} else {
|
|
1180
|
-
for (const frag of fragmentItems) {
|
|
1181
|
-
children.push(
|
|
1182
|
-
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1229
|
+
if (updates.metadata !== void 0) {
|
|
1230
|
+
storeUpdates.metadata = {
|
|
1231
|
+
...this.#chatData?.metadata,
|
|
1232
|
+
...updates.metadata
|
|
1233
|
+
};
|
|
1185
1234
|
}
|
|
1186
|
-
|
|
1235
|
+
this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
|
|
1187
1236
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* Consolidate context fragments (no-op for now).
|
|
1239
|
+
*
|
|
1240
|
+
* This is a placeholder for future functionality that merges context fragments
|
|
1241
|
+
* using specific rules. Currently, it does nothing.
|
|
1242
|
+
*
|
|
1243
|
+
* @experimental
|
|
1244
|
+
*/
|
|
1245
|
+
consolidate() {
|
|
1246
|
+
return void 0;
|
|
1190
1247
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1248
|
+
/**
|
|
1249
|
+
* Extract skill path mappings from available_skills fragments.
|
|
1250
|
+
* Returns array of { host, sandbox } for mounting in sandbox filesystem.
|
|
1251
|
+
*
|
|
1252
|
+
* Reads the original `paths` configuration stored in fragment metadata
|
|
1253
|
+
* by the skills() fragment helper.
|
|
1254
|
+
*
|
|
1255
|
+
* @example
|
|
1256
|
+
* ```ts
|
|
1257
|
+
* const context = new ContextEngine({ store, chatId, userId })
|
|
1258
|
+
* .set(skills({ paths: [{ host: './skills', sandbox: '/skills' }] }));
|
|
1259
|
+
*
|
|
1260
|
+
* const mounts = context.getSkillMounts();
|
|
1261
|
+
* // [{ host: './skills', sandbox: '/skills' }]
|
|
1262
|
+
* ```
|
|
1263
|
+
*/
|
|
1264
|
+
getSkillMounts() {
|
|
1265
|
+
const mounts = [];
|
|
1266
|
+
for (const fragment2 of this.#fragments) {
|
|
1267
|
+
if (fragment2.name === "available_skills" && fragment2.metadata && Array.isArray(fragment2.metadata.paths)) {
|
|
1268
|
+
for (const mapping of fragment2.metadata.paths) {
|
|
1269
|
+
if (typeof mapping === "object" && mapping !== null && typeof mapping.host === "string" && typeof mapping.sandbox === "string") {
|
|
1270
|
+
mounts.push({ host: mapping.host, sandbox: mapping.sandbox });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1194
1274
|
}
|
|
1195
|
-
|
|
1196
|
-
const children = items.filter((item) => item != null).map((item) => this.#leaf(itemTag, String(item), ctx.depth + 1));
|
|
1197
|
-
return this.#wrapIndented(key, children, ctx.depth);
|
|
1198
|
-
}
|
|
1199
|
-
renderObject(key, obj, ctx) {
|
|
1200
|
-
const children = this.renderEntries(obj, { ...ctx, depth: ctx.depth + 1 });
|
|
1201
|
-
return this.#wrapIndented(key, children, ctx.depth);
|
|
1202
|
-
}
|
|
1203
|
-
#escape(value) {
|
|
1204
|
-
if (value == null) {
|
|
1205
|
-
return "";
|
|
1206
|
-
}
|
|
1207
|
-
return value.replaceAll(/&/g, "&").replaceAll(/</g, "<").replaceAll(/>/g, ">").replaceAll(/"/g, """).replaceAll(/'/g, "'");
|
|
1208
|
-
}
|
|
1209
|
-
#indent(text, spaces) {
|
|
1210
|
-
if (!text.trim()) {
|
|
1211
|
-
return "";
|
|
1212
|
-
}
|
|
1213
|
-
const padding = " ".repeat(spaces);
|
|
1214
|
-
return text.split("\n").map((line) => line.length ? padding + line : padding).join("\n");
|
|
1215
|
-
}
|
|
1216
|
-
#leaf(tag, value, depth) {
|
|
1217
|
-
const safe = this.#escape(value);
|
|
1218
|
-
const pad = " ".repeat(depth);
|
|
1219
|
-
if (safe.includes("\n")) {
|
|
1220
|
-
return `${pad}<${tag}>
|
|
1221
|
-
${this.#indent(safe, (depth + 1) * 2)}
|
|
1222
|
-
${pad}</${tag}>`;
|
|
1223
|
-
}
|
|
1224
|
-
return `${pad}<${tag}>${safe}</${tag}>`;
|
|
1225
|
-
}
|
|
1226
|
-
#wrap(tag, children) {
|
|
1227
|
-
const content = children.filter(Boolean).join("\n");
|
|
1228
|
-
if (!content) {
|
|
1229
|
-
return "";
|
|
1230
|
-
}
|
|
1231
|
-
return `<${tag}>
|
|
1232
|
-
${content}
|
|
1233
|
-
</${tag}>`;
|
|
1234
|
-
}
|
|
1235
|
-
#wrapIndented(tag, children, depth) {
|
|
1236
|
-
const content = children.filter(Boolean).join("\n");
|
|
1237
|
-
if (!content) {
|
|
1238
|
-
return "";
|
|
1239
|
-
}
|
|
1240
|
-
const pad = " ".repeat(depth);
|
|
1241
|
-
return `${pad}<${tag}>
|
|
1242
|
-
${content}
|
|
1243
|
-
${pad}</${tag}>`;
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
var ContextStore = class {
|
|
1247
|
-
};
|
|
1248
|
-
var ContextEngine = class {
|
|
1249
|
-
/** Non-message fragments (role, hints, etc.) - not persisted in graph */
|
|
1250
|
-
#fragments = [];
|
|
1251
|
-
/** Pending message fragments to be added to graph */
|
|
1252
|
-
#pendingMessages = [];
|
|
1253
|
-
#store;
|
|
1254
|
-
#chatId;
|
|
1255
|
-
#branchName;
|
|
1256
|
-
#branch = null;
|
|
1257
|
-
#chatData = null;
|
|
1258
|
-
#initialized = false;
|
|
1259
|
-
constructor(options) {
|
|
1260
|
-
if (!options.chatId) {
|
|
1261
|
-
throw new Error("chatId is required");
|
|
1262
|
-
}
|
|
1263
|
-
this.#store = options.store;
|
|
1264
|
-
this.#chatId = options.chatId;
|
|
1265
|
-
this.#branchName = options.branch ?? "main";
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Initialize the chat and branch if they don't exist.
|
|
1269
|
-
*/
|
|
1270
|
-
async #ensureInitialized() {
|
|
1271
|
-
if (this.#initialized) {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
|
|
1275
|
-
const existingBranch = await this.#store.getBranch(
|
|
1276
|
-
this.#chatId,
|
|
1277
|
-
this.#branchName
|
|
1278
|
-
);
|
|
1279
|
-
if (existingBranch) {
|
|
1280
|
-
this.#branch = existingBranch;
|
|
1281
|
-
} else {
|
|
1282
|
-
this.#branch = {
|
|
1283
|
-
id: crypto.randomUUID(),
|
|
1284
|
-
chatId: this.#chatId,
|
|
1285
|
-
name: this.#branchName,
|
|
1286
|
-
headMessageId: null,
|
|
1287
|
-
isActive: true,
|
|
1288
|
-
createdAt: Date.now()
|
|
1289
|
-
};
|
|
1290
|
-
await this.#store.createBranch(this.#branch);
|
|
1291
|
-
}
|
|
1292
|
-
this.#initialized = true;
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Create a new branch from a specific message.
|
|
1296
|
-
* Shared logic between rewind() and btw().
|
|
1297
|
-
*/
|
|
1298
|
-
async #createBranchFrom(messageId, switchTo) {
|
|
1299
|
-
const branches = await this.#store.listBranches(this.#chatId);
|
|
1300
|
-
const samePrefix = branches.filter(
|
|
1301
|
-
(b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
|
|
1302
|
-
);
|
|
1303
|
-
const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
|
|
1304
|
-
const newBranch = {
|
|
1305
|
-
id: crypto.randomUUID(),
|
|
1306
|
-
chatId: this.#chatId,
|
|
1307
|
-
name: newBranchName,
|
|
1308
|
-
headMessageId: messageId,
|
|
1309
|
-
isActive: false,
|
|
1310
|
-
createdAt: Date.now()
|
|
1311
|
-
};
|
|
1312
|
-
await this.#store.createBranch(newBranch);
|
|
1313
|
-
if (switchTo) {
|
|
1314
|
-
await this.#store.setActiveBranch(this.#chatId, newBranch.id);
|
|
1315
|
-
this.#branch = { ...newBranch, isActive: true };
|
|
1316
|
-
this.#branchName = newBranchName;
|
|
1317
|
-
this.#pendingMessages = [];
|
|
1318
|
-
}
|
|
1319
|
-
const chain = await this.#store.getMessageChain(messageId);
|
|
1320
|
-
return {
|
|
1321
|
-
id: newBranch.id,
|
|
1322
|
-
name: newBranch.name,
|
|
1323
|
-
headMessageId: newBranch.headMessageId,
|
|
1324
|
-
isActive: switchTo,
|
|
1325
|
-
messageCount: chain.length,
|
|
1326
|
-
createdAt: newBranch.createdAt
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Get the current chat ID.
|
|
1331
|
-
*/
|
|
1332
|
-
get chatId() {
|
|
1333
|
-
return this.#chatId;
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* Get the current branch name.
|
|
1337
|
-
*/
|
|
1338
|
-
get branch() {
|
|
1339
|
-
return this.#branchName;
|
|
1340
|
-
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Get metadata for the current chat.
|
|
1343
|
-
* Returns null if the chat hasn't been initialized yet.
|
|
1344
|
-
*/
|
|
1345
|
-
get chat() {
|
|
1346
|
-
if (!this.#chatData) {
|
|
1347
|
-
return null;
|
|
1348
|
-
}
|
|
1349
|
-
return {
|
|
1350
|
-
id: this.#chatData.id,
|
|
1351
|
-
createdAt: this.#chatData.createdAt,
|
|
1352
|
-
updatedAt: this.#chatData.updatedAt,
|
|
1353
|
-
title: this.#chatData.title,
|
|
1354
|
-
metadata: this.#chatData.metadata
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Add fragments to the context.
|
|
1359
|
-
*
|
|
1360
|
-
* - Message fragments (user/assistant) are queued for persistence
|
|
1361
|
-
* - Non-message fragments (role/hint) are kept in memory for system prompt
|
|
1362
|
-
*/
|
|
1363
|
-
set(...fragments) {
|
|
1364
|
-
for (const fragment2 of fragments) {
|
|
1365
|
-
if (isMessageFragment(fragment2)) {
|
|
1366
|
-
this.#pendingMessages.push(fragment2);
|
|
1367
|
-
} else {
|
|
1368
|
-
this.#fragments.push(fragment2);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return this;
|
|
1372
|
-
}
|
|
1373
|
-
// Unset a fragment by ID (not implemented yet)
|
|
1374
|
-
unset(fragmentId) {
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Render all fragments using the provided renderer.
|
|
1378
|
-
* @internal Use resolve() instead for public API.
|
|
1379
|
-
*/
|
|
1380
|
-
render(renderer) {
|
|
1381
|
-
return renderer.render(this.#fragments);
|
|
1275
|
+
return mounts;
|
|
1382
1276
|
}
|
|
1383
1277
|
/**
|
|
1384
|
-
*
|
|
1278
|
+
* Inspect the full context state for debugging.
|
|
1279
|
+
* Returns a JSON-serializable object with context information.
|
|
1385
1280
|
*
|
|
1386
|
-
* -
|
|
1387
|
-
*
|
|
1388
|
-
* - Separates context fragments for system prompt
|
|
1389
|
-
* - Combines with pending messages
|
|
1281
|
+
* @param options - Inspection options (modelId and renderer required)
|
|
1282
|
+
* @returns Complete inspection data including estimates, rendered output, fragments, and graph
|
|
1390
1283
|
*
|
|
1391
1284
|
* @example
|
|
1392
1285
|
* ```ts
|
|
1393
|
-
* const
|
|
1394
|
-
*
|
|
1286
|
+
* const inspection = await context.inspect({
|
|
1287
|
+
* modelId: 'openai:gpt-4o',
|
|
1288
|
+
* renderer: new XmlRenderer(),
|
|
1289
|
+
* });
|
|
1290
|
+
* console.log(JSON.stringify(inspection, null, 2));
|
|
1395
1291
|
*
|
|
1396
|
-
*
|
|
1397
|
-
* await
|
|
1292
|
+
* // Or write to file for analysis
|
|
1293
|
+
* await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
|
|
1398
1294
|
* ```
|
|
1399
1295
|
*/
|
|
1400
|
-
async
|
|
1296
|
+
async inspect(options) {
|
|
1401
1297
|
await this.#ensureInitialized();
|
|
1402
|
-
const
|
|
1403
|
-
const
|
|
1298
|
+
const { renderer } = options;
|
|
1299
|
+
const estimateResult = await this.estimate(options.modelId, { renderer });
|
|
1300
|
+
const rendered = renderer.render(this.#fragments);
|
|
1301
|
+
const persistedMessages = [];
|
|
1404
1302
|
if (this.#branch?.headMessageId) {
|
|
1405
1303
|
const chain = await this.#store.getMessageChain(
|
|
1406
1304
|
this.#branch.headMessageId
|
|
1407
1305
|
);
|
|
1408
|
-
|
|
1409
|
-
|
|
1306
|
+
persistedMessages.push(...chain);
|
|
1307
|
+
}
|
|
1308
|
+
const graph = await this.#store.getGraph(this.#chatId);
|
|
1309
|
+
return {
|
|
1310
|
+
estimate: estimateResult,
|
|
1311
|
+
rendered,
|
|
1312
|
+
fragments: {
|
|
1313
|
+
context: [...this.#fragments],
|
|
1314
|
+
pending: [...this.#pendingMessages],
|
|
1315
|
+
persisted: persistedMessages
|
|
1316
|
+
},
|
|
1317
|
+
graph,
|
|
1318
|
+
meta: {
|
|
1319
|
+
chatId: this.#chatId,
|
|
1320
|
+
branch: this.#branchName,
|
|
1321
|
+
timestamp: Date.now()
|
|
1410
1322
|
}
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
function term(name, definition) {
|
|
1327
|
+
return {
|
|
1328
|
+
name: "term",
|
|
1329
|
+
data: { name, definition }
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function hint(text) {
|
|
1333
|
+
return {
|
|
1334
|
+
name: "hint",
|
|
1335
|
+
data: text
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function guardrail(input) {
|
|
1339
|
+
return {
|
|
1340
|
+
name: "guardrail",
|
|
1341
|
+
data: {
|
|
1342
|
+
rule: input.rule,
|
|
1343
|
+
...input.reason && { reason: input.reason },
|
|
1344
|
+
...input.action && { action: input.action }
|
|
1411
1345
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
* Each message is added as a node with parentId pointing to the previous message.
|
|
1422
|
-
* The branch head is updated to point to the last message.
|
|
1423
|
-
*
|
|
1424
|
-
* @example
|
|
1425
|
-
* ```ts
|
|
1426
|
-
* context.set(user('Hello'));
|
|
1427
|
-
* // AI responds...
|
|
1428
|
-
* context.set(assistant('Hi there!'));
|
|
1429
|
-
* await context.save(); // Persist to graph
|
|
1430
|
-
* ```
|
|
1431
|
-
*/
|
|
1432
|
-
async save() {
|
|
1433
|
-
await this.#ensureInitialized();
|
|
1434
|
-
if (this.#pendingMessages.length === 0) {
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
let parentId = this.#branch.headMessageId;
|
|
1438
|
-
const now = Date.now();
|
|
1439
|
-
for (const fragment2 of this.#pendingMessages) {
|
|
1440
|
-
const messageData = {
|
|
1441
|
-
id: fragment2.id ?? crypto.randomUUID(),
|
|
1442
|
-
chatId: this.#chatId,
|
|
1443
|
-
parentId,
|
|
1444
|
-
name: fragment2.name,
|
|
1445
|
-
type: fragment2.type,
|
|
1446
|
-
data: fragment2.codec.encode(),
|
|
1447
|
-
createdAt: now
|
|
1448
|
-
};
|
|
1449
|
-
await this.#store.addMessage(messageData);
|
|
1450
|
-
parentId = messageData.id;
|
|
1451
|
-
}
|
|
1452
|
-
await this.#store.updateBranchHead(this.#branch.id, parentId);
|
|
1453
|
-
this.#branch.headMessageId = parentId;
|
|
1454
|
-
this.#pendingMessages = [];
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Estimate token count and cost for the full context.
|
|
1458
|
-
*
|
|
1459
|
-
* Includes:
|
|
1460
|
-
* - System prompt fragments (role, hints, etc.)
|
|
1461
|
-
* - Persisted chat messages (from store)
|
|
1462
|
-
* - Pending messages (not yet saved)
|
|
1463
|
-
*
|
|
1464
|
-
* @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
|
|
1465
|
-
* @param options - Optional settings
|
|
1466
|
-
* @returns Estimate result with token counts, costs, and per-fragment breakdown
|
|
1467
|
-
*/
|
|
1468
|
-
async estimate(modelId, options = {}) {
|
|
1469
|
-
await this.#ensureInitialized();
|
|
1470
|
-
const renderer = options.renderer ?? new XmlRenderer();
|
|
1471
|
-
const registry = getModelsRegistry();
|
|
1472
|
-
await registry.load();
|
|
1473
|
-
const model = registry.get(modelId);
|
|
1474
|
-
if (!model) {
|
|
1475
|
-
throw new Error(
|
|
1476
|
-
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
const tokenizer = registry.getTokenizer(modelId);
|
|
1480
|
-
const fragmentEstimates = [];
|
|
1481
|
-
for (const fragment2 of this.#fragments) {
|
|
1482
|
-
const rendered = renderer.render([fragment2]);
|
|
1483
|
-
const tokens = tokenizer.count(rendered);
|
|
1484
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1485
|
-
fragmentEstimates.push({
|
|
1486
|
-
id: fragment2.id,
|
|
1487
|
-
name: fragment2.name,
|
|
1488
|
-
tokens,
|
|
1489
|
-
cost
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
if (this.#branch?.headMessageId) {
|
|
1493
|
-
const chain = await this.#store.getMessageChain(
|
|
1494
|
-
this.#branch.headMessageId
|
|
1495
|
-
);
|
|
1496
|
-
for (const msg of chain) {
|
|
1497
|
-
const content = String(msg.data);
|
|
1498
|
-
const tokens = tokenizer.count(content);
|
|
1499
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1500
|
-
fragmentEstimates.push({
|
|
1501
|
-
name: msg.name,
|
|
1502
|
-
id: msg.id,
|
|
1503
|
-
tokens,
|
|
1504
|
-
cost
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
for (const fragment2 of this.#pendingMessages) {
|
|
1509
|
-
const content = String(fragment2.data);
|
|
1510
|
-
const tokens = tokenizer.count(content);
|
|
1511
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1512
|
-
fragmentEstimates.push({
|
|
1513
|
-
name: fragment2.name,
|
|
1514
|
-
id: fragment2.id,
|
|
1515
|
-
tokens,
|
|
1516
|
-
cost
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
|
|
1520
|
-
const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
|
|
1521
|
-
return {
|
|
1522
|
-
model: model.id,
|
|
1523
|
-
provider: model.provider,
|
|
1524
|
-
tokens: totalTokens,
|
|
1525
|
-
cost: totalCost,
|
|
1526
|
-
limits: {
|
|
1527
|
-
context: model.limit.context,
|
|
1528
|
-
output: model.limit.output,
|
|
1529
|
-
exceedsContext: totalTokens > model.limit.context
|
|
1530
|
-
},
|
|
1531
|
-
fragments: fragmentEstimates
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
|
-
/**
|
|
1535
|
-
* Rewind to a specific message by ID.
|
|
1536
|
-
*
|
|
1537
|
-
* Creates a new branch from that message, preserving the original branch.
|
|
1538
|
-
* The new branch becomes active.
|
|
1539
|
-
*
|
|
1540
|
-
* @param messageId - The message ID to rewind to
|
|
1541
|
-
* @returns The new branch info
|
|
1542
|
-
*
|
|
1543
|
-
* @example
|
|
1544
|
-
* ```ts
|
|
1545
|
-
* context.set(user('What is 2 + 2?', { id: 'q1' }));
|
|
1546
|
-
* context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
|
|
1547
|
-
* await context.save();
|
|
1548
|
-
*
|
|
1549
|
-
* // Rewind to the question, creates new branch
|
|
1550
|
-
* const newBranch = await context.rewind('q1');
|
|
1551
|
-
*
|
|
1552
|
-
* // Now add correct answer on new branch
|
|
1553
|
-
* context.set(assistant('The answer is 4.'));
|
|
1554
|
-
* await context.save();
|
|
1555
|
-
* ```
|
|
1556
|
-
*/
|
|
1557
|
-
async rewind(messageId) {
|
|
1558
|
-
await this.#ensureInitialized();
|
|
1559
|
-
const message2 = await this.#store.getMessage(messageId);
|
|
1560
|
-
if (!message2) {
|
|
1561
|
-
throw new Error(`Message "${messageId}" not found`);
|
|
1562
|
-
}
|
|
1563
|
-
if (message2.chatId !== this.#chatId) {
|
|
1564
|
-
throw new Error(`Message "${messageId}" belongs to a different chat`);
|
|
1565
|
-
}
|
|
1566
|
-
return this.#createBranchFrom(messageId, true);
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Create a checkpoint at the current position.
|
|
1570
|
-
*
|
|
1571
|
-
* A checkpoint is a named pointer to the current branch head.
|
|
1572
|
-
* Use restore() to return to this point later.
|
|
1573
|
-
*
|
|
1574
|
-
* @param name - Name for the checkpoint
|
|
1575
|
-
* @returns The checkpoint info
|
|
1576
|
-
*
|
|
1577
|
-
* @example
|
|
1578
|
-
* ```ts
|
|
1579
|
-
* context.set(user('I want to learn a new skill.'));
|
|
1580
|
-
* context.set(assistant('Would you like coding or cooking?'));
|
|
1581
|
-
* await context.save();
|
|
1582
|
-
*
|
|
1583
|
-
* // Save checkpoint before user's choice
|
|
1584
|
-
* const cp = await context.checkpoint('before-choice');
|
|
1585
|
-
* ```
|
|
1586
|
-
*/
|
|
1587
|
-
async checkpoint(name) {
|
|
1588
|
-
await this.#ensureInitialized();
|
|
1589
|
-
if (!this.#branch?.headMessageId) {
|
|
1590
|
-
throw new Error("Cannot create checkpoint: no messages in conversation");
|
|
1591
|
-
}
|
|
1592
|
-
const checkpoint = {
|
|
1593
|
-
id: crypto.randomUUID(),
|
|
1594
|
-
chatId: this.#chatId,
|
|
1595
|
-
name,
|
|
1596
|
-
messageId: this.#branch.headMessageId,
|
|
1597
|
-
createdAt: Date.now()
|
|
1598
|
-
};
|
|
1599
|
-
await this.#store.createCheckpoint(checkpoint);
|
|
1600
|
-
return {
|
|
1601
|
-
id: checkpoint.id,
|
|
1602
|
-
name: checkpoint.name,
|
|
1603
|
-
messageId: checkpoint.messageId,
|
|
1604
|
-
createdAt: checkpoint.createdAt
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Restore to a checkpoint by creating a new branch from that point.
|
|
1609
|
-
*
|
|
1610
|
-
* @param name - Name of the checkpoint to restore
|
|
1611
|
-
* @returns The new branch info
|
|
1612
|
-
*
|
|
1613
|
-
* @example
|
|
1614
|
-
* ```ts
|
|
1615
|
-
* // User chose cooking, but wants to try coding path
|
|
1616
|
-
* await context.restore('before-choice');
|
|
1617
|
-
*
|
|
1618
|
-
* context.set(user('I want to learn coding.'));
|
|
1619
|
-
* context.set(assistant('Python is a great starting language!'));
|
|
1620
|
-
* await context.save();
|
|
1621
|
-
* ```
|
|
1622
|
-
*/
|
|
1623
|
-
async restore(name) {
|
|
1624
|
-
await this.#ensureInitialized();
|
|
1625
|
-
const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
|
|
1626
|
-
if (!checkpoint) {
|
|
1627
|
-
throw new Error(
|
|
1628
|
-
`Checkpoint "${name}" not found in chat "${this.#chatId}"`
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
return this.rewind(checkpoint.messageId);
|
|
1632
|
-
}
|
|
1633
|
-
/**
|
|
1634
|
-
* Switch to a different branch by name.
|
|
1635
|
-
*
|
|
1636
|
-
* @param name - Branch name to switch to
|
|
1637
|
-
*
|
|
1638
|
-
* @example
|
|
1639
|
-
* ```ts
|
|
1640
|
-
* // List branches (via store)
|
|
1641
|
-
* const branches = await store.listBranches(context.chatId);
|
|
1642
|
-
* console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
|
|
1643
|
-
*
|
|
1644
|
-
* // Switch to original branch
|
|
1645
|
-
* await context.switchBranch('main');
|
|
1646
|
-
* ```
|
|
1647
|
-
*/
|
|
1648
|
-
async switchBranch(name) {
|
|
1649
|
-
await this.#ensureInitialized();
|
|
1650
|
-
const branch = await this.#store.getBranch(this.#chatId, name);
|
|
1651
|
-
if (!branch) {
|
|
1652
|
-
throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
|
|
1653
|
-
}
|
|
1654
|
-
await this.#store.setActiveBranch(this.#chatId, branch.id);
|
|
1655
|
-
this.#branch = { ...branch, isActive: true };
|
|
1656
|
-
this.#branchName = name;
|
|
1657
|
-
this.#pendingMessages = [];
|
|
1658
|
-
}
|
|
1659
|
-
/**
|
|
1660
|
-
* Create a parallel branch from the current position ("by the way").
|
|
1661
|
-
*
|
|
1662
|
-
* Use this when you want to fork the conversation without leaving
|
|
1663
|
-
* the current branch. Common use case: user wants to ask another
|
|
1664
|
-
* question while waiting for the model to respond.
|
|
1665
|
-
*
|
|
1666
|
-
* Unlike rewind(), this method:
|
|
1667
|
-
* - Uses the current HEAD (no messageId needed)
|
|
1668
|
-
* - Does NOT switch to the new branch
|
|
1669
|
-
* - Keeps pending messages intact
|
|
1670
|
-
*
|
|
1671
|
-
* @returns The new branch info (does not switch to it)
|
|
1672
|
-
* @throws Error if no messages exist in the conversation
|
|
1673
|
-
*
|
|
1674
|
-
* @example
|
|
1675
|
-
* ```ts
|
|
1676
|
-
* // User asked a question, model is generating...
|
|
1677
|
-
* context.set(user('What is the weather?'));
|
|
1678
|
-
* await context.save();
|
|
1679
|
-
*
|
|
1680
|
-
* // User wants to ask something else without waiting
|
|
1681
|
-
* const newBranch = await context.btw();
|
|
1682
|
-
* // newBranch = { name: 'main-v2', ... }
|
|
1683
|
-
*
|
|
1684
|
-
* // Later, switch to the new branch and add the question
|
|
1685
|
-
* await context.switchBranch(newBranch.name);
|
|
1686
|
-
* context.set(user('Also, what time is it?'));
|
|
1687
|
-
* await context.save();
|
|
1688
|
-
* ```
|
|
1689
|
-
*/
|
|
1690
|
-
async btw() {
|
|
1691
|
-
await this.#ensureInitialized();
|
|
1692
|
-
if (!this.#branch?.headMessageId) {
|
|
1693
|
-
throw new Error("Cannot create btw branch: no messages in conversation");
|
|
1694
|
-
}
|
|
1695
|
-
return this.#createBranchFrom(this.#branch.headMessageId, false);
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Update metadata for the current chat.
|
|
1699
|
-
*
|
|
1700
|
-
* @param updates - Partial metadata to merge (title, metadata)
|
|
1701
|
-
*
|
|
1702
|
-
* @example
|
|
1703
|
-
* ```ts
|
|
1704
|
-
* await context.updateChat({
|
|
1705
|
-
* title: 'Coding Help Session',
|
|
1706
|
-
* metadata: { tags: ['python', 'debugging'] }
|
|
1707
|
-
* });
|
|
1708
|
-
* ```
|
|
1709
|
-
*/
|
|
1710
|
-
async updateChat(updates) {
|
|
1711
|
-
await this.#ensureInitialized();
|
|
1712
|
-
const storeUpdates = {};
|
|
1713
|
-
if (updates.title !== void 0) {
|
|
1714
|
-
storeUpdates.title = updates.title;
|
|
1715
|
-
}
|
|
1716
|
-
if (updates.metadata !== void 0) {
|
|
1717
|
-
storeUpdates.metadata = {
|
|
1718
|
-
...this.#chatData?.metadata,
|
|
1719
|
-
...updates.metadata
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
|
|
1723
|
-
}
|
|
1724
|
-
/**
|
|
1725
|
-
* Consolidate context fragments (no-op for now).
|
|
1726
|
-
*
|
|
1727
|
-
* This is a placeholder for future functionality that merges context fragments
|
|
1728
|
-
* using specific rules. Currently, it does nothing.
|
|
1729
|
-
*
|
|
1730
|
-
* @experimental
|
|
1731
|
-
*/
|
|
1732
|
-
consolidate() {
|
|
1733
|
-
return void 0;
|
|
1734
|
-
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Inspect the full context state for debugging.
|
|
1737
|
-
* Returns a comprehensive JSON-serializable object with all context information.
|
|
1738
|
-
*
|
|
1739
|
-
* @param options - Inspection options (modelId and renderer required)
|
|
1740
|
-
* @returns Complete inspection data including estimates, rendered output, fragments, and graph
|
|
1741
|
-
*
|
|
1742
|
-
* @example
|
|
1743
|
-
* ```ts
|
|
1744
|
-
* const inspection = await context.inspect({
|
|
1745
|
-
* modelId: 'openai:gpt-4o',
|
|
1746
|
-
* renderer: new XmlRenderer(),
|
|
1747
|
-
* });
|
|
1748
|
-
* console.log(JSON.stringify(inspection, null, 2));
|
|
1749
|
-
*
|
|
1750
|
-
* // Or write to file for analysis
|
|
1751
|
-
* await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
|
|
1752
|
-
* ```
|
|
1753
|
-
*/
|
|
1754
|
-
async inspect(options) {
|
|
1755
|
-
await this.#ensureInitialized();
|
|
1756
|
-
const { renderer } = options;
|
|
1757
|
-
const estimateResult = await this.estimate(options.modelId, { renderer });
|
|
1758
|
-
const rendered = renderer.render(this.#fragments);
|
|
1759
|
-
const persistedMessages = [];
|
|
1760
|
-
if (this.#branch?.headMessageId) {
|
|
1761
|
-
const chain = await this.#store.getMessageChain(
|
|
1762
|
-
this.#branch.headMessageId
|
|
1763
|
-
);
|
|
1764
|
-
persistedMessages.push(...chain);
|
|
1765
|
-
}
|
|
1766
|
-
const graph = await this.#store.getGraph(this.#chatId);
|
|
1767
|
-
return {
|
|
1768
|
-
estimate: estimateResult,
|
|
1769
|
-
rendered,
|
|
1770
|
-
fragments: {
|
|
1771
|
-
context: [...this.#fragments],
|
|
1772
|
-
pending: [...this.#pendingMessages],
|
|
1773
|
-
persisted: persistedMessages
|
|
1774
|
-
},
|
|
1775
|
-
graph,
|
|
1776
|
-
meta: {
|
|
1777
|
-
chatId: this.#chatId,
|
|
1778
|
-
branch: this.#branchName,
|
|
1779
|
-
timestamp: Date.now()
|
|
1780
|
-
}
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
function term(name, definition) {
|
|
1785
|
-
return {
|
|
1786
|
-
name: "term",
|
|
1787
|
-
data: { name, definition }
|
|
1788
|
-
};
|
|
1789
|
-
}
|
|
1790
|
-
function hint(text) {
|
|
1791
|
-
return {
|
|
1792
|
-
name: "hint",
|
|
1793
|
-
data: text
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
function guardrail(input) {
|
|
1797
|
-
return {
|
|
1798
|
-
name: "guardrail",
|
|
1799
|
-
data: {
|
|
1800
|
-
rule: input.rule,
|
|
1801
|
-
...input.reason && { reason: input.reason },
|
|
1802
|
-
...input.action && { action: input.action }
|
|
1803
|
-
}
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
function explain(input) {
|
|
1807
|
-
return {
|
|
1808
|
-
name: "explain",
|
|
1809
|
-
data: {
|
|
1810
|
-
concept: input.concept,
|
|
1811
|
-
explanation: input.explanation,
|
|
1812
|
-
...input.therefore && { therefore: input.therefore }
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function explain(input) {
|
|
1349
|
+
return {
|
|
1350
|
+
name: "explain",
|
|
1351
|
+
data: {
|
|
1352
|
+
concept: input.concept,
|
|
1353
|
+
explanation: input.explanation,
|
|
1354
|
+
...input.therefore && { therefore: input.therefore }
|
|
1813
1355
|
}
|
|
1814
1356
|
};
|
|
1815
1357
|
}
|
|
@@ -1880,7 +1422,8 @@ function persona(input) {
|
|
|
1880
1422
|
name: "persona",
|
|
1881
1423
|
data: {
|
|
1882
1424
|
name: input.name,
|
|
1883
|
-
role: input.role,
|
|
1425
|
+
...input.role && { role: input.role },
|
|
1426
|
+
...input.objective && { objective: input.objective },
|
|
1884
1427
|
...input.tone && { tone: input.tone }
|
|
1885
1428
|
}
|
|
1886
1429
|
};
|
|
@@ -1890,6 +1433,7 @@ var STORE_DDL = `
|
|
|
1890
1433
|
-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
|
|
1891
1434
|
CREATE TABLE IF NOT EXISTS chats (
|
|
1892
1435
|
id TEXT PRIMARY KEY,
|
|
1436
|
+
userId TEXT NOT NULL,
|
|
1893
1437
|
title TEXT,
|
|
1894
1438
|
metadata TEXT,
|
|
1895
1439
|
createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
@@ -1897,6 +1441,7 @@ CREATE TABLE IF NOT EXISTS chats (
|
|
|
1897
1441
|
);
|
|
1898
1442
|
|
|
1899
1443
|
CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
|
|
1444
|
+
CREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);
|
|
1900
1445
|
|
|
1901
1446
|
-- Messages table (nodes in the DAG)
|
|
1902
1447
|
CREATE TABLE IF NOT EXISTS messages (
|
|
@@ -1962,37 +1507,67 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
1962
1507
|
this.#db.exec("PRAGMA foreign_keys = ON");
|
|
1963
1508
|
this.#db.exec(STORE_DDL);
|
|
1964
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Execute a function within a transaction.
|
|
1512
|
+
* Automatically commits on success or rolls back on error.
|
|
1513
|
+
*/
|
|
1514
|
+
#useTransaction(fn) {
|
|
1515
|
+
this.#db.exec("BEGIN TRANSACTION");
|
|
1516
|
+
try {
|
|
1517
|
+
const result = fn();
|
|
1518
|
+
this.#db.exec("COMMIT");
|
|
1519
|
+
return result;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
this.#db.exec("ROLLBACK");
|
|
1522
|
+
throw error;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1965
1525
|
// ==========================================================================
|
|
1966
1526
|
// Chat Operations
|
|
1967
1527
|
// ==========================================================================
|
|
1968
1528
|
async createChat(chat) {
|
|
1969
|
-
this.#
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1529
|
+
this.#useTransaction(() => {
|
|
1530
|
+
this.#db.prepare(
|
|
1531
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
1532
|
+
VALUES (?, ?, ?, ?)`
|
|
1533
|
+
).run(
|
|
1534
|
+
chat.id,
|
|
1535
|
+
chat.userId,
|
|
1536
|
+
chat.title ?? null,
|
|
1537
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
1538
|
+
);
|
|
1539
|
+
this.#db.prepare(
|
|
1540
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
1541
|
+
VALUES (?, ?, 'main', NULL, 1, ?)`
|
|
1542
|
+
).run(crypto.randomUUID(), chat.id, Date.now());
|
|
1543
|
+
});
|
|
1977
1544
|
}
|
|
1978
1545
|
async upsertChat(chat) {
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1546
|
+
return this.#useTransaction(() => {
|
|
1547
|
+
const row = this.#db.prepare(
|
|
1548
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
1549
|
+
VALUES (?, ?, ?, ?)
|
|
1550
|
+
ON CONFLICT(id) DO UPDATE SET id = excluded.id
|
|
1551
|
+
RETURNING *`
|
|
1552
|
+
).get(
|
|
1553
|
+
chat.id,
|
|
1554
|
+
chat.userId,
|
|
1555
|
+
chat.title ?? null,
|
|
1556
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
1557
|
+
);
|
|
1558
|
+
this.#db.prepare(
|
|
1559
|
+
`INSERT OR IGNORE INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
1560
|
+
VALUES (?, ?, 'main', NULL, 1, ?)`
|
|
1561
|
+
).run(crypto.randomUUID(), chat.id, Date.now());
|
|
1562
|
+
return {
|
|
1563
|
+
id: row.id,
|
|
1564
|
+
userId: row.userId,
|
|
1565
|
+
title: row.title ?? void 0,
|
|
1566
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1567
|
+
createdAt: row.createdAt,
|
|
1568
|
+
updatedAt: row.updatedAt
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1996
1571
|
}
|
|
1997
1572
|
async getChat(chatId) {
|
|
1998
1573
|
const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
|
|
@@ -2001,6 +1576,7 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2001
1576
|
}
|
|
2002
1577
|
return {
|
|
2003
1578
|
id: row.id,
|
|
1579
|
+
userId: row.userId,
|
|
2004
1580
|
title: row.title ?? void 0,
|
|
2005
1581
|
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2006
1582
|
createdAt: row.createdAt,
|
|
@@ -2024,16 +1600,33 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2024
1600
|
).get(...params);
|
|
2025
1601
|
return {
|
|
2026
1602
|
id: row.id,
|
|
1603
|
+
userId: row.userId,
|
|
2027
1604
|
title: row.title ?? void 0,
|
|
2028
1605
|
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2029
1606
|
createdAt: row.createdAt,
|
|
2030
1607
|
updatedAt: row.updatedAt
|
|
2031
1608
|
};
|
|
2032
1609
|
}
|
|
2033
|
-
async listChats() {
|
|
1610
|
+
async listChats(options) {
|
|
1611
|
+
const params = [];
|
|
1612
|
+
let whereClause = "";
|
|
1613
|
+
let limitClause = "";
|
|
1614
|
+
if (options?.userId) {
|
|
1615
|
+
whereClause = "WHERE c.userId = ?";
|
|
1616
|
+
params.push(options.userId);
|
|
1617
|
+
}
|
|
1618
|
+
if (options?.limit !== void 0) {
|
|
1619
|
+
limitClause = " LIMIT ?";
|
|
1620
|
+
params.push(options.limit);
|
|
1621
|
+
if (options.offset !== void 0) {
|
|
1622
|
+
limitClause += " OFFSET ?";
|
|
1623
|
+
params.push(options.offset);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
2034
1626
|
const rows = this.#db.prepare(
|
|
2035
1627
|
`SELECT
|
|
2036
1628
|
c.id,
|
|
1629
|
+
c.userId,
|
|
2037
1630
|
c.title,
|
|
2038
1631
|
c.createdAt,
|
|
2039
1632
|
c.updatedAt,
|
|
@@ -2042,11 +1635,13 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2042
1635
|
FROM chats c
|
|
2043
1636
|
LEFT JOIN messages m ON m.chatId = c.id
|
|
2044
1637
|
LEFT JOIN branches b ON b.chatId = c.id
|
|
1638
|
+
${whereClause}
|
|
2045
1639
|
GROUP BY c.id
|
|
2046
|
-
ORDER BY c.updatedAt DESC`
|
|
2047
|
-
).all();
|
|
1640
|
+
ORDER BY c.updatedAt DESC${limitClause}`
|
|
1641
|
+
).all(...params);
|
|
2048
1642
|
return rows.map((row) => ({
|
|
2049
1643
|
id: row.id,
|
|
1644
|
+
userId: row.userId,
|
|
2050
1645
|
title: row.title ?? void 0,
|
|
2051
1646
|
messageCount: row.messageCount,
|
|
2052
1647
|
branchCount: row.branchCount,
|
|
@@ -2054,371 +1649,1062 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2054
1649
|
updatedAt: row.updatedAt
|
|
2055
1650
|
}));
|
|
2056
1651
|
}
|
|
1652
|
+
async deleteChat(chatId, options) {
|
|
1653
|
+
return this.#useTransaction(() => {
|
|
1654
|
+
const messageIds = this.#db.prepare("SELECT id FROM messages WHERE chatId = ?").all(chatId);
|
|
1655
|
+
let sql = "DELETE FROM chats WHERE id = ?";
|
|
1656
|
+
const params = [chatId];
|
|
1657
|
+
if (options?.userId !== void 0) {
|
|
1658
|
+
sql += " AND userId = ?";
|
|
1659
|
+
params.push(options.userId);
|
|
1660
|
+
}
|
|
1661
|
+
const result = this.#db.prepare(sql).run(...params);
|
|
1662
|
+
if (result.changes > 0 && messageIds.length > 0) {
|
|
1663
|
+
const placeholders = messageIds.map(() => "?").join(", ");
|
|
1664
|
+
this.#db.prepare(
|
|
1665
|
+
`DELETE FROM messages_fts WHERE messageId IN (${placeholders})`
|
|
1666
|
+
).run(...messageIds.map((m) => m.id));
|
|
1667
|
+
}
|
|
1668
|
+
return result.changes > 0;
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
// ==========================================================================
|
|
1672
|
+
// Message Operations (Graph Nodes)
|
|
1673
|
+
// ==========================================================================
|
|
1674
|
+
async addMessage(message2) {
|
|
1675
|
+
const existingParent = message2.parentId === message2.id ? this.#db.prepare("SELECT parentId FROM messages WHERE id = ?").get(message2.id) : void 0;
|
|
1676
|
+
const parentId = message2.parentId === message2.id ? existingParent?.parentId ?? null : message2.parentId;
|
|
1677
|
+
this.#db.prepare(
|
|
1678
|
+
`INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
|
|
1679
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1680
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1681
|
+
name = excluded.name,
|
|
1682
|
+
type = excluded.type,
|
|
1683
|
+
data = excluded.data`
|
|
1684
|
+
).run(
|
|
1685
|
+
message2.id,
|
|
1686
|
+
message2.chatId,
|
|
1687
|
+
parentId,
|
|
1688
|
+
message2.name,
|
|
1689
|
+
message2.type ?? null,
|
|
1690
|
+
JSON.stringify(message2.data),
|
|
1691
|
+
message2.createdAt
|
|
1692
|
+
);
|
|
1693
|
+
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
1694
|
+
this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
|
|
1695
|
+
this.#db.prepare(
|
|
1696
|
+
`INSERT INTO messages_fts(messageId, chatId, name, content)
|
|
1697
|
+
VALUES (?, ?, ?, ?)`
|
|
1698
|
+
).run(message2.id, message2.chatId, message2.name, content);
|
|
1699
|
+
}
|
|
1700
|
+
async getMessage(messageId) {
|
|
1701
|
+
const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
|
|
1702
|
+
if (!row) {
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
return {
|
|
1706
|
+
id: row.id,
|
|
1707
|
+
chatId: row.chatId,
|
|
1708
|
+
parentId: row.parentId,
|
|
1709
|
+
name: row.name,
|
|
1710
|
+
type: row.type ?? void 0,
|
|
1711
|
+
data: JSON.parse(row.data),
|
|
1712
|
+
createdAt: row.createdAt
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
async getMessageChain(headId) {
|
|
1716
|
+
const rows = this.#db.prepare(
|
|
1717
|
+
`WITH RECURSIVE chain AS (
|
|
1718
|
+
SELECT *, 0 as depth FROM messages WHERE id = ?
|
|
1719
|
+
UNION ALL
|
|
1720
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
1721
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
1722
|
+
)
|
|
1723
|
+
SELECT * FROM chain
|
|
1724
|
+
ORDER BY depth DESC`
|
|
1725
|
+
).all(headId);
|
|
1726
|
+
return rows.map((row) => ({
|
|
1727
|
+
id: row.id,
|
|
1728
|
+
chatId: row.chatId,
|
|
1729
|
+
parentId: row.parentId,
|
|
1730
|
+
name: row.name,
|
|
1731
|
+
type: row.type ?? void 0,
|
|
1732
|
+
data: JSON.parse(row.data),
|
|
1733
|
+
createdAt: row.createdAt
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
async hasChildren(messageId) {
|
|
1737
|
+
const row = this.#db.prepare(
|
|
1738
|
+
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
|
|
1739
|
+
).get(messageId);
|
|
1740
|
+
return row.hasChildren === 1;
|
|
1741
|
+
}
|
|
1742
|
+
async getMessages(chatId) {
|
|
1743
|
+
const chat = await this.getChat(chatId);
|
|
1744
|
+
if (!chat) {
|
|
1745
|
+
throw new Error(`Chat "${chatId}" not found`);
|
|
1746
|
+
}
|
|
1747
|
+
const activeBranch = await this.getActiveBranch(chatId);
|
|
1748
|
+
if (!activeBranch?.headMessageId) {
|
|
1749
|
+
return [];
|
|
1750
|
+
}
|
|
1751
|
+
return this.getMessageChain(activeBranch.headMessageId);
|
|
1752
|
+
}
|
|
1753
|
+
// ==========================================================================
|
|
1754
|
+
// Branch Operations
|
|
1755
|
+
// ==========================================================================
|
|
1756
|
+
async createBranch(branch) {
|
|
1757
|
+
this.#db.prepare(
|
|
1758
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
1759
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1760
|
+
).run(
|
|
1761
|
+
branch.id,
|
|
1762
|
+
branch.chatId,
|
|
1763
|
+
branch.name,
|
|
1764
|
+
branch.headMessageId,
|
|
1765
|
+
branch.isActive ? 1 : 0,
|
|
1766
|
+
branch.createdAt
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
async getBranch(chatId, name) {
|
|
1770
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
1771
|
+
if (!row) {
|
|
1772
|
+
return void 0;
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
id: row.id,
|
|
1776
|
+
chatId: row.chatId,
|
|
1777
|
+
name: row.name,
|
|
1778
|
+
headMessageId: row.headMessageId,
|
|
1779
|
+
isActive: row.isActive === 1,
|
|
1780
|
+
createdAt: row.createdAt
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async getActiveBranch(chatId) {
|
|
1784
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
|
|
1785
|
+
if (!row) {
|
|
1786
|
+
return void 0;
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
id: row.id,
|
|
1790
|
+
chatId: row.chatId,
|
|
1791
|
+
name: row.name,
|
|
1792
|
+
headMessageId: row.headMessageId,
|
|
1793
|
+
isActive: true,
|
|
1794
|
+
createdAt: row.createdAt
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
async setActiveBranch(chatId, branchId) {
|
|
1798
|
+
this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
|
|
1799
|
+
this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
|
|
1800
|
+
}
|
|
1801
|
+
async updateBranchHead(branchId, messageId) {
|
|
1802
|
+
this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
|
|
1803
|
+
}
|
|
1804
|
+
async listBranches(chatId) {
|
|
1805
|
+
const branches = this.#db.prepare(
|
|
1806
|
+
`SELECT
|
|
1807
|
+
b.id,
|
|
1808
|
+
b.name,
|
|
1809
|
+
b.headMessageId,
|
|
1810
|
+
b.isActive,
|
|
1811
|
+
b.createdAt
|
|
1812
|
+
FROM branches b
|
|
1813
|
+
WHERE b.chatId = ?
|
|
1814
|
+
ORDER BY b.createdAt ASC`
|
|
1815
|
+
).all(chatId);
|
|
1816
|
+
const result = [];
|
|
1817
|
+
for (const branch of branches) {
|
|
1818
|
+
let messageCount = 0;
|
|
1819
|
+
if (branch.headMessageId) {
|
|
1820
|
+
const countRow = this.#db.prepare(
|
|
1821
|
+
`WITH RECURSIVE chain AS (
|
|
1822
|
+
SELECT id, parentId FROM messages WHERE id = ?
|
|
1823
|
+
UNION ALL
|
|
1824
|
+
SELECT m.id, m.parentId FROM messages m
|
|
1825
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
1826
|
+
)
|
|
1827
|
+
SELECT COUNT(*) as count FROM chain`
|
|
1828
|
+
).get(branch.headMessageId);
|
|
1829
|
+
messageCount = countRow.count;
|
|
1830
|
+
}
|
|
1831
|
+
result.push({
|
|
1832
|
+
id: branch.id,
|
|
1833
|
+
name: branch.name,
|
|
1834
|
+
headMessageId: branch.headMessageId,
|
|
1835
|
+
isActive: branch.isActive === 1,
|
|
1836
|
+
messageCount,
|
|
1837
|
+
createdAt: branch.createdAt
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
return result;
|
|
1841
|
+
}
|
|
2057
1842
|
// ==========================================================================
|
|
2058
|
-
//
|
|
1843
|
+
// Checkpoint Operations
|
|
2059
1844
|
// ==========================================================================
|
|
2060
|
-
async
|
|
1845
|
+
async createCheckpoint(checkpoint) {
|
|
2061
1846
|
this.#db.prepare(
|
|
2062
|
-
`INSERT INTO
|
|
2063
|
-
VALUES (?, ?, ?, ?,
|
|
2064
|
-
ON CONFLICT(
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
type = excluded.type,
|
|
2068
|
-
data = excluded.data`
|
|
1847
|
+
`INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
|
|
1848
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1849
|
+
ON CONFLICT(chatId, name) DO UPDATE SET
|
|
1850
|
+
messageId = excluded.messageId,
|
|
1851
|
+
createdAt = excluded.createdAt`
|
|
2069
1852
|
).run(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
JSON.stringify(message2.data),
|
|
2076
|
-
message2.createdAt
|
|
1853
|
+
checkpoint.id,
|
|
1854
|
+
checkpoint.chatId,
|
|
1855
|
+
checkpoint.name,
|
|
1856
|
+
checkpoint.messageId,
|
|
1857
|
+
checkpoint.createdAt
|
|
2077
1858
|
);
|
|
2078
|
-
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
2079
|
-
this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
|
|
2080
|
-
this.#db.prepare(
|
|
2081
|
-
`INSERT INTO messages_fts(messageId, chatId, name, content)
|
|
2082
|
-
VALUES (?, ?, ?, ?)`
|
|
2083
|
-
).run(message2.id, message2.chatId, message2.name, content);
|
|
2084
1859
|
}
|
|
2085
|
-
async
|
|
2086
|
-
const row = this.#db.prepare("SELECT * FROM
|
|
1860
|
+
async getCheckpoint(chatId, name) {
|
|
1861
|
+
const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
2087
1862
|
if (!row) {
|
|
2088
1863
|
return void 0;
|
|
2089
1864
|
}
|
|
2090
1865
|
return {
|
|
2091
1866
|
id: row.id,
|
|
2092
1867
|
chatId: row.chatId,
|
|
2093
|
-
parentId: row.parentId,
|
|
2094
1868
|
name: row.name,
|
|
2095
|
-
|
|
2096
|
-
data: JSON.parse(row.data),
|
|
1869
|
+
messageId: row.messageId,
|
|
2097
1870
|
createdAt: row.createdAt
|
|
2098
1871
|
};
|
|
2099
1872
|
}
|
|
2100
|
-
async
|
|
1873
|
+
async listCheckpoints(chatId) {
|
|
2101
1874
|
const rows = this.#db.prepare(
|
|
2102
|
-
`
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
)
|
|
2108
|
-
SELECT * FROM chain
|
|
2109
|
-
ORDER BY depth DESC`
|
|
2110
|
-
).all(headId);
|
|
1875
|
+
`SELECT id, name, messageId, createdAt
|
|
1876
|
+
FROM checkpoints
|
|
1877
|
+
WHERE chatId = ?
|
|
1878
|
+
ORDER BY createdAt DESC`
|
|
1879
|
+
).all(chatId);
|
|
2111
1880
|
return rows.map((row) => ({
|
|
2112
1881
|
id: row.id,
|
|
2113
|
-
chatId: row.chatId,
|
|
2114
|
-
parentId: row.parentId,
|
|
2115
1882
|
name: row.name,
|
|
2116
|
-
|
|
2117
|
-
data: JSON.parse(row.data),
|
|
1883
|
+
messageId: row.messageId,
|
|
2118
1884
|
createdAt: row.createdAt
|
|
2119
1885
|
}));
|
|
2120
1886
|
}
|
|
2121
|
-
async
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1887
|
+
async deleteCheckpoint(chatId, name) {
|
|
1888
|
+
this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
|
|
1889
|
+
}
|
|
1890
|
+
// ==========================================================================
|
|
1891
|
+
// Search Operations
|
|
1892
|
+
// ==========================================================================
|
|
1893
|
+
async searchMessages(chatId, query, options) {
|
|
1894
|
+
const limit = options?.limit ?? 20;
|
|
1895
|
+
const roles = options?.roles;
|
|
1896
|
+
let sql = `
|
|
1897
|
+
SELECT
|
|
1898
|
+
m.id,
|
|
1899
|
+
m.chatId,
|
|
1900
|
+
m.parentId,
|
|
1901
|
+
m.name,
|
|
1902
|
+
m.type,
|
|
1903
|
+
m.data,
|
|
1904
|
+
m.createdAt,
|
|
1905
|
+
fts.rank,
|
|
1906
|
+
snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
|
|
1907
|
+
FROM messages_fts fts
|
|
1908
|
+
JOIN messages m ON m.id = fts.messageId
|
|
1909
|
+
WHERE messages_fts MATCH ?
|
|
1910
|
+
AND fts.chatId = ?
|
|
1911
|
+
`;
|
|
1912
|
+
const params = [query, chatId];
|
|
1913
|
+
if (roles && roles.length > 0) {
|
|
1914
|
+
const placeholders = roles.map(() => "?").join(", ");
|
|
1915
|
+
sql += ` AND fts.name IN (${placeholders})`;
|
|
1916
|
+
params.push(...roles);
|
|
1917
|
+
}
|
|
1918
|
+
sql += " ORDER BY fts.rank LIMIT ?";
|
|
1919
|
+
params.push(limit);
|
|
1920
|
+
const rows = this.#db.prepare(sql).all(...params);
|
|
1921
|
+
return rows.map((row) => ({
|
|
1922
|
+
message: {
|
|
1923
|
+
id: row.id,
|
|
1924
|
+
chatId: row.chatId,
|
|
1925
|
+
parentId: row.parentId,
|
|
1926
|
+
name: row.name,
|
|
1927
|
+
type: row.type ?? void 0,
|
|
1928
|
+
data: JSON.parse(row.data),
|
|
1929
|
+
createdAt: row.createdAt
|
|
1930
|
+
},
|
|
1931
|
+
rank: row.rank,
|
|
1932
|
+
snippet: row.snippet
|
|
1933
|
+
}));
|
|
1934
|
+
}
|
|
1935
|
+
// ==========================================================================
|
|
1936
|
+
// Visualization Operations
|
|
1937
|
+
// ==========================================================================
|
|
1938
|
+
async getGraph(chatId) {
|
|
1939
|
+
const messageRows = this.#db.prepare(
|
|
1940
|
+
`SELECT id, parentId, name, data, createdAt
|
|
1941
|
+
FROM messages
|
|
1942
|
+
WHERE chatId = ?
|
|
1943
|
+
ORDER BY createdAt ASC`
|
|
1944
|
+
).all(chatId);
|
|
1945
|
+
const nodes = messageRows.map((row) => {
|
|
1946
|
+
const data = JSON.parse(row.data);
|
|
1947
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
1948
|
+
return {
|
|
1949
|
+
id: row.id,
|
|
1950
|
+
parentId: row.parentId,
|
|
1951
|
+
role: row.name,
|
|
1952
|
+
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
1953
|
+
createdAt: row.createdAt
|
|
1954
|
+
};
|
|
1955
|
+
});
|
|
1956
|
+
const branchRows = this.#db.prepare(
|
|
1957
|
+
`SELECT name, headMessageId, isActive
|
|
1958
|
+
FROM branches
|
|
1959
|
+
WHERE chatId = ?
|
|
1960
|
+
ORDER BY createdAt ASC`
|
|
1961
|
+
).all(chatId);
|
|
1962
|
+
const branches = branchRows.map((row) => ({
|
|
1963
|
+
name: row.name,
|
|
1964
|
+
headMessageId: row.headMessageId,
|
|
1965
|
+
isActive: row.isActive === 1
|
|
1966
|
+
}));
|
|
1967
|
+
const checkpointRows = this.#db.prepare(
|
|
1968
|
+
`SELECT name, messageId
|
|
1969
|
+
FROM checkpoints
|
|
1970
|
+
WHERE chatId = ?
|
|
1971
|
+
ORDER BY createdAt ASC`
|
|
1972
|
+
).all(chatId);
|
|
1973
|
+
const checkpoints = checkpointRows.map((row) => ({
|
|
1974
|
+
name: row.name,
|
|
1975
|
+
messageId: row.messageId
|
|
1976
|
+
}));
|
|
1977
|
+
return {
|
|
1978
|
+
chatId,
|
|
1979
|
+
nodes,
|
|
1980
|
+
branches,
|
|
1981
|
+
checkpoints
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
var InMemoryContextStore = class extends SqliteContextStore {
|
|
1986
|
+
constructor() {
|
|
1987
|
+
super(":memory:");
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
function structuredOutput(options) {
|
|
1991
|
+
return {
|
|
1992
|
+
async generate(contextVariables, config) {
|
|
1993
|
+
if (!options.context) {
|
|
1994
|
+
throw new Error(`structuredOutput is missing a context.`);
|
|
1995
|
+
}
|
|
1996
|
+
if (!options.model) {
|
|
1997
|
+
throw new Error(`structuredOutput is missing a model.`);
|
|
1998
|
+
}
|
|
1999
|
+
const { messages, systemPrompt } = await options.context.resolve({
|
|
2000
|
+
renderer: new XmlRenderer()
|
|
2001
|
+
});
|
|
2002
|
+
const result = await generateText({
|
|
2003
|
+
abortSignal: config?.abortSignal,
|
|
2004
|
+
providerOptions: options.providerOptions,
|
|
2005
|
+
model: options.model,
|
|
2006
|
+
system: systemPrompt,
|
|
2007
|
+
messages: await convertToModelMessages(messages),
|
|
2008
|
+
stopWhen: stepCountIs(25),
|
|
2009
|
+
experimental_repairToolCall: repairToolCall,
|
|
2010
|
+
experimental_context: contextVariables,
|
|
2011
|
+
output: Output.object({ schema: options.schema }),
|
|
2012
|
+
tools: options.tools
|
|
2013
|
+
});
|
|
2014
|
+
return result.output;
|
|
2015
|
+
},
|
|
2016
|
+
async stream(contextVariables, config) {
|
|
2017
|
+
if (!options.context) {
|
|
2018
|
+
throw new Error(`structuredOutput is missing a context.`);
|
|
2019
|
+
}
|
|
2020
|
+
if (!options.model) {
|
|
2021
|
+
throw new Error(`structuredOutput is missing a model.`);
|
|
2022
|
+
}
|
|
2023
|
+
const { messages, systemPrompt } = await options.context.resolve({
|
|
2024
|
+
renderer: new XmlRenderer()
|
|
2025
|
+
});
|
|
2026
|
+
return streamText({
|
|
2027
|
+
abortSignal: config?.abortSignal,
|
|
2028
|
+
providerOptions: options.providerOptions,
|
|
2029
|
+
model: options.model,
|
|
2030
|
+
system: systemPrompt,
|
|
2031
|
+
experimental_repairToolCall: repairToolCall,
|
|
2032
|
+
messages: await convertToModelMessages(messages),
|
|
2033
|
+
stopWhen: stepCountIs(50),
|
|
2034
|
+
experimental_transform: config?.transform ?? smoothStream(),
|
|
2035
|
+
experimental_context: contextVariables,
|
|
2036
|
+
output: Output.object({ schema: options.schema }),
|
|
2037
|
+
tools: options.tools
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
var repairToolCall = async ({
|
|
2043
|
+
toolCall,
|
|
2044
|
+
tools,
|
|
2045
|
+
inputSchema,
|
|
2046
|
+
error
|
|
2047
|
+
}) => {
|
|
2048
|
+
console.log(
|
|
2049
|
+
`Debug: ${chalk2.yellow("RepairingToolCall")}: ${toolCall.toolName}`,
|
|
2050
|
+
error.name
|
|
2051
|
+
);
|
|
2052
|
+
if (NoSuchToolError.isInstance(error)) {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
const tool = tools[toolCall.toolName];
|
|
2056
|
+
const { output } = await generateText({
|
|
2057
|
+
model: groq("openai/gpt-oss-20b"),
|
|
2058
|
+
output: Output.object({ schema: tool.inputSchema }),
|
|
2059
|
+
prompt: [
|
|
2060
|
+
`The model tried to call the tool "${toolCall.toolName}" with the following inputs:`,
|
|
2061
|
+
JSON.stringify(toolCall.input),
|
|
2062
|
+
`The tool accepts the following schema:`,
|
|
2063
|
+
JSON.stringify(inputSchema(toolCall)),
|
|
2064
|
+
"Please fix the inputs."
|
|
2065
|
+
].join("\n")
|
|
2066
|
+
});
|
|
2067
|
+
return { ...toolCall, input: JSON.stringify(output) };
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
// packages/text2sql/src/lib/synthesis/extractors/base-contextual-extractor.ts
|
|
2071
|
+
var contextResolverSchema = z.object({
|
|
2072
|
+
question: z.string().describe(
|
|
2073
|
+
"A standalone natural language question that the SQL query answers"
|
|
2074
|
+
)
|
|
2075
|
+
});
|
|
2076
|
+
async function resolveContext(params) {
|
|
2077
|
+
const context = new ContextEngine({
|
|
2078
|
+
store: new InMemoryContextStore(),
|
|
2079
|
+
chatId: `context-resolver-${crypto.randomUUID()}`,
|
|
2080
|
+
userId: "system"
|
|
2081
|
+
});
|
|
2082
|
+
context.set(
|
|
2083
|
+
persona({
|
|
2084
|
+
name: "context_resolver",
|
|
2085
|
+
role: "You are an expert at understanding conversational context and generating clear, standalone questions from multi-turn conversations.",
|
|
2086
|
+
objective: "Transform context-dependent messages into standalone questions that fully capture user intent"
|
|
2087
|
+
}),
|
|
2088
|
+
...params.introspection ? [fragment("database_schema", params.introspection)] : [],
|
|
2089
|
+
fragment("conversation", params.conversation),
|
|
2090
|
+
fragment("sql", params.sql),
|
|
2091
|
+
fragment(
|
|
2092
|
+
"task",
|
|
2093
|
+
dedent`
|
|
2094
|
+
Given the conversation above and the SQL query that was executed,
|
|
2095
|
+
generate a single, standalone natural language question that:
|
|
2096
|
+
1. Fully captures the user's intent without needing prior context
|
|
2097
|
+
2. Uses natural business language (not SQL terminology)
|
|
2098
|
+
3. Could be asked by someone who hasn't seen the conversation
|
|
2099
|
+
4. Accurately represents what the SQL query answers
|
|
2100
|
+
`
|
|
2101
|
+
),
|
|
2102
|
+
fragment(
|
|
2103
|
+
"examples",
|
|
2104
|
+
dedent`
|
|
2105
|
+
Conversation: "Show me customers" → "Filter to NY" → "Sort by revenue"
|
|
2106
|
+
SQL: SELECT * FROM customers WHERE region = 'NY' ORDER BY revenue DESC
|
|
2107
|
+
Question: "Show me customers in the NY region sorted by revenue"
|
|
2108
|
+
|
|
2109
|
+
Conversation: "What were sales last month?" → "Break it down by category"
|
|
2110
|
+
SQL: SELECT category, SUM(amount) FROM sales WHERE date >= '2024-11-01' GROUP BY category
|
|
2111
|
+
Question: "What were sales by category for last month?"
|
|
2112
|
+
`
|
|
2113
|
+
),
|
|
2114
|
+
user("Generate a standalone question for this SQL query.")
|
|
2115
|
+
);
|
|
2116
|
+
const resolverOutput = structuredOutput({
|
|
2117
|
+
model: groq2("openai/gpt-oss-20b"),
|
|
2118
|
+
context,
|
|
2119
|
+
schema: contextResolverSchema
|
|
2120
|
+
});
|
|
2121
|
+
return resolverOutput.generate();
|
|
2122
|
+
}
|
|
2123
|
+
function getMessageText(message2) {
|
|
2124
|
+
const textParts = message2.parts.filter(isTextUIPart).map((part) => part.text);
|
|
2125
|
+
return textParts.join(" ").trim();
|
|
2126
|
+
}
|
|
2127
|
+
function formatConversation(messages) {
|
|
2128
|
+
return messages.map((msg, i) => `[${i + 1}] ${msg}`).join("\n");
|
|
2129
|
+
}
|
|
2130
|
+
var BaseContextualExtractor = class extends PairProducer {
|
|
2131
|
+
context = [];
|
|
2132
|
+
results = [];
|
|
2133
|
+
messages;
|
|
2134
|
+
adapter;
|
|
2135
|
+
options;
|
|
2136
|
+
constructor(messages, adapter, options = {}) {
|
|
2137
|
+
super();
|
|
2138
|
+
this.messages = messages;
|
|
2139
|
+
this.adapter = adapter;
|
|
2140
|
+
this.options = options;
|
|
2126
2141
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
);
|
|
2142
|
+
/**
|
|
2143
|
+
* Template method - defines the extraction algorithm skeleton.
|
|
2144
|
+
* Subclasses customize behavior via hooks, not by overriding this method.
|
|
2145
|
+
*/
|
|
2146
|
+
async *produce() {
|
|
2147
|
+
this.context = [];
|
|
2148
|
+
this.results = [];
|
|
2149
|
+
const { includeFailures = false, toolName = "db_query" } = this.options;
|
|
2150
|
+
await this.extractSqlsWithContext(toolName, includeFailures);
|
|
2151
|
+
if (this.results.length === 0) {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const introspection = "";
|
|
2155
|
+
yield* this.resolveQuestions(introspection);
|
|
2142
2156
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2157
|
+
/**
|
|
2158
|
+
* Core extraction loop - iterates through messages and calls hooks.
|
|
2159
|
+
*/
|
|
2160
|
+
async extractSqlsWithContext(toolName, includeFailures) {
|
|
2161
|
+
for (const message2 of this.messages) {
|
|
2162
|
+
if (message2.role === "user") {
|
|
2163
|
+
const text = getMessageText(message2);
|
|
2164
|
+
if (text) {
|
|
2165
|
+
await this.onUserMessage(text);
|
|
2166
|
+
}
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
if (message2.role === "assistant") {
|
|
2170
|
+
await this.extractFromAssistant(message2, toolName, includeFailures);
|
|
2171
|
+
}
|
|
2147
2172
|
}
|
|
2148
|
-
return {
|
|
2149
|
-
id: row.id,
|
|
2150
|
-
chatId: row.chatId,
|
|
2151
|
-
name: row.name,
|
|
2152
|
-
headMessageId: row.headMessageId,
|
|
2153
|
-
isActive: row.isActive === 1,
|
|
2154
|
-
createdAt: row.createdAt
|
|
2155
|
-
};
|
|
2156
2173
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2174
|
+
/**
|
|
2175
|
+
* Extract SQL from assistant message parts.
|
|
2176
|
+
*/
|
|
2177
|
+
async extractFromAssistant(message2, toolName, includeFailures) {
|
|
2178
|
+
for (const part of message2.parts) {
|
|
2179
|
+
if (!isToolOrDynamicToolUIPart(part)) {
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
if (getToolOrDynamicToolName(part) !== toolName) {
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const toolInput = "input" in part ? part.input : void 0;
|
|
2186
|
+
if (!toolInput?.sql) {
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const success = part.state === "output-available";
|
|
2190
|
+
const failed = part.state === "output-error";
|
|
2191
|
+
if (failed && !includeFailures) {
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
if (!success && !failed) {
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
const snapshot = this.getContextSnapshot();
|
|
2198
|
+
if (snapshot.length === 0) {
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
this.results.push({
|
|
2202
|
+
sql: toolInput.sql,
|
|
2203
|
+
success,
|
|
2204
|
+
conversationContext: snapshot
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
const assistantText2 = getMessageText(message2);
|
|
2208
|
+
if (assistantText2) {
|
|
2209
|
+
this.context.push(`Assistant: ${assistantText2}`);
|
|
2161
2210
|
}
|
|
2162
|
-
return {
|
|
2163
|
-
id: row.id,
|
|
2164
|
-
chatId: row.chatId,
|
|
2165
|
-
name: row.name,
|
|
2166
|
-
headMessageId: row.headMessageId,
|
|
2167
|
-
isActive: true,
|
|
2168
|
-
createdAt: row.createdAt
|
|
2169
|
-
};
|
|
2170
2211
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2212
|
+
/**
|
|
2213
|
+
* Resolve extracted SQL contexts into standalone questions using LLM.
|
|
2214
|
+
*/
|
|
2215
|
+
async *resolveQuestions(introspection) {
|
|
2216
|
+
for (const item of this.results) {
|
|
2217
|
+
const output = await resolveContext({
|
|
2218
|
+
conversation: formatConversation(item.conversationContext),
|
|
2219
|
+
sql: item.sql,
|
|
2220
|
+
introspection
|
|
2221
|
+
});
|
|
2222
|
+
yield [
|
|
2223
|
+
{
|
|
2224
|
+
question: output.question,
|
|
2225
|
+
sql: item.sql,
|
|
2226
|
+
context: item.conversationContext,
|
|
2227
|
+
success: item.success
|
|
2228
|
+
}
|
|
2229
|
+
];
|
|
2230
|
+
}
|
|
2174
2231
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2232
|
+
};
|
|
2233
|
+
|
|
2234
|
+
// packages/text2sql/src/lib/synthesis/extractors/message-extractor.ts
|
|
2235
|
+
var MessageExtractor = class extends PairProducer {
|
|
2236
|
+
#messages;
|
|
2237
|
+
#options;
|
|
2238
|
+
/**
|
|
2239
|
+
* @param messages - Chat history to extract pairs from
|
|
2240
|
+
* @param options - Extraction configuration
|
|
2241
|
+
*/
|
|
2242
|
+
constructor(messages, options = {}) {
|
|
2243
|
+
super();
|
|
2244
|
+
this.#messages = messages;
|
|
2245
|
+
this.#options = options;
|
|
2177
2246
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
).all(chatId);
|
|
2190
|
-
const result = [];
|
|
2191
|
-
for (const branch of branches) {
|
|
2192
|
-
let messageCount = 0;
|
|
2193
|
-
if (branch.headMessageId) {
|
|
2194
|
-
const countRow = this.#db.prepare(
|
|
2195
|
-
`WITH RECURSIVE chain AS (
|
|
2196
|
-
SELECT id, parentId FROM messages WHERE id = ?
|
|
2197
|
-
UNION ALL
|
|
2198
|
-
SELECT m.id, m.parentId FROM messages m
|
|
2199
|
-
INNER JOIN chain c ON m.id = c.parentId
|
|
2200
|
-
)
|
|
2201
|
-
SELECT COUNT(*) as count FROM chain`
|
|
2202
|
-
).get(branch.headMessageId);
|
|
2203
|
-
messageCount = countRow.count;
|
|
2247
|
+
/**
|
|
2248
|
+
* Extracts question-SQL pairs by parsing tool calls and pairing with user messages.
|
|
2249
|
+
* @returns Pairs extracted from db_query tool invocations
|
|
2250
|
+
*/
|
|
2251
|
+
async *produce() {
|
|
2252
|
+
const { includeFailures = false, toolName = "db_query" } = this.#options;
|
|
2253
|
+
let lastUserMessage = null;
|
|
2254
|
+
for (const message2 of this.#messages) {
|
|
2255
|
+
if (message2.role === "user") {
|
|
2256
|
+
lastUserMessage = message2;
|
|
2257
|
+
continue;
|
|
2204
2258
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2259
|
+
if (message2.role === "assistant" && lastUserMessage) {
|
|
2260
|
+
for (const part of message2.parts) {
|
|
2261
|
+
if (!isToolOrDynamicToolUIPart2(part)) {
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
if (getToolOrDynamicToolName2(part) !== toolName) {
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const toolInput = "input" in part ? part.input : void 0;
|
|
2268
|
+
if (!toolInput?.sql) {
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const success = part.state === "output-available";
|
|
2272
|
+
const failed = part.state === "output-error";
|
|
2273
|
+
if (failed && !includeFailures) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (!success && !failed) {
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
const question = getMessageText(lastUserMessage);
|
|
2280
|
+
if (!question) {
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
yield [
|
|
2284
|
+
{
|
|
2285
|
+
question,
|
|
2286
|
+
sql: toolInput.sql,
|
|
2287
|
+
success
|
|
2288
|
+
}
|
|
2289
|
+
];
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
|
|
2296
|
+
// packages/text2sql/src/lib/synthesis/extractors/sql-extractor.ts
|
|
2297
|
+
import { groq as groq3 } from "@ai-sdk/groq";
|
|
2298
|
+
import dedent2 from "dedent";
|
|
2299
|
+
import z2 from "zod";
|
|
2300
|
+
var outputSchema = z2.object({
|
|
2301
|
+
question: z2.string().describe("A natural language question that the SQL query answers")
|
|
2302
|
+
});
|
|
2303
|
+
var SqlExtractor = class extends PairProducer {
|
|
2304
|
+
#sqls;
|
|
2305
|
+
#adapter;
|
|
2306
|
+
#options;
|
|
2307
|
+
/**
|
|
2308
|
+
* @param sql - SQL query or queries to generate questions for
|
|
2309
|
+
* @param adapter - Database adapter for validation and schema introspection
|
|
2310
|
+
* @param options - Extraction configuration
|
|
2311
|
+
*/
|
|
2312
|
+
constructor(sql, adapter, options = {}) {
|
|
2313
|
+
super();
|
|
2314
|
+
this.#sqls = Array.isArray(sql) ? sql : [sql];
|
|
2315
|
+
this.#adapter = adapter;
|
|
2316
|
+
this.#options = options;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Generates natural language questions for each SQL query using an LLM.
|
|
2320
|
+
* @returns Pairs with generated questions and original SQL
|
|
2321
|
+
*/
|
|
2322
|
+
async *produce() {
|
|
2323
|
+
const { validateSql = true, skipInvalid = false } = this.#options;
|
|
2324
|
+
const introspection = "";
|
|
2325
|
+
for (const sql of this.#sqls) {
|
|
2326
|
+
let isValid = true;
|
|
2327
|
+
if (validateSql) {
|
|
2328
|
+
const error = await this.#adapter.validate(sql);
|
|
2329
|
+
isValid = error === void 0 || error === null;
|
|
2330
|
+
if (!isValid && skipInvalid) {
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const context = new ContextEngine({
|
|
2335
|
+
store: new InMemoryContextStore(),
|
|
2336
|
+
chatId: `sql-to-question-${crypto.randomUUID()}`,
|
|
2337
|
+
userId: "system"
|
|
2212
2338
|
});
|
|
2339
|
+
context.set(
|
|
2340
|
+
persona({
|
|
2341
|
+
name: "sql_to_question",
|
|
2342
|
+
role: "You are an expert at understanding SQL queries and generating clear, natural language questions that describe what the query retrieves.",
|
|
2343
|
+
objective: "Generate clear, natural language questions that describe what SQL queries retrieve"
|
|
2344
|
+
}),
|
|
2345
|
+
fragment("database_schema", introspection),
|
|
2346
|
+
fragment("sql", sql),
|
|
2347
|
+
fragment(
|
|
2348
|
+
"task",
|
|
2349
|
+
dedent2`
|
|
2350
|
+
Given the database schema and the SQL query above, generate a single
|
|
2351
|
+
natural language question that:
|
|
2352
|
+
1. Accurately describes what information the query retrieves
|
|
2353
|
+
2. Uses natural business language (not SQL terminology)
|
|
2354
|
+
3. Could be asked by a non-technical user
|
|
2355
|
+
4. Is concise but complete
|
|
2356
|
+
`
|
|
2357
|
+
),
|
|
2358
|
+
fragment(
|
|
2359
|
+
"examples",
|
|
2360
|
+
dedent2`
|
|
2361
|
+
SQL: SELECT COUNT(*) FROM customers WHERE region = 'NY'
|
|
2362
|
+
Question: "How many customers do we have in New York?"
|
|
2363
|
+
|
|
2364
|
+
SQL: SELECT product_name, SUM(quantity) as total FROM orders GROUP BY product_name ORDER BY total DESC LIMIT 10
|
|
2365
|
+
Question: "What are our top 10 products by quantity sold?"
|
|
2366
|
+
|
|
2367
|
+
SQL: SELECT c.name, COUNT(o.id) FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.id HAVING COUNT(o.id) = 0
|
|
2368
|
+
Question: "Which customers have never placed an order?"
|
|
2369
|
+
`
|
|
2370
|
+
),
|
|
2371
|
+
user("Generate a natural language question for this SQL query.")
|
|
2372
|
+
);
|
|
2373
|
+
const sqlToQuestionOutput = structuredOutput({
|
|
2374
|
+
model: groq3("openai/gpt-oss-20b"),
|
|
2375
|
+
context,
|
|
2376
|
+
schema: outputSchema
|
|
2377
|
+
});
|
|
2378
|
+
const output = await sqlToQuestionOutput.generate();
|
|
2379
|
+
yield [
|
|
2380
|
+
{
|
|
2381
|
+
question: output.question,
|
|
2382
|
+
sql,
|
|
2383
|
+
success: isValid
|
|
2384
|
+
}
|
|
2385
|
+
];
|
|
2213
2386
|
}
|
|
2214
|
-
return result;
|
|
2215
2387
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
VALUES (?, ?, ?, ?, ?)
|
|
2223
|
-
ON CONFLICT(chatId, name) DO UPDATE SET
|
|
2224
|
-
messageId = excluded.messageId,
|
|
2225
|
-
createdAt = excluded.createdAt`
|
|
2226
|
-
).run(
|
|
2227
|
-
checkpoint.id,
|
|
2228
|
-
checkpoint.chatId,
|
|
2229
|
-
checkpoint.name,
|
|
2230
|
-
checkpoint.messageId,
|
|
2231
|
-
checkpoint.createdAt
|
|
2232
|
-
);
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
// packages/text2sql/src/lib/synthesis/extractors/full-context-extractor.ts
|
|
2391
|
+
var FullContextExtractor = class extends BaseContextualExtractor {
|
|
2392
|
+
constructor(messages, adapter, options = {}) {
|
|
2393
|
+
super(messages, adapter, options);
|
|
2233
2394
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
}
|
|
2239
|
-
return {
|
|
2240
|
-
id: row.id,
|
|
2241
|
-
chatId: row.chatId,
|
|
2242
|
-
name: row.name,
|
|
2243
|
-
messageId: row.messageId,
|
|
2244
|
-
createdAt: row.createdAt
|
|
2245
|
-
};
|
|
2395
|
+
/**
|
|
2396
|
+
* Add user message to context (keeps all messages).
|
|
2397
|
+
*/
|
|
2398
|
+
async onUserMessage(text) {
|
|
2399
|
+
this.context.push(`User: ${text}`);
|
|
2246
2400
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
ORDER BY createdAt DESC`
|
|
2253
|
-
).all(chatId);
|
|
2254
|
-
return rows.map((row) => ({
|
|
2255
|
-
id: row.id,
|
|
2256
|
-
name: row.name,
|
|
2257
|
-
messageId: row.messageId,
|
|
2258
|
-
createdAt: row.createdAt
|
|
2259
|
-
}));
|
|
2401
|
+
/**
|
|
2402
|
+
* Return all context accumulated so far.
|
|
2403
|
+
*/
|
|
2404
|
+
getContextSnapshot() {
|
|
2405
|
+
return [...this.context];
|
|
2260
2406
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// packages/text2sql/src/lib/synthesis/extractors/windowed-context-extractor.ts
|
|
2410
|
+
var WindowedContextExtractor = class extends BaseContextualExtractor {
|
|
2411
|
+
windowSize;
|
|
2412
|
+
constructor(messages, adapter, options) {
|
|
2413
|
+
super(messages, adapter, options);
|
|
2414
|
+
this.windowSize = options.windowSize;
|
|
2263
2415
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
async
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
m.type,
|
|
2277
|
-
m.data,
|
|
2278
|
-
m.createdAt,
|
|
2279
|
-
fts.rank,
|
|
2280
|
-
snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
|
|
2281
|
-
FROM messages_fts fts
|
|
2282
|
-
JOIN messages m ON m.id = fts.messageId
|
|
2283
|
-
WHERE messages_fts MATCH ?
|
|
2284
|
-
AND fts.chatId = ?
|
|
2285
|
-
`;
|
|
2286
|
-
const params = [query, chatId];
|
|
2287
|
-
if (roles && roles.length > 0) {
|
|
2288
|
-
const placeholders = roles.map(() => "?").join(", ");
|
|
2289
|
-
sql += ` AND fts.name IN (${placeholders})`;
|
|
2290
|
-
params.push(...roles);
|
|
2416
|
+
/**
|
|
2417
|
+
* Add user message to context (keeps all, windowing happens on snapshot).
|
|
2418
|
+
*/
|
|
2419
|
+
async onUserMessage(text) {
|
|
2420
|
+
this.context.push(`User: ${text}`);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Return only the last N messages based on window size.
|
|
2424
|
+
*/
|
|
2425
|
+
getContextSnapshot() {
|
|
2426
|
+
if (this.context.length <= this.windowSize) {
|
|
2427
|
+
return [...this.context];
|
|
2291
2428
|
}
|
|
2292
|
-
|
|
2293
|
-
params.push(limit);
|
|
2294
|
-
const rows = this.#db.prepare(sql).all(...params);
|
|
2295
|
-
return rows.map((row) => ({
|
|
2296
|
-
message: {
|
|
2297
|
-
id: row.id,
|
|
2298
|
-
chatId: row.chatId,
|
|
2299
|
-
parentId: row.parentId,
|
|
2300
|
-
name: row.name,
|
|
2301
|
-
type: row.type ?? void 0,
|
|
2302
|
-
data: JSON.parse(row.data),
|
|
2303
|
-
createdAt: row.createdAt
|
|
2304
|
-
},
|
|
2305
|
-
rank: row.rank,
|
|
2306
|
-
snippet: row.snippet
|
|
2307
|
-
}));
|
|
2429
|
+
return this.context.slice(-this.windowSize);
|
|
2308
2430
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
// packages/text2sql/src/lib/synthesis/extractors/segmented-context-extractor.ts
|
|
2434
|
+
import { groq as groq4 } from "@ai-sdk/groq";
|
|
2435
|
+
import dedent3 from "dedent";
|
|
2436
|
+
import z3 from "zod";
|
|
2437
|
+
var topicChangeSchema = z3.object({
|
|
2438
|
+
isTopicChange: z3.boolean().describe("Whether the new message represents a topic change"),
|
|
2439
|
+
reason: z3.string().describe("Brief explanation for the decision")
|
|
2440
|
+
});
|
|
2441
|
+
async function detectTopicChange(params) {
|
|
2442
|
+
const context = new ContextEngine({
|
|
2443
|
+
store: new InMemoryContextStore(),
|
|
2444
|
+
chatId: `topic-change-${crypto.randomUUID()}`,
|
|
2445
|
+
userId: "system"
|
|
2446
|
+
});
|
|
2447
|
+
context.set(
|
|
2448
|
+
persona({
|
|
2449
|
+
name: "topic_change_detector",
|
|
2450
|
+
role: "You are an expert at understanding conversational flow and detecting topic changes.",
|
|
2451
|
+
objective: "Detect significant topic changes in database conversations"
|
|
2452
|
+
}),
|
|
2453
|
+
fragment("conversation_context", params.context || "(no prior context)"),
|
|
2454
|
+
fragment("new_message", params.newMessage),
|
|
2455
|
+
fragment(
|
|
2456
|
+
"task",
|
|
2457
|
+
dedent3`
|
|
2458
|
+
Determine if the new message represents a significant topic change from the
|
|
2459
|
+
prior conversation context. A topic change occurs when:
|
|
2460
|
+
1. The user asks about a completely different entity/table/domain
|
|
2461
|
+
2. The user starts a new analytical question unrelated to prior discussion
|
|
2462
|
+
3. There's a clear shift in what data or metrics are being discussed
|
|
2463
|
+
|
|
2464
|
+
NOT a topic change:
|
|
2465
|
+
- Follow-up questions refining the same query ("filter by...", "sort by...")
|
|
2466
|
+
- Questions about the same entities with different conditions
|
|
2467
|
+
- Requests for more details on the same topic
|
|
2468
|
+
`
|
|
2469
|
+
),
|
|
2470
|
+
fragment(
|
|
2471
|
+
"examples",
|
|
2472
|
+
dedent3`
|
|
2473
|
+
Context: "Show me customers in NY" → "Sort by revenue"
|
|
2474
|
+
New: "Filter to those with orders over $1000"
|
|
2475
|
+
Decision: NOT a topic change (still refining customer query)
|
|
2476
|
+
|
|
2477
|
+
Context: "Show me customers in NY" → "Sort by revenue"
|
|
2478
|
+
New: "What were our total sales last quarter?"
|
|
2479
|
+
Decision: Topic change (shifted from customers to sales metrics)
|
|
2480
|
+
|
|
2481
|
+
Context: "List all products"
|
|
2482
|
+
New: "How many orders did we have last month?"
|
|
2483
|
+
Decision: Topic change (products → orders/sales)
|
|
2484
|
+
`
|
|
2485
|
+
),
|
|
2486
|
+
user("Determine if this is a topic change.")
|
|
2487
|
+
);
|
|
2488
|
+
const topicOutput = structuredOutput({
|
|
2489
|
+
model: groq4("openai/gpt-oss-20b"),
|
|
2490
|
+
context,
|
|
2491
|
+
schema: topicChangeSchema
|
|
2492
|
+
});
|
|
2493
|
+
return topicOutput.generate();
|
|
2494
|
+
}
|
|
2495
|
+
var SegmentedContextExtractor = class extends BaseContextualExtractor {
|
|
2496
|
+
constructor(messages, adapter, options = {}) {
|
|
2497
|
+
super(messages, adapter, options);
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Handle user message with topic change detection.
|
|
2501
|
+
* If topic changes, resolve the message to standalone form before resetting.
|
|
2502
|
+
*
|
|
2503
|
+
* Note: We capture context snapshot before async LLM calls to prevent race conditions
|
|
2504
|
+
* where context might be modified during the async operation.
|
|
2505
|
+
*/
|
|
2506
|
+
async onUserMessage(text) {
|
|
2507
|
+
if (this.context.length >= 2) {
|
|
2508
|
+
const contextSnapshot = [...this.context];
|
|
2509
|
+
const { isTopicChange } = await detectTopicChange({
|
|
2510
|
+
context: formatConversation(contextSnapshot),
|
|
2511
|
+
newMessage: text
|
|
2512
|
+
});
|
|
2513
|
+
if (isTopicChange) {
|
|
2514
|
+
const resolved = await this.resolveToStandalone(text, contextSnapshot);
|
|
2515
|
+
this.context = [`User: ${resolved}`];
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
this.context.push(`User: ${text}`);
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Return all context in current topic segment.
|
|
2523
|
+
*/
|
|
2524
|
+
getContextSnapshot() {
|
|
2525
|
+
return [...this.context];
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Resolve a context-dependent message into a standalone question.
|
|
2529
|
+
* Called when topic change is detected to preserve the meaning of
|
|
2530
|
+
* the triggering message before context is reset.
|
|
2531
|
+
* @param text - The user message to resolve
|
|
2532
|
+
* @param contextSnapshot - Snapshot of context captured before this async call
|
|
2533
|
+
*/
|
|
2534
|
+
async resolveToStandalone(text, contextSnapshot) {
|
|
2535
|
+
const output = await resolveContext({
|
|
2536
|
+
conversation: formatConversation([...contextSnapshot, `User: ${text}`]),
|
|
2537
|
+
sql: ""
|
|
2538
|
+
// No SQL yet, just resolving the question
|
|
2329
2539
|
});
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
}));
|
|
2351
|
-
return {
|
|
2352
|
-
chatId,
|
|
2353
|
-
nodes,
|
|
2354
|
-
branches,
|
|
2355
|
-
checkpoints
|
|
2356
|
-
};
|
|
2540
|
+
return output.question;
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// packages/text2sql/src/lib/synthesis/extractors/last-query-extractor.ts
|
|
2545
|
+
var LastQueryExtractor = class extends BaseContextualExtractor {
|
|
2546
|
+
constructor(messages, adapter, options = {}) {
|
|
2547
|
+
super(messages, adapter, options);
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Add user message to context (keeps all messages).
|
|
2551
|
+
*/
|
|
2552
|
+
async onUserMessage(text) {
|
|
2553
|
+
this.context.push(`User: ${text}`);
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Return all context accumulated so far.
|
|
2557
|
+
*/
|
|
2558
|
+
getContextSnapshot() {
|
|
2559
|
+
return [...this.context];
|
|
2357
2560
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2561
|
+
/**
|
|
2562
|
+
* Override to only resolve the LAST query instead of all queries.
|
|
2563
|
+
*/
|
|
2564
|
+
async *resolveQuestions(introspection) {
|
|
2565
|
+
if (this.results.length === 0) {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
const last = this.results.at(-1);
|
|
2569
|
+
const output = await resolveContext({
|
|
2570
|
+
conversation: formatConversation(last.conversationContext),
|
|
2571
|
+
sql: last.sql,
|
|
2572
|
+
introspection
|
|
2573
|
+
});
|
|
2574
|
+
yield [
|
|
2575
|
+
{
|
|
2576
|
+
question: output.question,
|
|
2577
|
+
sql: last.sql,
|
|
2578
|
+
context: last.conversationContext,
|
|
2579
|
+
success: last.success
|
|
2580
|
+
}
|
|
2581
|
+
];
|
|
2362
2582
|
}
|
|
2363
2583
|
};
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2584
|
+
|
|
2585
|
+
// packages/text2sql/src/lib/synthesis/synthesizers/schema-synthesizer.ts
|
|
2586
|
+
import pLimit from "p-limit";
|
|
2587
|
+
|
|
2588
|
+
// packages/text2sql/src/lib/agents/question.agent.ts
|
|
2589
|
+
import { groq as groq5 } from "@ai-sdk/groq";
|
|
2590
|
+
import dedent4 from "dedent";
|
|
2591
|
+
import z4 from "zod";
|
|
2592
|
+
import "@deepagents/agent";
|
|
2593
|
+
var complexityInstructions = {
|
|
2594
|
+
simple: dedent4`
|
|
2595
|
+
Generate simple questions that require:
|
|
2596
|
+
- Basic SELECT with single table
|
|
2597
|
+
- Simple WHERE clauses with one condition
|
|
2598
|
+
- COUNT(*) or basic aggregations
|
|
2599
|
+
- No joins required
|
|
2600
|
+
Examples: "How many customers do we have?", "List all products", "What is the total revenue?"
|
|
2601
|
+
`,
|
|
2602
|
+
moderate: dedent4`
|
|
2603
|
+
Generate moderate questions that require:
|
|
2604
|
+
- JOINs between 2-3 tables
|
|
2605
|
+
- Multiple WHERE conditions (AND/OR)
|
|
2606
|
+
- GROUP BY with HAVING clauses
|
|
2607
|
+
- ORDER BY with LIMIT
|
|
2608
|
+
- Basic subqueries
|
|
2609
|
+
Examples: "What are the top 5 customers by total orders?", "Which products have never been ordered?"
|
|
2610
|
+
`,
|
|
2611
|
+
complex: dedent4`
|
|
2612
|
+
Generate complex questions that require:
|
|
2613
|
+
- Multiple JOINs (3+ tables)
|
|
2614
|
+
- Nested subqueries or CTEs
|
|
2615
|
+
- Complex aggregations with multiple GROUP BY columns
|
|
2616
|
+
- CASE expressions
|
|
2617
|
+
- Date/time calculations
|
|
2618
|
+
Examples: "What is the month-over-month growth rate?", "Which customers have increased spending compared to last year?"
|
|
2619
|
+
`,
|
|
2620
|
+
"high complex": dedent4`
|
|
2621
|
+
Generate highly complex questions that require advanced SQL features:
|
|
2622
|
+
- Window functions (ROW_NUMBER, RANK, DENSE_RANK)
|
|
2623
|
+
- LAG, LEAD for comparisons
|
|
2624
|
+
- Running totals (SUM OVER)
|
|
2625
|
+
- Moving averages
|
|
2626
|
+
- PARTITION BY clauses
|
|
2627
|
+
- Complex CTEs with multiple levels
|
|
2628
|
+
Examples: "What is the running total of sales per month?", "Rank customers by their purchase frequency within each region"
|
|
2629
|
+
`
|
|
2630
|
+
};
|
|
2631
|
+
var outputSchema2 = z4.object({
|
|
2632
|
+
questions: z4.array(z4.string().describe("A natural language question about the data")).min(1).describe("List of natural language questions a user might ask")
|
|
2633
|
+
});
|
|
2634
|
+
async function generateQuestions(params) {
|
|
2635
|
+
const { introspection, complexity, count, prompt, model } = params;
|
|
2636
|
+
const context = new ContextEngine({
|
|
2637
|
+
store: new InMemoryContextStore(),
|
|
2638
|
+
chatId: `question-gen-${crypto.randomUUID()}`,
|
|
2639
|
+
userId: "system"
|
|
2640
|
+
});
|
|
2641
|
+
context.set(
|
|
2642
|
+
persona({
|
|
2643
|
+
name: "question_generator",
|
|
2644
|
+
role: "You are a synthetic data generator specializing in creating realistic natural language questions that users might ask about a database.",
|
|
2645
|
+
objective: "Generate diverse, realistic natural language questions that match the specified complexity level"
|
|
2646
|
+
}),
|
|
2647
|
+
fragment("database_schema", introspection || ""),
|
|
2648
|
+
fragment(
|
|
2649
|
+
"complexity",
|
|
2650
|
+
{ level: complexity },
|
|
2651
|
+
complexityInstructions[complexity]
|
|
2652
|
+
),
|
|
2653
|
+
fragment(
|
|
2654
|
+
"task",
|
|
2655
|
+
dedent4`
|
|
2656
|
+
Generate exactly ${count} natural language questions at the "${complexity}" complexity level.
|
|
2657
|
+
The questions should:
|
|
2658
|
+
1. Match the complexity requirements above
|
|
2659
|
+
2. Use natural business language, not technical SQL terms
|
|
2660
|
+
3. Be realistic questions a non-technical user would actually ask
|
|
2661
|
+
4. Cover different tables and relationships when possible
|
|
2662
|
+
`
|
|
2663
|
+
),
|
|
2664
|
+
guardrail({
|
|
2665
|
+
rule: "Questions MUST ONLY reference tables and columns that exist in the schema above"
|
|
2666
|
+
}),
|
|
2667
|
+
guardrail({
|
|
2668
|
+
rule: "Before generating each question, verify that ALL entities (tables, columns, relationships) you reference are explicitly listed in the schema"
|
|
2669
|
+
}),
|
|
2670
|
+
guardrail({
|
|
2671
|
+
rule: "DO NOT invent or assume tables/columns that are not explicitly shown in the schema"
|
|
2672
|
+
}),
|
|
2673
|
+
guardrail({
|
|
2674
|
+
rule: "Use natural language without SQL keywords like SELECT, WHERE, etc."
|
|
2675
|
+
}),
|
|
2676
|
+
guardrail({
|
|
2677
|
+
rule: "All questions must match the specified complexity level"
|
|
2678
|
+
}),
|
|
2679
|
+
user(
|
|
2680
|
+
prompt ?? `Generate ${count} questions at ${complexity} complexity given db schema.`
|
|
2681
|
+
)
|
|
2682
|
+
);
|
|
2683
|
+
const questionOutput = structuredOutput({
|
|
2684
|
+
model: model ?? groq5("openai/gpt-oss-20b"),
|
|
2685
|
+
context,
|
|
2686
|
+
schema: outputSchema2
|
|
2687
|
+
});
|
|
2688
|
+
return questionOutput.generate();
|
|
2419
2689
|
}
|
|
2420
2690
|
|
|
2421
2691
|
// packages/text2sql/src/lib/agents/sql.agent.ts
|
|
2692
|
+
import { groq as groq6 } from "@ai-sdk/groq";
|
|
2693
|
+
import {
|
|
2694
|
+
APICallError,
|
|
2695
|
+
JSONParseError,
|
|
2696
|
+
NoContentGeneratedError,
|
|
2697
|
+
NoObjectGeneratedError,
|
|
2698
|
+
NoOutputGeneratedError,
|
|
2699
|
+
TypeValidationError,
|
|
2700
|
+
defaultSettingsMiddleware,
|
|
2701
|
+
wrapLanguageModel
|
|
2702
|
+
} from "ai";
|
|
2703
|
+
import { Console } from "node:console";
|
|
2704
|
+
import { createWriteStream } from "node:fs";
|
|
2705
|
+
import pRetry from "p-retry";
|
|
2706
|
+
import z5 from "zod";
|
|
2707
|
+
import "@deepagents/agent";
|
|
2422
2708
|
var logger = new Console({
|
|
2423
2709
|
stdout: createWriteStream("./sql-agent.log", { flags: "a" }),
|
|
2424
2710
|
stderr: createWriteStream("./sql-agent-error.log", { flags: "a" }),
|
|
@@ -2456,37 +2742,36 @@ async function toSql(options) {
|
|
|
2456
2742
|
async (attemptNumber, errors, attempts) => {
|
|
2457
2743
|
const context = new ContextEngine({
|
|
2458
2744
|
store: new InMemoryContextStore(),
|
|
2459
|
-
chatId: `sql-gen-${crypto.randomUUID()}
|
|
2745
|
+
chatId: `sql-gen-${crypto.randomUUID()}`,
|
|
2746
|
+
userId: "system"
|
|
2460
2747
|
});
|
|
2461
2748
|
context.set(
|
|
2462
2749
|
persona({
|
|
2463
2750
|
name: "Freya",
|
|
2464
|
-
role: "You are an expert SQL query generator. You translate natural language questions into precise, efficient SQL queries based on the provided database schema."
|
|
2751
|
+
role: "You are an expert SQL query generator. You translate natural language questions into precise, efficient SQL queries based on the provided database schema.",
|
|
2752
|
+
objective: "Translate natural language questions into precise, efficient SQL queries"
|
|
2465
2753
|
}),
|
|
2466
2754
|
...options.instructions,
|
|
2467
2755
|
...options.schemaFragments
|
|
2468
2756
|
);
|
|
2469
2757
|
if (errors.length) {
|
|
2470
2758
|
context.set(
|
|
2471
|
-
|
|
2472
|
-
|
|
2759
|
+
user(options.input),
|
|
2760
|
+
user(
|
|
2473
2761
|
`<validation_error>Your previous SQL query had the following error: ${errors.at(-1)?.message}. Please fix the query.</validation_error>`
|
|
2474
2762
|
)
|
|
2475
2763
|
);
|
|
2476
2764
|
} else {
|
|
2477
|
-
context.set(
|
|
2765
|
+
context.set(user(options.input));
|
|
2478
2766
|
}
|
|
2767
|
+
const temperature = RETRY_TEMPERATURES[attemptNumber - 1] ?? RETRY_TEMPERATURES[RETRY_TEMPERATURES.length - 1];
|
|
2768
|
+
const baseModel = options.model ?? groq6("openai/gpt-oss-20b");
|
|
2769
|
+
const model = wrapLanguageModel({
|
|
2770
|
+
model: baseModel,
|
|
2771
|
+
middleware: defaultSettingsMiddleware({ settings: { temperature } })
|
|
2772
|
+
});
|
|
2479
2773
|
const sqlOutput = structuredOutput({
|
|
2480
|
-
|
|
2481
|
-
model: wrapLanguageModel2({
|
|
2482
|
-
model: options.model ?? groq5("openai/gpt-oss-20b"),
|
|
2483
|
-
middleware: defaultSettingsMiddleware2({
|
|
2484
|
-
settings: {
|
|
2485
|
-
temperature: RETRY_TEMPERATURES[attemptNumber - 1] ?? 0.3,
|
|
2486
|
-
topP: 1
|
|
2487
|
-
}
|
|
2488
|
-
})
|
|
2489
|
-
}),
|
|
2774
|
+
model,
|
|
2490
2775
|
context,
|
|
2491
2776
|
schema: z5.union([
|
|
2492
2777
|
z5.object({
|
|
@@ -2669,17 +2954,11 @@ Generate ${this.options.count} questions at ${complexity} complexity.` : void 0;
|
|
|
2669
2954
|
};
|
|
2670
2955
|
|
|
2671
2956
|
// packages/text2sql/src/lib/synthesis/synthesizers/breadth-evolver.ts
|
|
2672
|
-
import { groq as
|
|
2673
|
-
import { defaultSettingsMiddleware as defaultSettingsMiddleware3, wrapLanguageModel as wrapLanguageModel3 } from "ai";
|
|
2957
|
+
import { groq as groq7 } from "@ai-sdk/groq";
|
|
2674
2958
|
import dedent5 from "dedent";
|
|
2675
2959
|
import pLimit2 from "p-limit";
|
|
2676
2960
|
import z6 from "zod";
|
|
2677
|
-
import
|
|
2678
|
-
agent as agent5,
|
|
2679
|
-
generate as generate6,
|
|
2680
|
-
toOutput,
|
|
2681
|
-
user as user7
|
|
2682
|
-
} from "@deepagents/agent";
|
|
2961
|
+
import "@deepagents/agent";
|
|
2683
2962
|
|
|
2684
2963
|
// packages/text2sql/src/lib/synthesis/synthesizers/styles.ts
|
|
2685
2964
|
var ALL_STYLES = [
|
|
@@ -2715,63 +2994,53 @@ var styleInstructions = {
|
|
|
2715
2994
|
};
|
|
2716
2995
|
|
|
2717
2996
|
// packages/text2sql/src/lib/synthesis/synthesizers/breadth-evolver.ts
|
|
2718
|
-
var
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
)
|
|
2733
|
-
}),
|
|
2734
|
-
prompt: (state) => {
|
|
2735
|
-
const personaInstruction = state?.persona ? dedent5`
|
|
2736
|
-
<persona role="${state.persona.role}">
|
|
2737
|
-
${state.persona.perspective}
|
|
2997
|
+
var paraphraserOutputSchema = z6.object({
|
|
2998
|
+
paraphrases: z6.array(
|
|
2999
|
+
z6.string().describe("A paraphrased version of the original question")
|
|
3000
|
+
).min(1).describe("List of paraphrased questions that would produce the same SQL")
|
|
3001
|
+
});
|
|
3002
|
+
async function paraphraseQuestion(params) {
|
|
3003
|
+
const context = new ContextEngine({
|
|
3004
|
+
store: new InMemoryContextStore(),
|
|
3005
|
+
chatId: `paraphraser-${crypto.randomUUID()}`,
|
|
3006
|
+
userId: "system"
|
|
3007
|
+
});
|
|
3008
|
+
const personaInstruction = params.persona ? dedent5`
|
|
3009
|
+
<persona role="${params.persona.role}">
|
|
3010
|
+
${params.persona.perspective}
|
|
2738
3011
|
|
|
2739
3012
|
Paraphrase the question as this persona would naturally ask it.
|
|
2740
3013
|
Use their vocabulary, priorities, and framing style.
|
|
2741
3014
|
</persona>
|
|
2742
3015
|
` : "";
|
|
2743
|
-
|
|
3016
|
+
const styleInstruction = params.persona?.styles && params.persona.styles.length > 0 ? dedent5`
|
|
2744
3017
|
<communication_styles>
|
|
2745
|
-
Generate paraphrases using these communication styles: ${
|
|
3018
|
+
Generate paraphrases using these communication styles: ${params.persona.styles.join(", ")}
|
|
2746
3019
|
|
|
2747
3020
|
Style definitions:
|
|
2748
|
-
${
|
|
3021
|
+
${params.persona.styles.map((s) => `- ${s}: ${styleInstructions[s]}`).join("\n")}
|
|
2749
3022
|
|
|
2750
3023
|
Distribute paraphrases across these styles for variety.
|
|
2751
3024
|
</communication_styles>
|
|
2752
3025
|
` : "";
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
${styleInstruction}
|
|
2772
|
-
|
|
2773
|
-
<task>
|
|
2774
|
-
Generate exactly ${state?.count} paraphrased versions of the original question.
|
|
3026
|
+
context.set(
|
|
3027
|
+
persona({
|
|
3028
|
+
name: "question_paraphraser",
|
|
3029
|
+
role: "You are a linguistic expert specializing in paraphrasing database questions. Your task is to generate alternative phrasings of questions that preserve the exact same semantic meaning - they must all produce the identical SQL query.",
|
|
3030
|
+
objective: "Generate paraphrased versions of questions that preserve exact semantic meaning and produce identical SQL"
|
|
3031
|
+
}),
|
|
3032
|
+
fragment("original_question", params.question),
|
|
3033
|
+
fragment(
|
|
3034
|
+
"reference_sql",
|
|
3035
|
+
params.sql,
|
|
3036
|
+
"This SQL shows what the question is really asking - all paraphrases must ask for exactly this"
|
|
3037
|
+
),
|
|
3038
|
+
...personaInstruction ? [fragment("persona", personaInstruction)] : [],
|
|
3039
|
+
...styleInstruction ? [fragment("communication_styles", styleInstruction)] : [],
|
|
3040
|
+
fragment(
|
|
3041
|
+
"task",
|
|
3042
|
+
dedent5`
|
|
3043
|
+
Generate exactly ${params.count} paraphrased versions of the original question.
|
|
2775
3044
|
|
|
2776
3045
|
Requirements:
|
|
2777
3046
|
1. Each paraphrase must be semantically equivalent - it should produce the EXACT same SQL
|
|
@@ -2779,18 +3048,30 @@ var paraphraserAgent = agent5({
|
|
|
2779
3048
|
3. Use natural language without SQL keywords (SELECT, WHERE, JOIN, etc.)
|
|
2780
3049
|
4. Keep paraphrases realistic - how actual users would ask
|
|
2781
3050
|
5. Do not add or remove any conditions, filters, or requirements from the original
|
|
2782
|
-
${
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3051
|
+
${params.persona?.styles?.length ? "6. Apply the specified communication styles to create diverse phrasings" : ""}
|
|
3052
|
+
`
|
|
3053
|
+
),
|
|
3054
|
+
guardrail({ rule: "NEVER change what data is being requested" }),
|
|
3055
|
+
guardrail({
|
|
3056
|
+
rule: "NEVER add filters, aggregations, or conditions not in the original"
|
|
3057
|
+
}),
|
|
3058
|
+
guardrail({
|
|
3059
|
+
rule: "NEVER remove any specificity from the original question"
|
|
3060
|
+
}),
|
|
3061
|
+
guardrail({
|
|
3062
|
+
rule: "All paraphrases must be answerable by the exact same SQL query"
|
|
3063
|
+
}),
|
|
3064
|
+
user(
|
|
3065
|
+
`Paraphrase this question ${params.count} times: "${params.question}"`
|
|
3066
|
+
)
|
|
3067
|
+
);
|
|
3068
|
+
const paraphraserOutput = structuredOutput({
|
|
3069
|
+
model: params.model ?? groq7("openai/gpt-oss-20b"),
|
|
3070
|
+
context,
|
|
3071
|
+
schema: paraphraserOutputSchema
|
|
3072
|
+
});
|
|
3073
|
+
return paraphraserOutput.generate();
|
|
3074
|
+
}
|
|
2794
3075
|
var BreadthEvolver = class extends PairProducer {
|
|
2795
3076
|
/**
|
|
2796
3077
|
* @param source - Source pairs or producer to evolve
|
|
@@ -2811,23 +3092,14 @@ var BreadthEvolver = class extends PairProducer {
|
|
|
2811
3092
|
for await (const chunk of this.from(this.source)) {
|
|
2812
3093
|
const tasks = chunk.map(
|
|
2813
3094
|
(pair) => this.#limit(async () => {
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
{
|
|
2823
|
-
question: pair.question,
|
|
2824
|
-
sql: pair.sql,
|
|
2825
|
-
count: this.options.count,
|
|
2826
|
-
persona: this.options.persona
|
|
2827
|
-
}
|
|
2828
|
-
)
|
|
2829
|
-
);
|
|
2830
|
-
return paraphrases.map((paraphrase) => ({
|
|
3095
|
+
const result = await paraphraseQuestion({
|
|
3096
|
+
question: pair.question,
|
|
3097
|
+
sql: pair.sql,
|
|
3098
|
+
count: this.options.count,
|
|
3099
|
+
persona: this.options.persona,
|
|
3100
|
+
model: this.options.model
|
|
3101
|
+
});
|
|
3102
|
+
return result.paraphrases.map((paraphrase) => ({
|
|
2831
3103
|
question: paraphrase,
|
|
2832
3104
|
sql: pair.sql,
|
|
2833
3105
|
context: pair.context,
|
|
@@ -2842,18 +3114,13 @@ var BreadthEvolver = class extends PairProducer {
|
|
|
2842
3114
|
};
|
|
2843
3115
|
|
|
2844
3116
|
// packages/text2sql/src/lib/synthesis/synthesizers/depth-evolver.ts
|
|
2845
|
-
import { groq as
|
|
2846
|
-
import {
|
|
2847
|
-
NoObjectGeneratedError as NoObjectGeneratedError2,
|
|
2848
|
-
NoOutputGeneratedError as NoOutputGeneratedError2,
|
|
2849
|
-
defaultSettingsMiddleware as defaultSettingsMiddleware4,
|
|
2850
|
-
wrapLanguageModel as wrapLanguageModel4
|
|
2851
|
-
} from "ai";
|
|
3117
|
+
import { groq as groq8 } from "@ai-sdk/groq";
|
|
3118
|
+
import { NoObjectGeneratedError as NoObjectGeneratedError2, NoOutputGeneratedError as NoOutputGeneratedError2 } from "ai";
|
|
2852
3119
|
import dedent6 from "dedent";
|
|
2853
3120
|
import pLimit3 from "p-limit";
|
|
2854
3121
|
import pRetry2 from "p-retry";
|
|
2855
3122
|
import z7 from "zod";
|
|
2856
|
-
import
|
|
3123
|
+
import "@deepagents/agent";
|
|
2857
3124
|
var techniqueInstructions = {
|
|
2858
3125
|
"add-aggregation": dedent6`
|
|
2859
3126
|
Add aggregation requirements to the question.
|
|
@@ -2896,44 +3163,37 @@ var techniqueInstructions = {
|
|
|
2896
3163
|
- "Get costs" → "What would be the impact of a 10% discount on profit margins?"
|
|
2897
3164
|
`
|
|
2898
3165
|
};
|
|
2899
|
-
var
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
<technique name="${state?.technique}">
|
|
2932
|
-
${state?.techniqueInstruction}
|
|
2933
|
-
</technique>
|
|
2934
|
-
|
|
2935
|
-
<task>
|
|
2936
|
-
Evolve the original question using the "${state?.technique}" technique.
|
|
3166
|
+
var evolverOutputSchema = z7.object({
|
|
3167
|
+
evolvedQuestion: z7.string().describe("The evolved, more complex version of the original question")
|
|
3168
|
+
});
|
|
3169
|
+
async function evolveQuestion(params) {
|
|
3170
|
+
const context = new ContextEngine({
|
|
3171
|
+
store: new InMemoryContextStore(),
|
|
3172
|
+
chatId: `evolver-${crypto.randomUUID()}`,
|
|
3173
|
+
userId: "system"
|
|
3174
|
+
});
|
|
3175
|
+
context.set(
|
|
3176
|
+
persona({
|
|
3177
|
+
name: "question_evolver",
|
|
3178
|
+
role: "You are an expert at evolving simple database questions into more complex ones. Your task is to take a basic question and transform it into a more sophisticated version that requires advanced SQL techniques to answer.",
|
|
3179
|
+
objective: "Transform simple questions into complex versions requiring advanced SQL techniques"
|
|
3180
|
+
}),
|
|
3181
|
+
fragment("original_question", params.question),
|
|
3182
|
+
fragment(
|
|
3183
|
+
"original_sql",
|
|
3184
|
+
params.sql,
|
|
3185
|
+
"(This shows what the original question required)"
|
|
3186
|
+
),
|
|
3187
|
+
fragment("database_schema", params.schema),
|
|
3188
|
+
fragment(
|
|
3189
|
+
"technique",
|
|
3190
|
+
{ name: params.technique },
|
|
3191
|
+
params.techniqueInstruction
|
|
3192
|
+
),
|
|
3193
|
+
fragment(
|
|
3194
|
+
"task",
|
|
3195
|
+
dedent6`
|
|
3196
|
+
Evolve the original question using the "${params.technique}" technique.
|
|
2937
3197
|
|
|
2938
3198
|
Requirements:
|
|
2939
3199
|
1. The evolved question must be MORE COMPLEX than the original
|
|
@@ -2942,17 +3202,29 @@ var questionEvolverAgent = agent6({
|
|
|
2942
3202
|
4. Use natural language - no SQL keywords
|
|
2943
3203
|
5. Keep the question realistic and practical
|
|
2944
3204
|
6. The evolved question should build upon the original topic/domain
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
})
|
|
3205
|
+
`
|
|
3206
|
+
),
|
|
3207
|
+
guardrail({
|
|
3208
|
+
rule: "The evolved question MUST require more complex SQL than the original"
|
|
3209
|
+
}),
|
|
3210
|
+
guardrail({
|
|
3211
|
+
rule: "Do not ask for data that does not exist in the schema"
|
|
3212
|
+
}),
|
|
3213
|
+
guardrail({
|
|
3214
|
+
rule: "Keep the question grounded in the same domain as the original"
|
|
3215
|
+
}),
|
|
3216
|
+
guardrail({ rule: "Make sure the question is clear and unambiguous" }),
|
|
3217
|
+
user(
|
|
3218
|
+
`Evolve this question using "${params.technique}": "${params.question}"`
|
|
3219
|
+
)
|
|
3220
|
+
);
|
|
3221
|
+
const evolverOutput = structuredOutput({
|
|
3222
|
+
model: params.model ?? groq8("openai/gpt-oss-20b"),
|
|
3223
|
+
context,
|
|
3224
|
+
schema: evolverOutputSchema
|
|
3225
|
+
});
|
|
3226
|
+
return evolverOutput.generate();
|
|
3227
|
+
}
|
|
2956
3228
|
var ALL_TECHNIQUES = [
|
|
2957
3229
|
"add-aggregation",
|
|
2958
3230
|
"add-filter",
|
|
@@ -2998,22 +3270,17 @@ var DepthEvolver = class extends PairProducer {
|
|
|
2998
3270
|
}
|
|
2999
3271
|
}
|
|
3000
3272
|
async #processTask(pair, technique, introspection) {
|
|
3001
|
-
const
|
|
3002
|
-
() =>
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
schema: introspection,
|
|
3011
|
-
technique,
|
|
3012
|
-
techniqueInstruction: techniqueInstructions[technique]
|
|
3013
|
-
}
|
|
3014
|
-
)
|
|
3273
|
+
const output = await withRetry2(
|
|
3274
|
+
() => evolveQuestion({
|
|
3275
|
+
question: pair.question,
|
|
3276
|
+
sql: pair.sql,
|
|
3277
|
+
schema: introspection,
|
|
3278
|
+
technique,
|
|
3279
|
+
techniqueInstruction: techniqueInstructions[technique],
|
|
3280
|
+
model: this.options?.model
|
|
3281
|
+
})
|
|
3015
3282
|
);
|
|
3016
|
-
const evolvedQuestion =
|
|
3283
|
+
const evolvedQuestion = output.evolvedQuestion;
|
|
3017
3284
|
try {
|
|
3018
3285
|
const sqlResult = await toSql({
|
|
3019
3286
|
input: evolvedQuestion,
|
|
@@ -3069,12 +3336,11 @@ async function withRetry2(computation) {
|
|
|
3069
3336
|
}
|
|
3070
3337
|
|
|
3071
3338
|
// packages/text2sql/src/lib/synthesis/synthesizers/persona-generator.ts
|
|
3072
|
-
import { groq as
|
|
3073
|
-
import { defaultSettingsMiddleware as defaultSettingsMiddleware5, wrapLanguageModel as wrapLanguageModel5 } from "ai";
|
|
3339
|
+
import { groq as groq9 } from "@ai-sdk/groq";
|
|
3074
3340
|
import dedent7 from "dedent";
|
|
3075
3341
|
import z8 from "zod";
|
|
3076
3342
|
import "@deepagents/agent";
|
|
3077
|
-
var
|
|
3343
|
+
var outputSchema3 = z8.object({
|
|
3078
3344
|
personas: z8.array(
|
|
3079
3345
|
z8.object({
|
|
3080
3346
|
role: z8.string().describe("The job title or role of this persona"),
|
|
@@ -3092,48 +3358,52 @@ async function generatePersonas(schemaFragments, options) {
|
|
|
3092
3358
|
const count = options?.count ?? 5;
|
|
3093
3359
|
const context = new ContextEngine({
|
|
3094
3360
|
store: new InMemoryContextStore(),
|
|
3095
|
-
chatId: `persona-gen-${crypto.randomUUID()}
|
|
3361
|
+
chatId: `persona-gen-${crypto.randomUUID()}`,
|
|
3362
|
+
userId: "system"
|
|
3096
3363
|
});
|
|
3097
3364
|
context.set(
|
|
3098
3365
|
persona({
|
|
3099
3366
|
name: "persona_generator",
|
|
3100
|
-
role: "You are an expert at understanding database schemas and inferring who would use them."
|
|
3367
|
+
role: "You are an expert at understanding database schemas and inferring who would use them.",
|
|
3368
|
+
objective: "Generate realistic personas representing users who would query this database"
|
|
3101
3369
|
}),
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3370
|
+
fragment("database_schema", schema),
|
|
3371
|
+
fragment(
|
|
3372
|
+
"task",
|
|
3373
|
+
dedent7`
|
|
3374
|
+
Analyze the database schema and generate realistic personas representing
|
|
3375
|
+
the different types of users who would query this database.
|
|
3376
|
+
|
|
3377
|
+
For each persona, provide:
|
|
3378
|
+
1. **role**: Their job title or role (e.g., "Financial Analyst", "Customer Support Rep")
|
|
3379
|
+
2. **perspective**: A rich description of what they care about, including:
|
|
3380
|
+
- What questions they typically ask
|
|
3381
|
+
- What metrics/data points matter to them
|
|
3382
|
+
- How they prefer data formatted or presented
|
|
3383
|
+
- Their priorities (speed vs accuracy, detail vs summary)
|
|
3384
|
+
- Domain-specific concerns relevant to their role
|
|
3385
|
+
3. **styles**: 1-3 communication styles typical for this persona. Choose from:
|
|
3386
|
+
- formal: Professional business language, complete sentences
|
|
3387
|
+
- colloquial: Casual everyday speech, contractions
|
|
3388
|
+
- imperative: Commands like "Show me...", "Get...", "List..."
|
|
3389
|
+
- interrogative: Questions like "What is...", "How many..."
|
|
3390
|
+
- descriptive: Verbose, detailed phrasing
|
|
3391
|
+
- concise: Brief, minimal words
|
|
3392
|
+
- vague: Ambiguous, hedging language
|
|
3393
|
+
- metaphorical: Figurative language, analogies
|
|
3394
|
+
- conversational: Chat-like, casual tone
|
|
3109
3395
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
- imperative: Commands like "Show me...", "Get...", "List..."
|
|
3122
|
-
- interrogative: Questions like "What is...", "How many..."
|
|
3123
|
-
- descriptive: Verbose, detailed phrasing
|
|
3124
|
-
- concise: Brief, minimal words
|
|
3125
|
-
- vague: Ambiguous, hedging language
|
|
3126
|
-
- metaphorical: Figurative language, analogies
|
|
3127
|
-
- conversational: Chat-like, casual tone
|
|
3128
|
-
|
|
3129
|
-
Requirements:
|
|
3130
|
-
- Personas should be realistic for the given schema
|
|
3131
|
-
- Each persona should have distinct concerns and priorities
|
|
3132
|
-
- Perspectives should be detailed enough to guide question paraphrasing
|
|
3133
|
-
- Cover different levels of technical expertise (some technical, some business-focused)
|
|
3134
|
-
- Styles should match how this persona would naturally communicate
|
|
3135
|
-
|
|
3136
|
-
<example>
|
|
3396
|
+
Requirements:
|
|
3397
|
+
- Personas should be realistic for the given schema
|
|
3398
|
+
- Each persona should have distinct concerns and priorities
|
|
3399
|
+
- Perspectives should be detailed enough to guide question paraphrasing
|
|
3400
|
+
- Cover different levels of technical expertise (some technical, some business-focused)
|
|
3401
|
+
- Styles should match how this persona would naturally communicate
|
|
3402
|
+
`
|
|
3403
|
+
),
|
|
3404
|
+
fragment(
|
|
3405
|
+
"example",
|
|
3406
|
+
dedent7`
|
|
3137
3407
|
For an e-commerce schema with orders, customers, products tables:
|
|
3138
3408
|
|
|
3139
3409
|
{
|
|
@@ -3147,40 +3417,36 @@ async function generatePersonas(schemaFragments, options) {
|
|
|
3147
3417
|
"perspective": "As inventory manager, I care about:\\n- Current stock levels and reorder points\\n- Product availability across warehouses\\n- Slow-moving inventory identification\\n- Supplier lead times and pending orders\\n- I need accurate counts, often aggregated by location",
|
|
3148
3418
|
"styles": ["formal", "interrogative"]
|
|
3149
3419
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3420
|
+
`
|
|
3421
|
+
),
|
|
3422
|
+
guardrail({
|
|
3423
|
+
rule: "Only generate personas relevant to the actual schema provided"
|
|
3424
|
+
}),
|
|
3425
|
+
guardrail({
|
|
3426
|
+
rule: "Do not invent tables or data that do not exist in the schema"
|
|
3427
|
+
}),
|
|
3428
|
+
guardrail({
|
|
3429
|
+
rule: "Ensure perspectives are specific to the domain, not generic"
|
|
3430
|
+
}),
|
|
3431
|
+
user(
|
|
3159
3432
|
`Generate exactly ${count} distinct personas who would query this database.`
|
|
3160
3433
|
)
|
|
3161
3434
|
);
|
|
3162
3435
|
const personaOutput = structuredOutput({
|
|
3163
|
-
|
|
3164
|
-
model: wrapLanguageModel5({
|
|
3165
|
-
model: options?.model ?? groq8("openai/gpt-oss-20b"),
|
|
3166
|
-
middleware: defaultSettingsMiddleware5({
|
|
3167
|
-
settings: { temperature: 0.8, topP: 0.95, presencePenalty: 0.2 }
|
|
3168
|
-
})
|
|
3169
|
-
}),
|
|
3436
|
+
model: options?.model ?? groq9("openai/gpt-oss-20b"),
|
|
3170
3437
|
context,
|
|
3171
|
-
schema:
|
|
3438
|
+
schema: outputSchema3
|
|
3172
3439
|
});
|
|
3173
3440
|
const output = await personaOutput.generate();
|
|
3174
3441
|
return output.personas;
|
|
3175
3442
|
}
|
|
3176
3443
|
|
|
3177
3444
|
// packages/text2sql/src/lib/agents/teachables.agent.ts
|
|
3178
|
-
import { groq as
|
|
3179
|
-
import { defaultSettingsMiddleware as defaultSettingsMiddleware6, wrapLanguageModel as wrapLanguageModel6 } from "ai";
|
|
3445
|
+
import { groq as groq10 } from "@ai-sdk/groq";
|
|
3180
3446
|
import dedent8 from "dedent";
|
|
3181
3447
|
import z9 from "zod";
|
|
3182
|
-
import
|
|
3183
|
-
var
|
|
3448
|
+
import "@deepagents/agent";
|
|
3449
|
+
var outputSchema4 = z9.object({
|
|
3184
3450
|
terms: z9.array(z9.object({ name: z9.string(), definition: z9.string() })).optional().describe("Domain terminology definitions"),
|
|
3185
3451
|
hints: z9.array(z9.object({ text: z9.string() })).optional().describe("Helpful hints for SQL generation"),
|
|
3186
3452
|
guardrails: z9.array(
|
|
@@ -3231,62 +3497,58 @@ var outputSchema2 = z9.object({
|
|
|
3231
3497
|
})
|
|
3232
3498
|
).optional().describe("Concept analogies")
|
|
3233
3499
|
});
|
|
3234
|
-
var teachablesAuthorAgent = agent7({
|
|
3235
|
-
name: "teachables-author",
|
|
3236
|
-
model: wrapLanguageModel6({
|
|
3237
|
-
model: groq9("openai/gpt-oss-20b"),
|
|
3238
|
-
middleware: defaultSettingsMiddleware6({
|
|
3239
|
-
settings: { temperature: 0.4, topP: 0.95 }
|
|
3240
|
-
})
|
|
3241
|
-
}),
|
|
3242
|
-
output: outputSchema2,
|
|
3243
|
-
prompt: (state) => dedent8`
|
|
3244
|
-
<identity>
|
|
3245
|
-
You design "fragments" for a Text2SQL system. Fragments become structured XML instructions.
|
|
3246
|
-
Choose only high-impact items that improve accuracy, safety, or clarity for this database.
|
|
3247
|
-
</identity>
|
|
3248
|
-
|
|
3249
|
-
<database_schema>
|
|
3250
|
-
${state?.schema}
|
|
3251
|
-
</database_schema>
|
|
3252
|
-
|
|
3253
|
-
${state?.context ? `<additional_context>${state.context}</additional_context>` : ""}
|
|
3254
|
-
|
|
3255
|
-
<output_structure>
|
|
3256
|
-
Output a JSON object with these optional arrays (include only relevant ones):
|
|
3257
|
-
- terms: [{ name: string, definition: string }] - Domain terminology
|
|
3258
|
-
- hints: [{ text: string }] - Helpful SQL generation hints
|
|
3259
|
-
- guardrails: [{ rule: string, reason?: string, action?: string }] - Safety constraints
|
|
3260
|
-
- explains: [{ concept: string, explanation: string, therefore?: string }] - Concept explanations
|
|
3261
|
-
- examples: [{ question: string, answer: string, note?: string }] - Q&A examples
|
|
3262
|
-
- clarifications: [{ when: string, ask: string, reason: string }] - Clarification triggers
|
|
3263
|
-
- workflows: [{ task: string, steps: string[], triggers?: string[], notes?: string }] - Multi-step tasks
|
|
3264
|
-
- quirks: [{ issue: string, workaround: string }] - Known issues
|
|
3265
|
-
- styleGuides: [{ prefer: string, never?: string, always?: string }] - SQL style rules
|
|
3266
|
-
- analogies: [{ concepts: string[], relationship: string, insight?: string, therefore?: string, pitfall?: string }]
|
|
3267
|
-
</output_structure>
|
|
3268
|
-
|
|
3269
|
-
<instructions>
|
|
3270
|
-
1. Analyze the schema to infer domain, relationships, and sensitive columns.
|
|
3271
|
-
2. Generate 3-10 fragments total across all categories, prioritizing:
|
|
3272
|
-
- guardrails for PII columns (email, ssn, phone, etc)
|
|
3273
|
-
- hints for status/enum columns
|
|
3274
|
-
- clarifications for ambiguous terms
|
|
3275
|
-
3. Ground everything in the schema - do not invent tables/columns.
|
|
3276
|
-
4. Only include categories that are relevant to this schema.
|
|
3277
|
-
</instructions>
|
|
3278
|
-
`
|
|
3279
|
-
});
|
|
3280
3500
|
async function toTeachings(input, options) {
|
|
3281
|
-
const
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3501
|
+
const context = new ContextEngine({
|
|
3502
|
+
store: new InMemoryContextStore(),
|
|
3503
|
+
chatId: `teachables-gen-${crypto.randomUUID()}`,
|
|
3504
|
+
userId: "system"
|
|
3505
|
+
});
|
|
3506
|
+
context.set(
|
|
3507
|
+
persona({
|
|
3508
|
+
name: "teachables-author",
|
|
3509
|
+
role: 'You design "fragments" for a Text2SQL system. Fragments become structured XML instructions.',
|
|
3510
|
+
objective: "Choose only high-impact items that improve accuracy, safety, or clarity for this database"
|
|
3511
|
+
}),
|
|
3512
|
+
fragment("database_schema", input.schema),
|
|
3513
|
+
...input.context ? [fragment("additional_context", input.context)] : [],
|
|
3514
|
+
fragment(
|
|
3515
|
+
"output_structure",
|
|
3516
|
+
dedent8`
|
|
3517
|
+
Output a JSON object with these optional arrays (include only relevant ones):
|
|
3518
|
+
- terms: [{ name: string, definition: string }] - Domain terminology
|
|
3519
|
+
- hints: [{ text: string }] - Helpful SQL generation hints
|
|
3520
|
+
- guardrails: [{ rule: string, reason?: string, action?: string }] - Safety constraints
|
|
3521
|
+
- explains: [{ concept: string, explanation: string, therefore?: string }] - Concept explanations
|
|
3522
|
+
- examples: [{ question: string, answer: string, note?: string }] - Q&A examples
|
|
3523
|
+
- clarifications: [{ when: string, ask: string, reason: string }] - Clarification triggers
|
|
3524
|
+
- workflows: [{ task: string, steps: string[], triggers?: string[], notes?: string }] - Multi-step tasks
|
|
3525
|
+
- quirks: [{ issue: string, workaround: string }] - Known issues
|
|
3526
|
+
- styleGuides: [{ prefer: string, never?: string, always?: string }] - SQL style rules
|
|
3527
|
+
- analogies: [{ concepts: string[], relationship: string, insight?: string, therefore?: string, pitfall?: string }]
|
|
3528
|
+
`
|
|
3529
|
+
),
|
|
3530
|
+
fragment(
|
|
3531
|
+
"task",
|
|
3532
|
+
dedent8`
|
|
3533
|
+
1. Analyze the schema to infer domain, relationships, and sensitive columns.
|
|
3534
|
+
2. Generate 3-10 fragments total across all categories, prioritizing:
|
|
3535
|
+
- guardrails for PII columns (email, ssn, phone, etc)
|
|
3536
|
+
- hints for status/enum columns
|
|
3537
|
+
- clarifications for ambiguous terms
|
|
3538
|
+
3. Ground everything in the schema - do not invent tables/columns.
|
|
3539
|
+
4. Only include categories that are relevant to this schema.
|
|
3540
|
+
`
|
|
3541
|
+
),
|
|
3542
|
+
user(
|
|
3543
|
+
`Analyze this database schema and generate fragments that will help an AI generate accurate SQL queries.`
|
|
3544
|
+
)
|
|
3289
3545
|
);
|
|
3546
|
+
const teachablesOutput = structuredOutput({
|
|
3547
|
+
model: options?.model ?? groq10("openai/gpt-oss-20b"),
|
|
3548
|
+
context,
|
|
3549
|
+
schema: outputSchema4
|
|
3550
|
+
});
|
|
3551
|
+
const result = await teachablesOutput.generate();
|
|
3290
3552
|
const fragments = [];
|
|
3291
3553
|
result.terms?.forEach((t) => fragments.push(term(t.name, t.definition)));
|
|
3292
3554
|
result.hints?.forEach((h) => fragments.push(hint(h.text)));
|
|
@@ -3383,11 +3645,11 @@ export {
|
|
|
3383
3645
|
SqlExtractor,
|
|
3384
3646
|
ValidatedProducer,
|
|
3385
3647
|
WindowedContextExtractor,
|
|
3386
|
-
contextResolverAgent,
|
|
3387
3648
|
formatConversation,
|
|
3388
3649
|
generatePersonas,
|
|
3389
3650
|
generateTeachings,
|
|
3390
3651
|
getMessageText,
|
|
3652
|
+
resolveContext,
|
|
3391
3653
|
styleInstructions,
|
|
3392
3654
|
toPairs
|
|
3393
3655
|
};
|