Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions config/sql-agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,20 @@
'default_limit' => env('SQL_AGENT_DEFAULT_LIMIT', 100),
'chat_history_length' => env('SQL_AGENT_CHAT_HISTORY', 10),

// Custom tool class names resolved from the container, e.g.:
// [App\SqlAgent\MyCustomTool::class]
'tools' => [],
// Tool class names resolved from the container. Includes all built-in tools
// by default. Remove entries to disable specific tools, or add your own.
'tools' => array_merge([
\Knobik\SqlAgent\Tools\RunSqlTool::class,
\Knobik\SqlAgent\Tools\IntrospectSchemaTool::class,
\Knobik\SqlAgent\Tools\SearchKnowledgeTool::class,
\Knobik\SqlAgent\Tools\AskUserTool::class,
], env('SQL_AGENT_LEARNING_ENABLED', true) ? [
\Knobik\SqlAgent\Tools\SaveLearningTool::class,
\Knobik\SqlAgent\Tools\SaveQueryTool::class,
] : []),

// Timeout in seconds for the ask_user tool to wait for a user reply
'ask_user_timeout' => env('SQL_AGENT_ASK_USER_TIMEOUT', 300),

// MCP server names (from config/relay.php) whose tools should be
// available to the agent. Requires prism-php/relay to be installed.
Expand Down
18 changes: 14 additions & 4 deletions docs/src/content/docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Control how the agentic loop operates:
'max_iterations' => env('SQL_AGENT_MAX_ITERATIONS', 10),
'default_limit' => env('SQL_AGENT_DEFAULT_LIMIT', 100),
'chat_history_length' => env('SQL_AGENT_CHAT_HISTORY', 10),
'ask_user_timeout' => env('SQL_AGENT_ASK_USER_TIMEOUT', 300),
],
```

Expand All @@ -228,20 +229,29 @@ Control how the agentic loop operates:
| `max_iterations` | Maximum number of tool-calling rounds before the agent stops | `10` |
| `default_limit` | `LIMIT` applied to queries that don't specify one | `100` |
| `chat_history_length` | Number of previous messages included for conversational context | `10` |
| `ask_user_timeout` | Seconds to wait for a user reply when the `ask_user` tool is invoked | `300` |

### Custom Tools

You can extend the agent with your own tools by listing class names in the `tools` array:
All agent tools — including built-in ones — are registered via the `tools` array. You can add your own tools, remove built-in ones you don't need, or reorder them:

```php
'agent' => [
// ... other options ...
'tools' => [
\App\SqlAgent\CurrentDateTimeTool::class,
],
'tools' => array_merge([
\Knobik\SqlAgent\Tools\RunSqlTool::class,
\Knobik\SqlAgent\Tools\IntrospectSchemaTool::class,
\Knobik\SqlAgent\Tools\SearchKnowledgeTool::class,
\Knobik\SqlAgent\Tools\AskUserTool::class,
], env('SQL_AGENT_LEARNING_ENABLED', true) ? [
\Knobik\SqlAgent\Tools\SaveLearningTool::class,
\Knobik\SqlAgent\Tools\SaveQueryTool::class,
] : []),
],
```

To disable a tool, simply remove its entry from the array. For example, remove `AskUserTool::class` to prevent the LLM from asking clarifying questions. The learning tools (`SaveLearningTool`, `SaveQueryTool`) are automatically included or excluded based on the `SQL_AGENT_LEARNING_ENABLED` environment variable.

Each class must extend `Prism\Prism\Tool` and is resolved from the Laravel container with full dependency injection support. See the [Custom Tools](/sql-agent/guides/custom-tools/) guide for detailed examples and best practices.

### MCP Server Tools (Relay)
Expand Down
137 changes: 135 additions & 2 deletions docs/src/content/docs/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,138 @@ If a query pattern with the same question already exists, the tool returns an er

---

## `ask_user`

Ask the user a clarifying question when their request is ambiguous. The agent pauses and presents a question card in the web UI. The card can include clickable suggestion buttons with optional descriptions, supports multi-select mode, and always includes a free-text input so the user can type a custom answer. Once the user responds, the agent continues with that answer in context.

**Description sent to LLM:**
> Ask the user a clarifying question when their request is ambiguous. Use this when you need more information before proceeding. You may provide suggested options with optional descriptions. Set multiple=true to let the user pick more than one option. The user can always type a custom free-text response instead of picking a suggestion.

:::note
Included in the default `agent.tools` config. Remove `AskUserTool::class` from the array to disable it. In non-streaming mode (`run()`), the tool returns a fallback message instructing the LLM to make its best guess.
:::

### Parameters

```json
{
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The clarifying question to ask the user."
},
"suggestions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "The short label for this suggestion (displayed on the button)."
},
"description": {
"type": "string",
"description": "Optional longer description explaining this option."
}
},
"required": ["label"]
},
"description": "Optional list of suggested answers. Each has a label and optional description."
},
"multiple": {
"type": "boolean",
"description": "Set to true to allow the user to select multiple suggestions. Defaults to false."
}
},
"required": ["question"]
}
```

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `question` | string | Yes | — | The clarifying question to display. |
| `suggestions` | object[] | No | `[]` | Suggested answers, each with a `label` (string, required) and `description` (string, optional). |
| `multiple` | boolean | No | `false` | When `true`, the user can select multiple suggestions before submitting. |

### Return Value

The tool returns a plain string to the LLM:

- `"User answered: <answer>"` — when the user clicks a suggestion, submits a multi-selection, or types a custom answer.
- A fallback message when the user doesn't respond in time or the connection is lost.

For multi-select, the answer is a comma-separated list of selected labels (e.g., `"User answered: Revenue trends, Customer segments"`).

### How It Works

1. The LLM calls `ask_user` with a question, optional suggestions, and an optional `multiple` flag.
2. An `ask_user` SSE event is sent to the frontend with the question, suggestions, multiple flag, and a unique request ID.
3. The frontend renders a question card:
- **Single-select** (default): clicking a suggestion immediately submits it.
- **Multi-select** (`multiple: true`): suggestions have checkboxes that toggle on/off. A "Submit selection" button sends all selected labels.
- Suggestions with a `description` show the description text below the label.
- A free-text input is always available at the bottom.
4. The user's answer POSTs to `/ask-user-reply` and writes to the cache.
5. The tool polls the cache, picks up the answer, and returns it to the LLM.
6. The LLM continues with the user's answer in context.

### Configuration

The timeout for waiting on a user reply is configurable:

```php
'agent' => [
'ask_user_timeout' => env('SQL_AGENT_ASK_USER_TIMEOUT', 300), // seconds
],
```

### Example Tool Calls

**Simple single-select:**

```json
{
"question": "Which time period are you interested in?",
"suggestions": [
{ "label": "Last 7 days" },
{ "label": "Last 30 days" },
{ "label": "Last quarter" },
{ "label": "All time" }
]
}
```

**With descriptions:**

```json
{
"question": "What kind of analysis would you like?",
"suggestions": [
{ "label": "Revenue trends", "description": "Monthly revenue breakdown with growth rates" },
{ "label": "Customer segments", "description": "RFM analysis grouping customers by behavior" },
{ "label": "Product performance", "description": "Sales volume and margin by product category" }
]
}
```

**Multi-select:**

```json
{
"question": "Which metrics should I include in the report?",
"suggestions": [
{ "label": "Total revenue", "description": "Sum of all completed orders" },
{ "label": "Order count", "description": "Number of orders placed" },
{ "label": "Average order value" },
{ "label": "Customer count" }
],
"multiple": true
}
```

---

## Tool Availability

Not all tools are available in every configuration:
Expand All @@ -481,7 +613,8 @@ Not all tools are available in every configuration:
| `search_knowledge` | Yes | — |
| `save_learning` | No | Requires `sql-agent.learning.enabled = true` |
| `save_validated_query` | No | Requires `sql-agent.learning.enabled = true` |
| `ask_user` | Yes | Remove from `agent.tools` to disable |

When learning is disabled (`SQL_AGENT_LEARNING_ENABLED=false`), the `save_learning` and `save_validated_query` tools are not registered with the LLM, and the related instructions are removed from the system prompt.
All tools are registered via the `agent.tools` config array. Remove any entry to disable that tool. When learning is disabled (`SQL_AGENT_LEARNING_ENABLED=false`), the `save_learning` and `save_validated_query` tools are automatically skipped even if present in the array.

In addition to the built-in tools above, you can register your own tools via the `agent.tools` config option. See the [Custom Tools](/sql-agent/guides/custom-tools/) guide for details.
You can also register your own tools by adding class names to the `agent.tools` array. See the [Custom Tools](/sql-agent/guides/custom-tools/) guide for details.
22 changes: 1 addition & 21 deletions resources/prompts/system.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,11 @@
You cannot JOIN across databases. Run separate queries and combine results in your response.

## Available Tools

### run_sql
Execute a SQL query. Specify the `connection` parameter to choose which database to query. Only {{ implode(' and ', config('sql-agent.sql.allowed_statements', ['SELECT', 'WITH'])) }} statements allowed.

### introspect_schema
Get detailed schema information about tables, columns, relationships, and data types. Specify the `connection` parameter to choose which database to inspect.

### search_knowledge
Search for relevant query patterns, learnings, and past discoveries about the database.

@if(config('sql-agent.learning.enabled', true))
### save_learning
Save a discovery to the knowledge base (type errors, date formats, column quirks, business logic).

### save_validated_query
Save a successful query pattern for reuse. Use when a query correctly answers a common question.
@endif
@if(!empty($customTools))
{{-- Custom tools registered via config('sql-agent.agent.tools') --}}
@foreach($customTools as $tool)
@foreach($tools as $tool)

### {{ $tool->name() }}
{{ $tool->description() }}
@endforeach
@endif

## Workflow

Expand Down
16 changes: 16 additions & 0 deletions resources/views/components/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@
.markdown-content tool[data-type="default"]::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z'/%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 12a3 3 0 11-6 0 3 3 0 016 0z'/%3E%3C/svg%3E");
}
.markdown-content tool[data-type="ask"] {
background: #fffbeb;
color: #b45309;
border-color: #fde68a;
}
.markdown-content tool[data-type="ask"]::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23b45309'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'/%3E%3C/svg%3E");
}
.dark .markdown-content tool[data-type="ask"] {
background: rgba(120, 53, 15, 0.3);
color: #fbbf24;
border-color: rgba(251, 191, 36, 0.3);
}
.dark .markdown-content tool[data-type="ask"]::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23fbbf24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'/%3E%3C/svg%3E");
}
.dark .markdown-content tool[data-type="sql"]::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23fca5a5'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E");
}
Expand Down
Loading