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 ships with a fixed set of built-in tools (SQL execution, schema introspection, knowledge search, and learning). You can register additional tools so the LLM can call your own logic during the agentic loop.
Custom tools are plain PHP classes that extend Prism\Prism\Tool. They are resolved from the Laravel container, so constructor dependencies are injected automatically.
List your tool class names in the agent.tools array in config/sql-agent.php:
'agent' => [
// ... other options ...
'tools' => [
\App\SqlAgent\CurrentDateTimeTool::class,
\App\SqlAgent\FormatCurrencyTool::class,
],
],
Each class is resolved via app()->make(), so any constructor dependencies are injected by the container. Custom tools appear alongside the built-in tools — the LLM sees all of them and can call any tool on each iteration.
A custom tool must extend Prism\Prism\Tool. Use the fluent API in the constructor to declare the tool's name, description, and parameters. Then pass $this to ->using() and implement the logic in an __invoke method. This is the same pattern the built-in tools use.
<?php
namespace App\SqlAgent;
use Prism\Prism\Tool;
class CurrentDateTimeTool extends Tool
{
public function __construct()
{
$this
->as('current_datetime')
->for('Get the current date and time. Use this when the user asks questions involving relative dates like "today", "this week", or "last month".')
->withStringParameter('timezone', 'IANA timezone (e.g. UTC, America/New_York). Defaults to UTC.', required: false)
->using($this);
}
public function __invoke(?string $timezone = null): string
{
$tz = new \DateTimeZone($timezone ?? config('app.timezone', 'UTC'));
$now = new \DateTimeImmutable('now', $tz);
return json_encode([
'datetime' => $now->format('Y-m-d H:i:s'),
'date' => $now->format('Y-m-d'),
'time' => $now->format('H:i:s'),
'timezone' => $tz->getName(),
'day_of_week' => $now->format('l'),
], JSON_THROW_ON_ERROR);
}
}
The fluent methods available on Prism\Prism\Tool:
| Method | Description |
|---|---|
as(string $name) |
Internal tool name the LLM uses to call it (snake_case recommended). |
for(string $description) |
Description shown to the LLM — explain when to use the tool. |
using(callable $fn) |
The handler to invoke. Pass $this for the __invoke pattern. |
withStringParameter(name, description, required) |
Add a string parameter. |
withNumberParameter(name, description, required) |
Add a numeric parameter. |
withBooleanParameter(name, description, required) |
Add a boolean parameter. |
withEnumParameter(name, description, values, required) |
Add a parameter limited to specific values. |
withArrayParameter(name, description, schema, required) |
Add an array parameter with an item schema. |
withParameter(Schema $schema, required) |
Add a parameter with a custom schema object. |
:::tip
Write the for() description from the LLM's perspective — tell it when and why to use the tool, not just what it does. A good description like "Get the current date and time. Use this when the user asks questions involving relative dates" guides the LLM to call the tool at the right moment.
:::
Because tools are resolved from the container, you can type-hint services in the constructor:
class LookupExchangeRateTool extends Tool
{
public function __construct(private ExchangeRateService $rates)
{
$this
->as('lookup_exchange_rate')
->for('Get the current exchange rate between two currencies')
->withStringParameter('from', 'Source currency code')
->withStringParameter('to', 'Target currency code')
->using($this);
}
public function __invoke(string $from, string $to): string
{
return (string) $this->rates->getRate($from, $to);
}
}
Tool handlers must return a string. The LLM receives this string as the tool result and uses it to formulate its answer. For structured data, return JSON:
public function __invoke(string $code): string
{
$rate = $this->rates->getRate($code);
return json_encode([
'currency' => $code,
'rate' => $rate,
'updated_at' => now()->toIso8601String(),
], JSON_THROW_ON_ERROR);
}
If the tool throws an exception, the agent's error handler captures it and reports the error message back to the LLM, which may retry or adjust its approach.
SqlAgent validates custom tools at boot time:
InvalidArgumentException is thrown with a clear message.Prism\Prism\Tool, an InvalidArgumentException is thrown.These errors surface immediately when the application boots, not at query time, so misconfigurations are caught early.
SqlAgent integrates with Prism Relay to bring tools from MCP (Model Context Protocol) servers into the agentic loop. This lets you connect external tool servers — filesystem access, API wrappers, code interpreters, or any MCP-compatible server — without writing custom PHP tool classes.
Install the Relay package:
composer require prism-php/relay
Then publish and configure your MCP servers in config/relay.php following the Relay documentation.
List the MCP server names (as defined in config/relay.php) in the agent.relay array in config/sql-agent.php:
'agent' => [
// ... other options ...
'tools' => [],
'relay' => [
'weather-server',
'filesystem-server',
],
],
At boot time, SqlAgent calls Relay::tools($server) for each configured server and registers all discovered tools alongside the built-in and custom tools. The LLM sees and can call all of them.
:::tip
Relay tools are dynamically discovered Tool instances — you don't need to create PHP classes for them. Just configure the MCP server in config/relay.php and reference it by name in the relay array.
:::
:::note
If prism-php/relay is not installed, the relay config key is silently ignored. This means you can ship a config that references Relay servers without requiring the package — useful for shared config across environments where only some have Relay installed.
:::
Custom tools and Relay tools can be used together. They all end up in the same tool registry and are passed to the LLM equally:
'agent' => [
'tools' => [
\App\SqlAgent\CurrentDateTimeTool::class,
],
'relay' => [
'weather-server',
],
],
If a Relay tool has the same name as a built-in or custom tool, it will overwrite the previous registration (last write wins).
mode parameter.for() description. Vague descriptions lead to tools being called incorrectly or not at all.__invoke method.How can I help you explore Laravel packages today?