@deepagents/text2sql

中文文档
7.5.2发布于 2026/11/08MITTypeScriptES Module构建来源已验证近 30 天下载 2041

AI-powered natural language to SQL. Ask questions in plain English, get executable queries.

原始使用文档
文档目录16 节
  1. Features
  2. Installation
  3. Quick start
  4. AI model providers
  5. Building a conversational agent
  6. Advanced: SQL CLI in sandboxes
  7. Fragments
  8. Grounding
  9. Conversations
  10. Checkpoint files
  11. Schema index caching
  12. Introspect once for concurrent callers
  13. Streaming index progress
  14. Direct SQL generation with extra fragments
  15. Documentation
  16. Repository

@deepagents/text2sql

AI-powered natural language to SQL. Ask questions in plain English, get executable queries.

Features

  • Natural language to SQL - Convert questions to validated, executable queries
  • Database adapters - PostgreSQL, SQLite, DuckDB, SQL Server, MySQL/MariaDB, BigQuery, ClickHouse, and PostHog HogQL adapters
  • Schema introspection - Automatic introspection of tables, relationships, indexes, and constraints
  • Domain knowledge - Inject business terms, guardrails, and query patterns via fragments
  • Conversational - Multi-turn conversations with context persistence
  • Safe by default - Read-only queries, validation, and configurable guardrails

Installation

npm install @deepagents/text2sql

Install the database driver or client library that matches your adapter:

npm install pg                       # PostgreSQL
npm install mssql                    # SQL Server
npm install mysql2                   # MySQL / MariaDB
npm install @duckdb/node-api         # DuckDB
npm install @google-cloud/bigquery   # BigQuery
npm install @clickhouse/client       # ClickHouse, or use your preferred client
# PostHog uses native fetch and needs no additional client package

The spreadsheet adapter requires xlsx for Excel, CSV, and TSV files. It is an optional peer dependency; install it only if you use @deepagents/text2sql/spreadsheet. With npm 12, add this to your project's .npmrc to allow directly declared URL dependencies:

allow-remote=root

Then install SheetJS xlsx 0.20.3 from its CDN:

npm install https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz

Requires Node.js LTS.

Quick start

import { groq } from '@ai-sdk/groq';
import pg from 'pg';

import { Text2Sql, toSql } from '@deepagents/text2sql';
import {
  Postgres,
  columnValues,
  constraints,
  indexes,
  info,
  rowCount,
  tables,
  views,
} from '@deepagents/text2sql/postgres';

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

const adapter = new Postgres({
  execute: async (sql) => {
    const result = await pool.query(sql);
    return result.rows;
  },
  grounding: [
    tables(),
    views(),
    info(),
    indexes(),
    constraints(),
    rowCount(),
    columnValues(),
  ],
});

const text2sql = new Text2Sql({ adapters: { main: adapter } });

const fragments = await text2sql.index({ names: ['main'] });
const { sql } = await toSql({
  input: 'Show me the top 10 customers by revenue',
  adapter,
  fragments,
  model: groq('openai/gpt-oss-20b'),
});
console.log(sql);

index() introspects the database schema and returns it as context fragments. toSql() gives these fragments to the model and validates the SQL that the model writes.

