knobik/sql-agent
Self-learning text-to-SQL agent for Laravel that turns natural language into accurate, safe SQL. Uses schema introspection, a curated knowledge base, query patterns, and conversation memory; recovers from errors and saves learnings. Includes built-in chat UI.
SqlAgent uses the database.connections map in config/sql-agent.php to determine which databases the agent can query. Each entry maps a logical name to a Laravel database connection. The agent autonomously decides which database to query for each question and can combine results across databases.
run_sql and introspect_schema tools have a connection parameter.No special "query planner" is needed — the LLM's existing tool-calling loop handles multi-step reasoning naturally.
:::note Cross-database JOINs are not supported. The agent runs separate queries per database and merges results at the response level. This works well for aggregations, comparisons, and lookups across systems. :::
Add entries to the connections map under the database key in config/sql-agent.php:
'database' => [
'storage_connection' => env('SQL_AGENT_STORAGE_CONNECTION', config('database.default')),
'connections' => [
'crm' => [
'connection' => 'mysql_crm',
'label' => 'CRM Database',
'description' => 'Customers, contacts, deals, and activities.',
],
'analytics' => [
'connection' => 'pgsql_analytics',
'label' => 'Analytics Database',
'description' => 'Page views, events, funnels, and attribution data.',
],
],
],
Each connection accepts these options:
| Option | Description | Required |
|---|---|---|
connection |
The Laravel database connection name (from config/database.php) |
Yes |
label |
Human-readable label shown to the LLM and in the UI | No (defaults to the key) |
description |
What data this database holds — helps the LLM choose the right database | No |
allowed_tables |
Whitelist of tables the agent may access on this connection (empty = all) | No |
denied_tables |
Blacklist of tables the agent may never access on this connection | No |
hidden_columns |
Columns to hide per table on this connection | No |
:::tip
Write clear, descriptive description values. The LLM reads these to decide which database to query. "Orders, products, and customers" is much better than "Sales data".
:::
By default, SqlAgent ships with a single default connection that uses your application's default database. If your application only has one database, the default config works out of the box — just update the label and description to match your data:
'connections' => [
'default' => [
'connection' => env('SQL_AGENT_CONNECTION', config('database.default')),
'label' => 'Database',
'description' => 'All application data including users, orders, and products.',
],
],
Each connection can define its own table and column restrictions:
'connections' => [
'hr' => [
'connection' => 'pgsql_hr',
'label' => 'HR Database',
'description' => 'Employees, departments, and leave records.',
'allowed_tables' => ['employees', 'departments', 'leave_requests'],
'denied_tables' => ['salary_details', 'performance_reviews'],
'hidden_columns' => [
'employees' => ['ssn', 'bank_account'],
],
],
],
How it works:
allowed_tables acts as a whitelist. When non-empty, only listed tables are visible to the agent on this connection. Leave empty to allow all tables.denied_tables acts as a blacklist. Listed tables are always denied, even if they appear in allowed_tables. This takes precedence.hidden_columns removes specific columns from schema introspection and semantic model output. The agent will not know these columns exist.Restrictions are enforced at every layer:
The chat header shows a badge indicating the number of connected databases (e.g., "2 databases connected"). The LLM handles connection routing — no user selection is needed.
Tool call indicators in the streaming UI show which database is being queried, for example "Running SQL query on crm" or "Inspecting schema on analytics".
With a CRM and analytics database configured, you might ask:
"Which of our top 10 customers by revenue had the most page views last month?"
The agent will:
Each step is visible in the streaming UI as a separate tool call with its connection label.
Table metadata is scoped per connection using the connection field in each JSON knowledge file. When you run sql-agent:load-knowledge, the loader reads the connection field from each table JSON file and stores it in the database alongside the metadata.
Add a connection field to your table JSON files matching the logical connection name (the key in your connections config, not the Laravel connection name):
{
"connection": "crm",
"table": "customers",
"description": "All registered customers.",
"columns": {
"id": "Primary key",
"name": "Customer full name",
"email": "Contact email address"
}
}
Files without a connection field default to "default" and are included for all connections.
connection value.php artisan sql-agent:load-knowledge to import all files.:::tip
Use the same logical names in your JSON files that you use as keys in the database.connections config. For example, if your config has 'crm' => [...], set "connection": "crm" in the corresponding knowledge files.
:::
allowed_tables to limit what the agent sees.How can I help you explore Laravel packages today?