Skip to content

Commit 240637c

Browse files
committed
feat: enhance error handling and add conversation tables migration
1 parent 6a03740 commit 240637c

7 files changed

Lines changed: 96 additions & 14 deletions

File tree

app/Http/Controllers/QueryController.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
use App\Models\DatabaseConnection;
1010
use App\Models\QueryRun;
1111
use App\Models\SavedQuery;
12-
use App\Services\AiSqlAssistantService;
1312
use App\Services\AiMemoryProfileService;
13+
use App\Services\AiSqlAssistantService;
1414
use App\Services\AuditService;
1515
use App\Services\QueryValidationService;
1616
use App\Services\ReadOnlyQueryExecutor;
@@ -111,18 +111,22 @@ public function execute(ExecuteQueryRequest $request): JsonResponse
111111
} catch (Throwable $throwable) {
112112
report($throwable);
113113

114+
$dbMessage = $throwable->getPrevious()?->getMessage()
115+
?? $throwable->getMessage();
116+
114117
$this->auditService->record(
115118
action: 'query.failed',
116119
user: $request->user(),
117120
connection: $connection,
118121
sql: $validation['sql_with_limit'],
119122
status: 'failed',
120123
request: $request,
121-
metadata: ['reason' => 'sanitized_sql_error'],
124+
metadata: ['reason' => $dbMessage],
122125
);
123126

124127
return response()->json([
125-
'message' => 'The SQL engine returned a sanitized error response.',
128+
'message' => 'Error de base de datos: '.$dbMessage,
129+
'sql' => $validation['sql_with_limit'],
126130
], 422);
127131
}
128132

@@ -143,12 +147,16 @@ public function execute(ExecuteQueryRequest $request): JsonResponse
143147
]);
144148

145149
if ($request->user() !== null) {
146-
$this->aiMemoryProfileService->recordSuccessfulExecution(
147-
user: $request->user(),
148-
connectionId: $connection->id,
149-
sql: $validation['sql_with_limit'],
150-
tablesUsed: $validation['tables'],
151-
);
150+
try {
151+
$this->aiMemoryProfileService->recordSuccessfulExecution(
152+
user: $request->user(),
153+
connectionId: $connection->id,
154+
sql: $validation['sql_with_limit'],
155+
tablesUsed: $validation['tables'],
156+
);
157+
} catch (Throwable) {
158+
// memory recording is non-critical
159+
}
152160
}
153161

154162
$this->auditService->record(

app/Services/AiSqlAssistantService.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,18 @@ public function generateStructuredQuery(
4343
): array {
4444
$candidateTables = $selectedTables !== [] ? $selectedTables : $allowedTables;
4545
$dialect = $this->sqlDialectStrategy->resolveForDriver($connection->driver);
46-
$memoryContext = $this->aiMemoryProfileService->promptContext($user, $connection->id);
47-
$schemaContext = $this->schemaContextBuilder->build($connection, $allowedTables, $selectedTables);
46+
47+
try {
48+
$memoryContext = $this->aiMemoryProfileService->promptContext($user, $connection->id);
49+
} catch (Throwable) {
50+
$memoryContext = ['applied' => false, 'context' => ''];
51+
}
52+
53+
try {
54+
$schemaContext = $this->schemaContextBuilder->build($connection, $allowedTables, $selectedTables);
55+
} catch (Throwable) {
56+
$schemaContext = ['context' => '(schema introspection failed)', 'tables_included' => []];
57+
}
4858

4959
if ($candidateTables === []) {
5060
return $this->withMetadata(

app/Services/SchemaContextBuilder.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ public function build(DatabaseConnection $connection, array $allowedTables, arra
2222
$maxChars = max(2000, $maxSchemaTokens * 4);
2323
$dialect = $this->sqlDialectStrategy->resolveForDriver($connection->driver);
2424

25-
$tableList = $this->schemaIntrospectionService->listTables($connection);
25+
try {
26+
$tableList = $this->schemaIntrospectionService->listTables($connection);
27+
} catch (\Throwable) {
28+
$tableList = [];
29+
}
2630

2731
$tableByName = collect($tableList)
2832
->filter(fn (array $table): bool => ! empty($table['name']))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
$conversationsTable = (string) config('ai.conversations.tables.conversations', 'agent_conversations');
12+
$messagesTable = (string) config('ai.conversations.tables.messages', 'agent_conversation_messages');
13+
14+
if (! Schema::hasTable($conversationsTable)) {
15+
Schema::create($conversationsTable, function (Blueprint $table): void {
16+
$table->string('id', 36)->primary();
17+
$table->foreignId('user_id')->nullable();
18+
$table->string('title');
19+
$table->timestamps();
20+
21+
$table->index(['user_id', 'updated_at']);
22+
});
23+
}
24+
25+
if (! Schema::hasTable($messagesTable)) {
26+
Schema::create($messagesTable, function (Blueprint $table): void {
27+
$table->string('id', 36)->primary();
28+
$table->string('conversation_id', 36)->index();
29+
$table->foreignId('user_id')->nullable();
30+
$table->string('agent');
31+
$table->string('role', 25);
32+
$table->text('content');
33+
$table->text('attachments');
34+
$table->text('tool_calls');
35+
$table->text('tool_results');
36+
$table->text('usage');
37+
$table->text('meta');
38+
$table->timestamps();
39+
40+
$table->index(['conversation_id', 'user_id', 'updated_at'], 'conversation_index');
41+
$table->index(['user_id']);
42+
});
43+
}
44+
}
45+
46+
public function down(): void
47+
{
48+
// Intentionally keep vendor-managed conversation tables.
49+
}
50+
};

database/seeders/DatabaseSeeder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,7 @@ public function run(): void
138138
'description' => 'SQL query assistant base system prompt.',
139139
],
140140
);
141+
142+
$this->call(SuperAdminDemoSeeder::class);
141143
}
142144
}

resources/js/pages/Chat.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ async function sendPrompt(promptText: string): Promise<AiGeneratedSql | null> {
232232
}>('/queries/ai-generate', {
233233
connection_id: context.selectedConnectionId.value,
234234
question: promptText,
235-
conversation_id: chat.conversationId.value,
235+
...(chat.conversationId.value ? { conversation_id: chat.conversationId.value } : {}),
236236
selected_tables: context.selectedTables.value,
237237
});
238238

tests/Feature/MonitorSqlQueryWorkflowTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ class MonitorSqlQueryWorkflowTest extends TestCase
2121
{
2222
use RefreshDatabase;
2323

24+
protected function setUp(): void
25+
{
26+
parent::setUp();
27+
28+
config()->set('ai.conversations.generate_title', false);
29+
}
30+
2431
public function test_query_validation_blocks_write_statements()
2532
{
2633
$user = $this->createUserWithPermission('queries.execute');
@@ -323,7 +330,8 @@ public function test_query_execute_returns_sanitized_message_on_engine_error()
323330
]);
324331

325332
$response->assertStatus(422);
326-
$response->assertJsonPath('message', 'The SQL engine returned a sanitized error response.');
333+
$response->assertJsonPath('message', 'Error de base de datos: driver error');
334+
$response->assertJsonPath('sql', 'SELECT * FROM customers LIMIT 100');
327335
}
328336

329337
public function test_query_execute_blocks_dialect_mismatch_for_mysql_connection()

0 commit comments

Comments
 (0)