@deepagents/text2sql 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,6 +17,15 @@ AI-powered natural language to SQL. Ask questions in plain English, get executab
17
17
  npm install @deepagents/text2sql
18
18
  ```
19
19
 
20
+ Install the database driver or client library that matches your adapter:
21
+
22
+ ```bash
23
+ npm install pg # PostgreSQL
24
+ npm install mssql # SQL Server
25
+ npm install mysql2 # MySQL / MariaDB
26
+ npm install @google-cloud/bigquery # BigQuery
27
+ ```
28
+
20
29
  Requires Node.js LTS (20+).
21
30
 
22
31
  ## Quick Start
@@ -32,6 +41,7 @@ import {
32
41
  createBashTool,
33
42
  createRoutingSandbox,
34
43
  createVirtualSandbox,
44
+ user,
35
45
  } from '@deepagents/context';
36
46
  import { Text2Sql, sqlSandboxExtension } from '@deepagents/text2sql';
37
47
  import {
@@ -50,6 +60,11 @@ const pool = new pg.Pool({
50
60
  });
51
61
 
52
62
  const store = new InMemoryContextStore();
63
+ const engine = new ContextEngine({
64
+ store,
65
+ chatId: 'chat-123',
66
+ userId: 'user-456',
67
+ });
53
68
  const adapter = new Postgres({
54
69
  execute: async (sql) => {
55
70
  const result = await pool.query(sql);
@@ -77,15 +92,7 @@ const text2sql = new Text2Sql({
77
92
  model: groq('openai/gpt-oss-20b'),
78
93
  sandbox,
79
94
  adapters: { main: adapter },
80
- context: (...fragments) => {
81
- const engine = new ContextEngine({
82
- store,
83
- chatId: 'chat-123',
84
- userId: 'user-456',
85
- });
86
- engine.set(...fragments);
87
- return engine;
88
- },
95
+ context: engine,
89
96
  });
90
97
 
91
98
  // Generate SQL
@@ -121,10 +128,10 @@ for composition patterns.
121
128
 
122
129
  ## Fragments
123
130
 
124
- Inject domain knowledge using fragments from `@deepagents/context` inside the
125
- `context` factory you pass to `Text2Sql`. Those fragments affect `chat()`
126
- sessions. If you need direct SQL generation with extra fragments, use the
127
- lower-level `toSql({ fragments })` helper exported from `@deepagents/text2sql`.
131
+ Inject domain knowledge by setting fragments on the `ContextEngine` you pass to
132
+ `Text2Sql`. Those fragments affect `chat()` sessions. If you need direct SQL
133
+ generation with extra fragments, use the lower-level `toSql({ fragments })`
134
+ helper exported from `@deepagents/text2sql`.
128
135
 
129
136
  ```typescript