The adapter-map key (main here) is the adapter name. Reuse that same key in text2sql.index({ names: ['main'] }) and in any sql validate <db> "..." / sql run <db> "..." calls from a sandbox where the package CLI is installed. This is the configured connection name, not the SQL-level database or schema name (for example, SQLite's default main schema).

Adapter names must match /^[A-Za-z_][A-Za-z0-9_]*$/. If you build adapter maps dynamically, use isValidAdapterName(name) to check one key or validateAdapterNames(names) to fail fast before constructing Text2Sql or the sandbox-side adapter module used by the sql CLI.

AI model providers

Text2SQL works with any model provider supported by the Vercel AI SDK, including OpenAI, Anthropic, Google, Groq, and more.

Building a conversational agent

Text2Sql owns SQL generation and schema indexing, but you own the chat loop. Build the agent by composing a ContextEngine, a sandbox, and agent + chat from @deepagents/context:

import {
  ContextEngine,
  type ContextFragment,
  InMemoryContextStore,
  agent,
  chat,
  createBashTool,
  createDockerSandbox,
  errorRecoveryGuardrail,
  npm,
  user,
} from '@deepagents/context';
import {
  createSqlCommandHooks,
  instructions,
  sqlValidateReminder,
} from '@deepagents/text2sql';

const store = new InMemoryContextStore();
const context = new ContextEngine({
  store,
  chatId: 'chat-123',
  userId: 'user-456',
});

const backend = await createDockerSandbox({
  installers: [npm('@deepagents/text2sql', { ensureRuntime: true })],
  volumes: [
    {
      type: 'bind',
      hostPath: process.cwd(),
      containerPath: '/workspace',
      readOnly: true,
    },
  ],
  env: {
    TEXT2SQL_ADAPTERS: '/workspace/text2sql-adapters.ts',
    TEXT2SQL_OUT_DIR: '/tmp/text2sql',
  },
});
const sandbox = await createBashTool({
  sandbox: backend,
  ...createSqlCommandHooks({ adapters: { main: adapter } }),
});

const indexResult = await sandbox.sandbox.executeCommand('sql index');
if (indexResult.exitCode !== 0) throw new Error(indexResult.stderr);
const manifest = JSON.parse(indexResult.stdout) as { fragmentsPath: string };
const fragments = JSON.parse(
  await sandbox.sandbox.readFile(manifest.fragmentsPath),
) as ContextFragment[];
context.set(...instructions(), ...fragments);
context.set(sqlValidateReminder());

const ai = agent({
  name: 'sql-assistant',
  sandbox,
  model: groq('openai/gpt-oss-20b'),
  context,
  guardrails: [errorRecoveryGuardrail],
  maxGuardrailRetries: 3,
});

await context.continue(user('Show me the top 10 customers by revenue'));
const stream = await chat(ai);

for await (const chunk of stream) {
  // handle streaming response
}

The /workspace/text2sql-adapters.ts module must exist in the sandbox and default-export your adapter map. Mount your project into /workspace (as above) or upload or write that module before calling sql index. With a remote Docker daemon, the bind mount's hostPath must exist on the daemon's host.

TEXT2SQL_OUT_DIR keeps schema and query artifacts in the sandbox's writable /tmp/text2sql directory. The default ./sql would be inside the read-only /workspace mount.

instructions() returns the SQL-flavored system fragments (policies, workflows, clarifications, style guides) — spread them into context.set() alongside the schema fragments returned by sql index. The index output starts with an available_databases fragment that lists the exact configured adapter names the model must pass to sql validate <db> and sql run <db>. Add or replace the instructions with your own domain fragments as needed.

sqlValidateReminder() returns a context reminder fragment backed by SQL_VALIDATE_REMINDER. After a successful sql run <db> "...", it adds a model-only tool-output reminder before the next generation when no prior sql validate <db> "..." used the same configured adapter name and exact SQL text. The raw SQL command result remains unchanged. During streamed chat() turns, the reminder is persisted as a synthetic user message for replay and prompt caching; bare createPrepareStep() integrations and agent.generate() own persistence of their generated assistant history.

Advanced: SQL CLI in sandboxes

sql validate <db> "..." and sql run <db> "..." are real commands from the @deepagents/text2sql package. <db> is the configured adapter name. With a single configured adapter, a mistaken database selector is routed to that sole adapter and the command prints a note; with multiple adapters, unknown names fail and print the available list. sql index writes an available_databases fragment, schema fragments, and progress events for chat setup, indexing all configured adapters by default (same as --all) unless adapter names are provided.

Install the package inside the sandbox and set TEXT2SQL_ADAPTERS to a module whose default export is Record<string, Adapter>. Missing sql means the sandbox was not prepared correctly.

sql index output details:

  • stdout: JSON manifest with fragmentsPath, eventsPath, adapters, and fragment count.
  • --verbose pretty or --verbose json: mirrors progress events to stderr while keeping stdout as the manifest.
  • --out-dir <path>: writes artifacts under that path (default: $TEXT2SQL_OUT_DIR or ./sql).

The sql CLI caches introspected schema only when you opt in via env: set TEXT2SQL_INDEX_CACHE_DIR (where cache files live) and/or TEXT2SQL_INDEX_VERSION (an invalidation token — bump it when the schema changes). With neither set, every sql index introspects fresh. See Schema index caching for the underlying injectable primitives.

For in-process or virtual-sandbox usage, install just-bash and wrap an existing Text2Sql instance as a custom command:

import { InMemoryFs } from 'just-bash';

import { createVirtualSandbox } from '@deepagents/context';
import {
  type CreateSqlCommandOptions,
  type CreateSqlCommandResult,
  Text2Sql,
  createSqlCommand,
} from '@deepagents/text2sql';

const text2sql = new Text2Sql({ adapters: { main: adapter } });

const commandOptions: CreateSqlCommandOptions = {
  outputDir: '/sql-artifacts',
};
const sqlCommand: CreateSqlCommandResult = createSqlCommand(
  text2sql,
  commandOptions,
);

const sandbox = await createVirtualSandbox({
  fs: new InMemoryFs(),
  customCommands: [sqlCommand.command],
});

await sandbox.executeCommand('sql validate main "SELECT 1"');

CreateSqlCommandOptions configures command defaults (currently outputDir). CreateSqlCommandResult returns the command plus a repair(raw) helper for normalizing model-generated argv before execution.

Spread createSqlCommandHooks({ adapters }) into createBashTool() for model-driven bash calls. The before hook preserves the old virtual-command tolerance for common LLM quote mistakes, rewrites SQL identifier backticks so bash does not run them as command substitutions, and blocks raw database access so read-only and scope checks stay behind sql validate / sql run. The after hook restores hidden formattedSql metadata from the host adapter map without putting that concern into the real CLI.

Read-only enforcement parses a single SELECT/WITH statement even when it starts with whitespace or SQL comments (-- ..., /* ... */). Before any adapter validator or executor runs, the dialect policy rejects write statements, multi-statement batches, parser failures, SELECT INTO, locking reads/hints, assignments, known state/file/extension/remote-access functions, BigQuery external queries, qualified BigQuery persistent routines, and ClickHouse table functions or unsafe UDF origins.

Scope checks compare referenced base tables and views against the entities produced by tables() and views() groundings. Entity-free queries such as SELECT 1 do not run grounding just to resolve an empty allowlist, and metadata groundings such as indexes(), constraints(), rowCount(), columnStats(), and columnValues() annotate schema context without expanding the validation allowlist.

This parser policy is one safety layer, not a database permission boundary. Static analysis cannot prove arbitrary unqualified user-defined functions are side-effect free, and an otherwise valid SELECT can still consume excessive CPU, memory, bytes, locks, or time. Production connections must use a dedicated least-privilege identity with SELECT only on intended relations; disable or withhold extension, file, remote-query, and routine execution capabilities; and configure dialect-appropriate statement timeouts, byte/result limits, and compute/memory limits.

The DuckDB adapter uses DuckDB's native JSON AST instead of the shared SQL parser, so DuckDB-specific syntax is analyzed in its real grammar. It resolves every referenced relation to a quoted catalog.schema.table, rejects writes, multi-statement batches, side-effecting or user-defined functions and macros, and arbitrary table functions, then compares those relations with the tables and views produced by grounding. Production DuckDB connections must still be read-only, disable external access and automatic/community extension loading, set resource limits, and lock configuration after setup.

The PostHog adapter does not pass HogQL through the local SQL parser. It asks PostHog to parse each query with HogQLMetadata, checks the returned base-table names against grounded schema, and only then sends a HogQLQuery for execution. Direct adapter.execute(sql, values) calls forward named HogQL values unchanged after validation, which is useful for application-owned filters added outside the model loop. Shell-escaped dollar-prefixed PostHog properties such as properties['\$device_type'] are decoded before validation and execution.

Fragments

Inject domain knowledge by setting fragments on the ContextEngine you build for the agent. Those fragments affect every chat() turn. For direct SQL generation, give toSql({ fragments }) the fragments from index() and your own fragments.

import {
  ContextEngine,
  InMemoryContextStore,
  example,
  guardrail,
  hint,
  term,
} from '@deepagents/context';

const store = new InMemoryContextStore();
const context = new ContextEngine({
  store,
  chatId: 'chat-123',
  userId: 'user-456',
});

context.set(
  term('MRR', 'monthly recurring revenue'),
  hint('Always exclude test accounts with email ending in @test.com'),
  guardrail({
    rule: 'Never expose individual salaries',
    reason: 'Confidential HR data',
    action: 'Aggregate by department instead',
  }),
  example({
    question: 'show me churned customers',
    answer: `SELECT * FROM customers WHERE status = 'churned' ORDER BY churned_at DESC`,
  }),
);

Domain fragments (11 types): term, hint, guardrail, example, explain, clarification, workflow, quirk, styleGuide, analogy, glossary.

User fragments (5 types): identity, persona, alias, preference, correction.

See @deepagents/context for full fragment documentation.

Grounding

Control what schema metadata the AI receives:

FunctionDescription
tables()Tables, columns, and primary keys
views()Database views
info()Database version and info
indexes()Index information for performance hints
constraints()Foreign keys and other constraints
rowCount()Table sizes (tiny, small, medium, large, huge)
columnStats()Min/max/null distribution for columns
columnValues()Enum-like and low-cardinality columns with sampled values

Conversations

chat() (from @deepagents/context) persists history through the ContextEngine you build for the agent. Reuse the same store, chatId, and userId to continue the same thread. Before each turn, append only the new incoming message to the engine; do not replay earlier turns that are already stored in the context store:

await context.continue(user('Show me orders from last month'));
const stream = await chat(ai);

for await (const chunk of stream) {
  // handle streaming response
}

// Continue the same conversation
await context.continue(user('Now filter to only completed ones'));
const followUp = await chat(ai);
for await (const chunk of followUp) {
  // handle streaming response
}

Checkpoint files

Checkpoint from @deepagents/text2sql saves completed computations so a pipeline can resume without repeating them. Load a file with Checkpoint.load({ path, configHash, clock: globalThis }), then use run() for one computation or each() for a sequence of inputs. Both require a codec to encode outputs and validate them on resume; jsonCodec(schema) handles plain JSON values. The optional configHash invalidates saved results when both the file and the caller supply nonempty hashes that differ.

clock is required and controls the delay between file replacement retries when a file is busy. Pass globalThis to use Node.js timers.

Writes from one Checkpoint instance run in order. Each write captures the latest state when it starts, so an earlier snapshot cannot overwrite a later one. The checkpoint uses durableWrite to flush the file before replacing it and sync the parent directory where supported. Await run(), each(), or point().through() before exiting. Separate instances or processes do not coordinate writes to the same file.

Schema index caching

Schema introspection is the expensive part of indexing. Text2Sql and AdapterIndexer own no storage. You can give them a cache. The cache key is the adapter name:

export interface IndexCache {
  read(key: string): Promise<ContextFragment[] | null>;
  write(key: string, fragments: ContextFragment[]): Promise<void>;
}
  • No cache: each index() call introspects the database.
  • A cache: index() reads the cache first. On a miss, it introspects the database and writes the result to the cache.

The package ships a file-backed cache:

import { FileIndexCache, Text2Sql } from '@deepagents/text2sql';

const text2sql = new Text2Sql({
  adapters: { main: adapter },
  cache: new FileIndexCache({
    clock: globalThis,
    dir: '/var/cache/text2sql',
    namespace: 'v1',
  }),
});

FileIndexCache writes a temporary file and renames it over the cache file. A missing file, invalid JSON, or data that fails context-fragment validation is a cache miss, so the next index() call introspects again. clock is required and controls file replacement retry delays; pass globalThis to use Node.js timers. dir defaults to the OS temp directory. The optional namespace separates cache generations; change it when the schema changes.

Introspect once for concurrent callers

index() does not make callers wait for each other. When two callers miss the cache at the same time, both introspect the database. The result stays correct, because each write replaces the whole cache file.

To let only one caller introspect, acquire a key around index(). This example uses @zukhruf/mutex:

import { Mutex } from '@zukhruf/mutex';
import { SqliteStore } from '@zukhruf/mutex/sqlite';

const mutex = new Mutex(
  new SqliteStore('/var/lib/my-app/locks', { clock: globalThis }),
);

const fragments = await mutex.acquire('text2sql:main', () =>
  text2sql.index({ names: ['main'] }),
);

Callers must share the cache directory, namespace, and adapter names. The first caller introspects the database and writes the cache. When each waiting caller acquires the same lock key, its index() call reads that cached schema.

SqliteStore shares keys between the processes on one host. For a fleet of hosts, acquire the key from a service that all hosts share, for example a Postgres advisory lock, and give all hosts the same cache.

On object-storage-backed volumes (GCS/S3 FUSE) rename is not atomic. The FileIndexCache parse-as-miss behavior tolerates a torn write: the next index() call introspects again.

Streaming index progress

AdapterIndexer#index({ onProgress }) emits progress events while it warms or reads the schema cache for host-side indexing. The sandbox CLI writes the same event shape to the eventsPath file returned by sql index:

import { createUIMessageStream } from 'ai';

import {
  AdapterIndexer,
  TEXT2SQL_INDEX_PROGRESS_CHUNK,
  type Text2SqlIndexProgressEvent,
} from '@deepagents/text2sql';

const indexer = new AdapterIndexer({ adapters });

await context.continue(user('Show me top 10 customers'));

const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    const head = await context.headMessage();
    if (!head || head.name !== 'assistant') {
      throw new Error(
        'expected head to be an assistant message — call context.continue() before chat()',
      );
    }
    writer.write({ type: 'start', messageId: head.id });

    const fragments = await indexer.index({
      onProgress: (event) =>
        writer.write({
          type: TEXT2SQL_INDEX_PROGRESS_CHUNK,
          data: event,
        }),
    });

    context.set(...instructions(), ...fragments);
    writer.merge(await chat(ai));
  },
});

