Skip to content
Draft
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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PromptSQL

PromptSQL is a secure PHP 8.2+ library that converts natural language questions into read-only SQL queries using OpenAI, and executes them safely against MySQL or PostgreSQL databases via PDO.
PromptSQL is a secure PHP 8.2+ library that converts natural language questions into read-only SQL queries using Google Gemini API, and executes them safely against MySQL or PostgreSQL databases via PDO.

- Only SELECT queries allowed
- Prepared statements with named parameters
Expand All @@ -21,7 +21,7 @@ composer require jona-odoh/promptsql
- PHP 8.2+
- ext-pdo, ext-json
- MySQL or PostgreSQL
- OpenAI API key
- Google Gemini API key

## Quick Start

Expand All @@ -31,7 +31,7 @@ composer require jona-odoh/promptsql
use PromptSQL\Config\PromptSQLConfig;
use PromptSQL\Database\PdoDatabaseAdapter;
use PromptSQL\Enums\DatabaseDriver;
use PromptSQL\OpenAI\GuzzleOpenAIClient;
use PromptSQL\OpenAI\GuzzleGeminiClient;
use PromptSQL\OpenAI\OpenAIBasedSqlGenerator;
use PromptSQL\PromptSQL;
use PromptSQL\Validation\SqlValidator;
Expand All @@ -44,8 +44,8 @@ $config = new PromptSQLConfig(
dsn: 'pgsql:host=127.0.0.1;port=5432;dbname=app_db',
username: 'readonly_user',
password: 'readonly_password',
openAiApiKey: getenv('OPENAI_API_KEY') ?: '',
openAiModel: 'gpt-4o-mini',
geminiApiKey: getenv('GEMINI_API_KEY') ?: '',
geminiModel: 'gemini-1.5-flash',
pdoOptions: [],
allowList: [
// Table => allowed columns (empty array means all columns allowed)
Expand All @@ -69,8 +69,8 @@ $config = new PromptSQLConfig(
);

// 2) Wire dependencies (constructor DI)
$openAI = new GuzzleOpenAIClient($config->openAiApiKey);
$generator = new OpenAIBasedSqlGenerator($openAI, $config->openAiModel);
$gemini = new GuzzleGeminiClient($config->geminiApiKey);
$generator = new OpenAIBasedSqlGenerator($gemini, $config->geminiModel);
$validator = new SqlValidator();
$db = new PdoDatabaseAdapter(
driver: $config->driver,
Expand Down
8 changes: 4 additions & 4 deletions src/Config/PromptSQLConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ final class PromptSQLConfig
{
/**
* @param DatabaseDriver $driver
* @param string $openAiApiKey
* @param string $openAiModel
* @param string $geminiApiKey
* @param string $geminiModel
* @param array<string, mixed> $pdoOptions
* @param array<string, array<int, string>> $allowList table => columns (empty list means all columns allowed)
* @param bool $allowSelectStar Whether SELECT * is allowed
Expand All @@ -23,8 +23,8 @@ public function __construct(
public readonly string $dsn,
public readonly string $username,
public readonly string $password,
public readonly string $openAiApiKey,
public readonly string $openAiModel = 'gpt-4o-mini',
public readonly string $geminiApiKey,
public readonly string $geminiModel = 'gemini-1.5-flash',
public readonly array $pdoOptions = [],
public readonly array $allowList = [],
public readonly bool $allowSelectStar = false,
Expand Down
85 changes: 85 additions & 0 deletions src/OpenAI/GuzzleGeminiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace PromptSQL\OpenAI;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use PromptSQL\Contracts\OpenAIClientInterface;
use PromptSQL\Exceptions\GenerationException;
use Psr\Log\LoggerInterface;

final class GuzzleGeminiClient implements OpenAIClientInterface
{
private readonly Client $http;

public function __construct(
private readonly string $apiKey,
private readonly ?LoggerInterface $logger = null,
?Client $httpClient = null
) {
$this->http = $httpClient ?? new Client([
'base_uri' => 'https://generativelanguage.googleapis.com/v1beta/',
'timeout' => 30,
]);
}

public function chat(array $messages, string $model, float $temperature = 0.0): string
{
try {
// Convert OpenAI-style messages to Gemini format
$contents = $this->convertMessagesToGeminiFormat($messages);

$response = $this->http->post("models/{$model}:generateContent", [
'query' => ['key' => $this->apiKey],
'json' => [
'contents' => $contents,
'generationConfig' => [
'temperature' => $temperature,
'responseMimeType' => 'application/json',
],
],
]);

$payload = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);

// Extract content from Gemini response format
$content = $payload['candidates'][0]['content']['parts'][0]['text'] ?? '';
if (!is_string($content) || $content === '') {
throw new GenerationException('Empty response content from Gemini.');
}

return $content;
} catch (GuzzleException $e) {
$this->logger?->error('Gemini request failed', ['exception' => $e]);
throw new GenerationException('Gemini request failed: ' . $e->getMessage());
} catch (\JsonException $e) {
$this->logger?->error('Failed to parse Gemini response JSON', ['exception' => $e]);
throw new GenerationException('Failed to parse Gemini response JSON: ' . $e->getMessage());
}
}

/**
* Convert OpenAI-style messages to Gemini format
*
* @param array<array{role:string, content:string}> $messages
* @return array<array{role:string, parts:array<array{text:string}>}>
*/
private function convertMessagesToGeminiFormat(array $messages): array
{
$contents = [];

foreach ($messages as $message) {
$role = $message['role'] === 'system' || $message['role'] === 'user' ? 'user' : 'model';
$contents[] = [
'role' => $role,
'parts' => [
['text' => $message['content']],
],
];
}

return $contents;
}
}