Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::connection($this->getConnection())->table('sql_agent_messages', function (Blueprint $table) {
$table->dropColumn(['sql', 'results']);
});

Schema::connection($this->getConnection())->table('sql_agent_messages', function (Blueprint $table) {
$table->json('queries')->nullable()->after('content');
});
}

public function down(): void
{
Schema::connection($this->getConnection())->table('sql_agent_messages', function (Blueprint $table) {
$table->dropColumn('queries');
});

Schema::connection($this->getConnection())->table('sql_agent_messages', function (Blueprint $table) {
$table->text('sql')->nullable()->after('content');
$table->json('results')->nullable()->after('sql');
});
}

public function getConnection(): ?string
{
return config('sql-agent.database.storage_connection');
}
};
15 changes: 4 additions & 11 deletions docs/src/content/docs/guides/web-interface.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Web Interface
description: Livewire chat UI, streaming, debug mode, and conversation exports.
description: Livewire chat UI, streaming, debug mode, and result exports.
sidebar:
order: 5
---
Expand Down Expand Up @@ -66,16 +66,9 @@ You may use the Livewire components directly in your own Blade templates:

Displays a searchable list of previous conversations for the current user.

## Exporting Conversations
## Exporting Results

Conversations can be exported as JSON or CSV via dedicated routes:

| Route | Named Route | Description |
|-------|-------------|-------------|
| `GET /sql-agent/export/{conversation}/json` | `sql-agent.export.json` | Download as JSON |
| `GET /sql-agent/export/{conversation}/csv` | `sql-agent.export.csv` | Download as CSV |

These routes share the same middleware as the rest of the UI.
Each result table in the chat interface includes **CSV** and **JSON** export buttons in the header bar. Clicking a button downloads the full result set (all rows, not just the current page) directly from the browser — no server round-trip required.

## Streaming (SSE)

Expand All @@ -86,7 +79,7 @@ The chat interface uses Server-Sent Events for real-time streaming. The streamin
| `conversation` | `{"id": 123}` | Sent first with the conversation ID |
| `thinking` | `{"thinking": "..."}` | LLM reasoning chunks (when thinking mode is enabled) |
| `content` | `{"text": "..."}` | Response text chunks |
| `done` | `{"sql": "...", "hasResults": true, "resultCount": 5}` | Sent when streaming completes |
| `done` | `{"queryCount": 2}` | Sent when streaming completes |
| `error` | `{"message": "..."}` | Sent if an error occurs |

## Debug Mode
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Cache and thought token fields are `null` when the provider does not support the
When using the web interface or the SSE streaming endpoint, usage data is included in the `done` event:

```json
{"event": "done", "data": {"sql": "...", "hasResults": true, "resultCount": 5, "usage": {"prompt_tokens": 1234, "completion_tokens": 567, ...}}}
{"event": "done", "data": {"queryCount": 2, "usage": {"prompt_tokens": 1234, "completion_tokens": 567, ...}}}
```

### Stored Messages
Expand Down
453 changes: 434 additions & 19 deletions resources/views/components/message.blade.php

Large diffs are not rendered by default.

83 changes: 30 additions & 53 deletions resources/views/livewire/chat-component.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,52 +47,6 @@ class="p-2.5 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gra
</svg>
</button>

{{-- Export Menu --}}
@if($conversationId)
<div x-data="{ open: false }" class="relative">
<button
@click="open = !open"
@click.away="open = false"
class="p-2.5 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 transition-colors"
title="Export"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>

<div
x-show="open"
x-cloak
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 mt-2 w-48 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 py-1 z-50"
>
<a
href="{{ route('sql-agent.export.json', $conversationId) }}"
class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Export as JSON
</a>
<a
href="{{ route('sql-agent.export.csv', $conversationId) }}"
class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export as CSV
</a>
</div>
</div>
@endif
</div>
</div>
</header>
Expand Down Expand Up @@ -162,9 +116,9 @@ class="group w-full p-4 text-left bg-white dark:bg-gray-800 hover:bg-gray-50 dar
<x-sql-agent::message
:role="$msg['role']"
:content="$msg['content']"
:sql="$msg['sql'] ?? null"
:results="$msg['results'] ?? null"
:queries="$msg['queries'] ?? null"
:metadata="$msg['metadata'] ?? null"
:message-id="$msg['id']"
/>
@endforeach