130
137
  import {
@@ -137,31 +144,25 @@ import {
137
144
  } from '@deepagents/context';
138
145
 
139
146
  const store = new InMemoryContextStore();
147
+ const engine = new ContextEngine({
148
+ store,
149
+ chatId: 'chat-123',
150
+ userId: 'user-456',
151
+ });
140
152
 
141
- const context = (...fragments) => {
142
- const engine = new ContextEngine({
143
- store,
144
- chatId: 'chat-123',
145
- userId: 'user-456',
146
- });
147
-
148
- engine.set(
149
- term('MRR', 'monthly recurring revenue'),
150
- hint('Always exclude test accounts with email ending in @test.com'),
151
- guardrail({
152
- rule: 'Never expose individual salaries',
153
- reason: 'Confidential HR data',
154
- action: 'Aggregate by department instead',
155
- }),
156
- example({
157
- question: 'show me churned customers',
158
- answer: `SELECT * FROM customers WHERE status = 'churned' ORDER BY churned_at DESC`,
159
- }),
160
- ...fragments,
161
- );
162
-
163
- return engine;
164
- };
153
+ engine.set(
154
+ term('MRR', 'monthly recurring revenue'),
155
+ hint('Always exclude test accounts with email ending in @test.com'),
156
+ guardrail({
157
+ rule: 'Never expose individual salaries',
158
+ reason: 'Confidential HR data',
159
+ action: 'Aggregate by department instead',
160
+ }),
161
+ example({
162
+ question: 'show me churned customers',
163
+ answer: `SELECT * FROM customers WHERE status = 'churned' ORDER BY churned_at DESC`,
164
+ }),
165
+ );
165
166
  ```
166
167
 
167
168
  **Domain fragments** (11 types): `term`, `hint`, `guardrail`, `example`, `explain`, `clarification`, `workflow`, `quirk`, `styleGuide`, `analogy`, `glossary`.
@@ -187,23 +188,68 @@ Control what schema metadata the AI receives:
187
188
 
188
189
  ## Conversations
189
190
 
190
- `chat()` persists history through the `ContextEngine` returned by your `context` factory. Reuse the same store, `chatId`, and `userId` to continue the same thread:
191
+ `chat()` persists history through the `ContextEngine` you pass to `Text2Sql`.
192
+ Reuse the same store, `chatId`, and `userId` to continue the same thread.
193
+ Before each turn, append only the new incoming message to the engine; do not
194
+ replay earlier turns that are already stored in the context store:
191
195
 
192
196
  ```typescript
193
- const stream = await text2sql.chat([
194
- { role: 'user', content: 'Show me orders from last month' },
195
- ]);
197
+ await engine.continue(user('Show me orders from last month'));
198
+ const stream = await text2sql.chat();
196
199
 
197
200
  for await (const chunk of stream) {
198
201
  // handle streaming response
199
202
  }
200
203
 
201
204
  // Continue the same conversation
202
- const followUp = await text2sql.chat([
203
- { role: 'user', content: 'Now filter to only completed ones' },
204
- ]);
205
+ await engine.continue(user('Now filter to only completed ones'));
206
+ const followUp = await text2sql.chat();
207
+ for await (const chunk of followUp) {
208
+ // handle streaming response
209
+ }
205
210
  ```
206
211
 
212
+ While `chat()` builds or reads cached schema context, it emits progress chunks
213
+ before assistant text starts:
214
+
215
+ ```typescript
216
+ import {
217
+ TEXT2SQL_INDEX_PROGRESS_CHUNK,
218
+ type Text2SqlIndexProgressEvent,
219
+ } from '@deepagents/text2sql';
220
+
221
+ await engine.continue(user('Show me top 10 customers'));
222
+ const stream = await text2sql.chat();
223
+ const startedAt = new Map<string, number>();
224
+
225
+ for await (const chunk of stream) {
226
+ if (chunk.type === TEXT2SQL_INDEX_PROGRESS_CHUNK) {
227
+ const event = chunk.data as Text2SqlIndexProgressEvent;
228
+ const scope =
229
+ [event.adapter, event.phase].filter(Boolean).join(':') || 'index';
230
+
231
+ if (event.type.endsWith(':start') || event.type === 'index:start') {
232
+ startedAt.set(scope, event.timestampMs ?? Date.now());
233
+ }
234
+
235
+ if (event.type.endsWith(':end') || event.type.endsWith(':error')) {
236
+ const durationMs =
237
+ (event.timestampMs ?? Date.now()) -
238
+ (startedAt.get(scope) ?? event.timestampMs ?? Date.now());
239
+ console.log(event.message, durationMs);
240
+ }
241
+
242
+ continue;
243
+ }
244
+
245
+ // handle assistant stream chunks
246
+ }
247
+ ```
248
+
249
+ Each progress event includes a generic Unix epoch `timestampMs` so clients can
250
+ calculate per-index, per-adapter, or per-phase durations without Text2SQL
251
+ imposing one duration policy.
252
+
207
253
  `toSql()` is stateless. It reads the current schema fragments, but it does not
208
254
  write messages, titles, or usage into your context store.
209
255
 
package/dist/index.js CHANGED
@@ -163,6 +163,17 @@ function relationship(input) {
163
163
  }
164
164
 
165
165
  // packages/text2sql/src/lib/adapters/groundings/context.ts
166
+ var noopProgress = () => {
167
+ };
168
+ function timestampProgress(onProgress) {
169
+ if (!onProgress) return noopProgress;
170
+ return (progress) => {
171
+ onProgress({
172
+ ...progress,
173
+ timestampMs: progress.timestampMs ?? Date.now()
174
+ });
175
+ };
176
+ }
166
177
  function createGroundingContext(options = {}) {
167
178
  return {
168
179
  tables: [],
@@ -170,7 +181,7 @@ function createGroundingContext(options = {}) {
170
181
  relationships: [],
171
182
  info: void 0,
172
183
  cache: /* @__PURE__ */ new Map(),
173
- onProgress: options.onProgress
184
+ onProgress: timestampProgress(options.onProgress)
174
185
  };
175
186
  }
176
187
 
@@ -317,7 +328,7 @@ var Adapter = class {
317
328
  for (const fn of this.grounding) {
318
329
  const grounding = fn(this);
319
330
  if (grounding.phase) {
320
- ctx.onProgress?.({
331
+ ctx.onProgress({
321
332
  type: "phase:start",
322
333
  phase: grounding.phase,
323
334
  message: `Starting ${grounding.name} introspection...`
@@ -327,7 +338,7 @@ var Adapter = class {
327
338
  await grounding.execute(ctx);
328
339
  } finally {
329
340
  if (grounding.phase) {
330
- ctx.onProgress?.({
341
+ ctx.onProgress({
331
342
  type: "phase:end",
332
343
  phase: grounding.phase,
333
344
  message: `Finished ${grounding.name} introspection.`
@@ -5238,6 +5249,17 @@ import {
5238
5249
  fragment as fragment3
5239
5250
  } from "@deepagents/context";
5240
5251
  var TEXT2SQL_INDEX_PROGRESS_CHUNK = "data-text2sql-index-progress";
5252
+ var noopProgressHandler = () => {
5253
+ };
5254
+ function timestampProgressHandler(onProgress) {
5255
+ if (!onProgress) return noopProgressHandler;
5256
+ return (event) => {
5257
+ onProgress({
5258
+ ...event,
5259
+ timestampMs: event.timestampMs ?? Date.now()
5260
+ });
5261
+ };
5262
+ }
5241
5263
  var Text2Sql = class {
5242
5264
  #config;
5243
5265
  #introspection;
@@ -5283,8 +5305,9 @@ var Text2Sql = class {
5283
5305
  return this.#index();
5284
5306
  }
5285
5307
  async #index(onProgress) {
5308
+ const progress = timestampProgressHandler(onProgress);
5286
5309
  const entries = Object.entries(this.#config.adapters);
5287
- onProgress?.({
5310
+ progress({
5288
5311
  type: "index:start",
5289
5312
  message: `Indexing ${entries.length} adapter${entries.length === 1 ? "" : "s"}...`,
5290
5313
  current: 0,
@@ -5292,13 +5315,13 @@ var Text2Sql = class {
5292
5315
  });
5293
5316
  const settled = await Promise.allSettled(
5294
5317
  entries.map(async ([name, adapter]) => {
5295
- const schema = await this.#indexAdapter(name, adapter, onProgress);
5318
+ const schema = await this.#indexAdapter(name, adapter, progress);
5296
5319
  return fragment3(name, ...schema);
5297
5320
  })
5298
5321
  );
5299
5322
  const failed = settled.find((result) => result.status === "rejected");
5300
5323
  if (failed) {
5301
- onProgress?.({
5324
+ progress({
5302
5325
  type: "index:error",
5303
5326
  message: failed.reason instanceof Error ? failed.reason.message : String(failed.reason)
5304
5327
  });
@@ -5310,7 +5333,7 @@ var Text2Sql = class {
5310
5333
  }
5311
5334
  return result.value;
5312
5335
  });
5313
- onProgress?.({
5336
+ progress({
5314
5337
  type: "index:end",
5315
5338
  message: "Finished indexing adapters.",
5316
5339
  current: entries.length,
@@ -5318,8 +5341,8 @@ var Text2Sql = class {
5318
5341
  });
5319
5342
  return wrapped;
5320
5343
  }
5321
- async #indexAdapter(name, adapter, onProgress) {
5322
- onProgress?.({
5344
+ async #indexAdapter(name, adapter, onProgress = noopProgressHandler) {
5345
+ onProgress({
5323
5346
  type: "adapter:start",
5324
5347
  adapter: name,
5325
5348
  message: `Indexing adapter "${name}"...`
@@ -5331,13 +5354,13 @@ var Text2Sql = class {
5331
5354
  try {
5332
5355
  const cached = await cache.read();
5333
5356
  if (cached) {
5334
- onProgress?.({
5357
+ onProgress({
5335
5358
  type: "adapter:cache-hit",
5336
5359
  adapter: name,
5337
5360
  message: `Using cached index for adapter "${name}".`,
5338
5361
  cached: true
5339
5362
  });
5340
- onProgress?.({
5363
+ onProgress({
5341
5364
  type: "adapter:end",
5342
5365
  adapter: name,
5343
5366
  message: `Finished indexing adapter "${name}".`,
@@ -5345,18 +5368,18 @@ var Text2Sql = class {
5345
5368
  });
5346
5369
  return cached;
5347
5370
  }
5348
- onProgress?.({
5371
+ onProgress({
5349
5372
  type: "adapter:cache-miss",
5350
5373
  adapter: name,
5351
5374
  message: `No cached index for adapter "${name}".`,
5352
5375
  cached: false
5353
5376
  });
5354
5377
  const ctx = createGroundingContext({
5355
- onProgress: (progress) => onProgress?.(this.#adapterProgressEvent(name, progress))
5378
+ onProgress: (event) => onProgress(this.#adapterProgressEvent(name, event))
5356
5379
  });
5357
5380
  const fragments = await adapter.introspect(ctx);
5358
5381
  await cache.write(fragments);
5359
- onProgress?.({
5382
+ onProgress({
5360
5383
  type: "adapter:end",
5361
5384
  adapter: name,
5362
5385
  message: `Finished indexing adapter "${name}".`,
@@ -5365,7 +5388,7 @@ var Text2Sql = class {
5365
5388
  return fragments;
5366
5389
  } catch (error) {
5367
5390
  const reason = error instanceof Error ? error.message : String(error);
5368
- onProgress?.({
5391
+ onProgress({
5369
5392
  type: "adapter:error",
5370
5393
  adapter: name,
5371
5394
  message: `Failed indexing adapter "${name}": ${reason}`
@@ -5384,7 +5407,8 @@ var Text2Sql = class {
5384
5407
  message: progress.message,
5385
5408
  current: progress.current,
5386
5409
  total: progress.total,
5387
- cached: false
5410
+ cached: false,
5411
+ timestampMs: progress.timestampMs
5388
5412
  };
5389
5413
  }
5390
5414
  async toPairs(adapterName, factory) {
@@ -5392,22 +5416,24 @@ var Text2Sql = class {
5392
5416
  const producer = factory(adapter);
5393
5417
  return producer.toPairs();
5394
5418
  }
5395
- async chat(messages, options) {
5396
- if (messages.length === 0) {
5397
- throw new Error("messages must not be empty");
5398
- }
5419
+ async chat(options) {
5399
5420
  return createUIMessageStream({
5400
5421
  execute: async ({ writer }) => {
5422
+ const context = this.#config.context;
5423
+ const head = await context.headMessage();
5424
+ if (!head || head.name !== "assistant") {
5425
+ throw new Error(
5426
+ "text2sql.chat: expected head to be an assistant message \u2014 call context.continue() before chat()"
5427
+ );
5428
+ }
5429
+ writer.write({ type: "start", messageId: head.id });
5401
5430
  const progress = (event) => {
5402
5431
  writer.write({
5403
5432
  type: TEXT2SQL_INDEX_PROGRESS_CHUNK,
5404
5433
  data: event
5405
5434
  });
5406
5435
  };
5407
- const context = this.#config.context(
5408
- ...guidelines(),
5409
- ...await this.#index(progress)
5410
- );
5436
+ context.set(...guidelines(), ...await this.#index(progress));
5411
5437
  const chatAgent = agent2({
5412
5438
  name: "text2sql",
5413
5439
  sandbox: this.#config.sandbox,
@@ -5417,7 +5443,7 @@ var Text2Sql = class {
5417
5443
  guardrails: [errorRecoveryGuardrail],
5418
5444
  maxGuardrailRetries: 3
5419
5445
  });
5420
- const chatStream = await chat(chatAgent, messages, {
5446
+ const chatStream = await chat(chatAgent, {
5421
5447
  abortSignal: options?.abortSignal,
5422
5448
  generateTitle: options?.generateTitle,
5423
5449
  transform: this.#config.transform