Each progress event includes a generic Unix epoch timestampMs so clients can calculate per-index, per-adapter, or per-phase durations without Text2SQL imposing one duration policy.

toSql() is stateless. It reads the current schema fragments, but it does not write messages, titles, or usage into your context store.

Direct SQL generation with extra fragments

This lower-level helper still takes a single adapter because it bypasses the multi-adapter Text2Sql wrapper entirely.

import { term } from '@deepagents/context';
import { toSql } from '@deepagents/text2sql';

const result = await toSql({
  input: 'Show ARR by plan',
  adapter,
  model: groq('openai/gpt-oss-20b'),
  fragments: [
    ...(await adapter.introspect()),
    term('ARR', 'annual recurring revenue'),
  ],
});

console.log(result.sql);

Documentation

Full documentation available at januarylabs.github.io/deepagents:

Repository

github.com/JanuaryLabs/deepagents

版本与发布记录

@deepagents/text2sql 共发布 79 个版本,首次发布于 2025/11/26;过去 12 个月发布了 79 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
7.5.2latest2026/11/086 天前
7.5.12026/11/086 天前
7.5.02026/11/077 天前
7.4.02026/11/077 天前
7.3.02026/10/3114 天前
7.2.02026/10/2718 天前

关于 @deepagents/text2sql 的常见问题

@deepagents/text2sql 是什么?

AI-powered natural language to SQL. Ask questions in plain English, get executable queries.

如何安装 @deepagents/text2sql?

在项目目录运行 npm install @deepagents/text2sql,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 sql,可以用 npx sql 直接运行。

@deepagents/text2sql 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@deepagents/text2sql 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@deepagents/text2sql 可以商用吗?

@deepagents/text2sql 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@deepagents/text2sql 的下载量有多少?

根据 npm 官方下载统计,@deepagents/text2sql 在 2026/10/12 – 2026/11/10 这 30 天内被下载 2,041 次,日均约 68 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@deepagents/text2sql 还在维护吗?

最新版本 7.5.2 发布于 2026/11/08(6 天前);至今共发布 79 个版本,其中过去 12 个月发布了 79 个;首次发布于 2025/11/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。