Expand Down Expand Up @@ -285,6 +239,7 @@ function chatStream() {
isFinishing: false, // True while waiting for Livewire refresh
streamedContent: '',
pendingUserMessage: '',
pendingMessageId: null,
conversationId: @json($conversationId),
abortController: null,

Expand Down Expand Up @@ -388,6 +343,12 @@ function chatStream() {
}
}
}

// Process any remaining data left in the buffer after stream ends
if (buffer.startsWith('data: ')) {
const data = JSON.parse(buffer.slice(6));
this.handleEvent(data);
}
} catch (error) {
// Check if this was a user-initiated cancellation
if (error.name === 'AbortError') {
Expand Down Expand Up @@ -419,6 +380,17 @@ function chatStream() {
this.isFinishing = false;
this.streamedContent = '';
this.abortController = null;

// Auto-execute queries on the newly rendered message
if (this.pendingMessageId) {
const messageId = this.pendingMessageId;
this.pendingMessageId = null;
this.$nextTick(() => {
window.dispatchEvent(new CustomEvent('auto-execute-queries', {
detail: { messageId: messageId }
}));
});
}
},

cancelStream() {
Expand Down Expand Up @@ -449,11 +421,16 @@ function chatStream() {
// Error event
this.streamedContent = 'Error: ' + data.message;
this.renderContent();
} else if (data.truncated) {
// Done event with truncation — model hit max_tokens
this.streamedContent += '\n\n> **Warning:** The response was cut short because the model reached its token limit. You can increase `SQL_AGENT_LLM_MAX_TOKENS` in your configuration.';
this.renderContent();
this.scrollToBottom();
} else if (data.queryCount !== undefined) {
// Done event
if (data.queryCount > 0 && data.messageId) {
this.pendingMessageId = data.messageId;
}
if (data.truncated) {
this.streamedContent += '\n\n> **Warning:** The response was cut short because the model reached its token limit. You can increase `SQL_AGENT_LLM_MAX_TOKENS` in your configuration.';
this.renderContent();
this.scrollToBottom();
}
}
},

Expand Down
7 changes: 3 additions & 4 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

use Illuminate\Support\Facades\Route;
use Knobik\SqlAgent\Http\Controllers\ExportController;
use Knobik\SqlAgent\Http\Controllers\QueryController;
use Knobik\SqlAgent\Http\Controllers\StreamController;

// Only register routes if UI is enabled
Expand All @@ -21,8 +21,7 @@
// Streaming endpoint for SSE
Route::post('/stream', StreamController::class)->name('stream');

// Export endpoints
Route::get('/export/{conversation}/json', [ExportController::class, 'json'])->name('export.json');
Route::get('/export/{conversation}/csv', [ExportController::class, 'csv'])->name('export.csv');
// On-demand query execution
Route::post('/query/execute', QueryController::class)->name('query.execute');
});
}
10 changes: 10 additions & 0 deletions src/Agent/SqlAgent.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class SqlAgent implements Agent

protected ?array $lastResults = null;

protected array $allQueries = [];

protected array $iterations = [];

protected ?string $currentQuestion = null;
Expand Down Expand Up @@ -185,6 +187,11 @@ public function getLastResults(): ?array
return $this->lastResults;
}

public function getAllQueries(): array
{
return $this->allQueries;
}

public function getIterations(): array
{
return $this->iterations;
Expand Down Expand Up @@ -231,6 +238,7 @@ protected function syncFromRunSqlTool(array $tools): void
if ($tool instanceof RunSqlTool) {
$this->lastSql = $tool->lastSql;
$this->lastResults = $tool->lastResults;
$this->allQueries = $tool->executedQueries;

return;
}
Expand Down Expand Up @@ -319,6 +327,7 @@ protected function reset(): void
{
$this->lastSql = null;
$this->lastResults = null;
$this->allQueries = [];
$this->iterations = [];
$this->currentQuestion = null;
$this->lastPrompt = null;
Expand All @@ -329,6 +338,7 @@ protected function reset(): void
if ($tool instanceof RunSqlTool) {
$tool->lastSql = null;
$tool->lastResults = null;
$tool->executedQueries = [];
}
}
}
Expand Down
50 changes: 0 additions & 50 deletions src/Http/Actions/ExportConversationCsv.php

This file was deleted.

47 changes: 0 additions & 47 deletions src/Http/Actions/ExportConversationJson.php

This file was deleted.

Loading