173 lines
6.1 KiB
PHP
173 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Core;
|
|
|
|
use App\Services\InvoiceExtractionService;
|
|
|
|
/**
|
|
* Gemini AI Integration for Invoice Extraction
|
|
* Optimized for Jordan UBL 2.1 Compliance
|
|
*/
|
|
class AI
|
|
{
|
|
private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/" . AIConfig::MODEL_NAME . ":generateContent";
|
|
|
|
private static int $maxRetries = 3;
|
|
|
|
/**
|
|
* Extract Data from Invoice Image or PDF (Base64)
|
|
*/
|
|
public static function extractInvoiceData(string $base64Data, string $mimeType): ?array
|
|
{
|
|
$apiKey = env('GEMINI_API_KEY');
|
|
if (!$apiKey) {
|
|
error_log('AI Error: GEMINI_API_KEY is missing');
|
|
return null;
|
|
}
|
|
|
|
$prompt = AIConfig::getExtractionPrompt();
|
|
|
|
$payload = [
|
|
"contents" => [
|
|
[
|
|
"parts" => [
|
|
["text" => $prompt . " If the image is not an invoice, is blank, or is completely unreadable, return ONLY: {\"error\": \"invalid_invoice\"}. DO NOT guess or invent data."],
|
|
[
|
|
"inline_data" => [
|
|
"mime_type" => $mimeType,
|
|
"data" => $base64Data
|
|
]
|
|
]
|
|
]
|
|
]
|
|
],
|
|
"generationConfig" => [
|
|
"response_mime_type" => "application/json"
|
|
]
|
|
];
|
|
|
|
// Retry with exponential backoff for 503/429 errors
|
|
for ($attempt = 1; $attempt <= self::$maxRetries; $attempt++) {
|
|
$ch = curl_init(self::$baseUrl . "?key=" . $apiKey);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($curlError) {
|
|
error_log("AI Error: cURL failed (attempt $attempt): $curlError");
|
|
if ($attempt < self::$maxRetries) {
|
|
$wait = pow(2, $attempt) + rand(1, 3);
|
|
echo " Retrying in {$wait}s (cURL error)...\n";
|
|
sleep($wait);
|
|
continue;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode === 200) {
|
|
break; // Success
|
|
}
|
|
|
|
// Retry on 503 (overloaded) or 429 (rate limit)
|
|
if (in_array($httpCode, [503, 429]) && $attempt < self::$maxRetries) {
|
|
$wait = pow(2, $attempt) + rand(1, 3);
|
|
echo " Gemini $httpCode — retrying in {$wait}s (attempt $attempt/" . self::$maxRetries . ")...\n";
|
|
sleep($wait);
|
|
continue;
|
|
}
|
|
|
|
error_log("AI Error: Gemini API returned code $httpCode. Response: " . $response);
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode !== 200) {
|
|
error_log("AI Error: All retries exhausted. Last code: $httpCode");
|
|
return null;
|
|
}
|
|
|
|
$result = json_decode($response, true);
|
|
$textResponse = $result['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
|
|
|
if (!$textResponse) return null;
|
|
|
|
$data = json_decode($textResponse, true);
|
|
if (isset($data['error']) && $data['error'] === 'invalid_invoice') {
|
|
return null;
|
|
}
|
|
|
|
// Track token usage from Gemini response
|
|
$usage = $result['usageMetadata'] ?? [];
|
|
if (!empty($usage)) {
|
|
self::logTokenUsage($usage);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Log AI token usage to the database for cost tracking
|
|
*/
|
|
private static function logTokenUsage(array $usage): void
|
|
{
|
|
try {
|
|
$db = Database::getInstance();
|
|
|
|
$inputTokens = (int)($usage['promptTokenCount'] ?? 0);
|
|
$outputTokens = (int)($usage['candidatesTokenCount'] ?? 0);
|
|
$totalTokens = (int)($usage['totalTokenCount'] ?? ($inputTokens + $outputTokens));
|
|
|
|
// Gemini Flash Lite pricing: $0.075/1M input, $0.30/1M output
|
|
$inputCost = ($inputTokens / 1000000) * 0.075;
|
|
$outputCost = ($outputTokens / 1000000) * 0.30;
|
|
$totalCostUsd = $inputCost + $outputCost;
|
|
$totalCostJod = $totalCostUsd * 0.709; // 1 USD ≈ 0.709 JOD
|
|
|
|
$db->prepare("
|
|
INSERT INTO ai_usage_log (id, input_tokens, output_tokens, total_tokens, cost_usd, cost_jod, model, created_at)
|
|
VALUES (UUID(), ?, ?, ?, ?, ?, 'gemini-flash-lite', NOW())
|
|
")->execute([
|
|
$inputTokens,
|
|
$outputTokens,
|
|
$totalTokens,
|
|
round($totalCostUsd, 8),
|
|
round($totalCostJod, 8),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
// Never crash the main flow for logging
|
|
error_log("[AI] Token usage log failed: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get aggregated AI usage stats
|
|
*/
|
|
public static function getUsageStats(?string $tenantId = null): array
|
|
{
|
|
try {
|
|
$db = Database::getInstance();
|
|
$stmt = $db->query("
|
|
SELECT
|
|
COUNT(*) as total_requests,
|
|
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
|
|
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
|
|
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
COALESCE(SUM(cost_usd), 0) as total_cost_usd,
|
|
COALESCE(SUM(cost_jod), 0) as total_cost_jod,
|
|
COALESCE(AVG(total_tokens), 0) as avg_tokens_per_request,
|
|
COALESCE(AVG(cost_jod), 0) as avg_cost_jod_per_request
|
|
FROM ai_usage_log
|
|
");
|
|
return $stmt->fetch() ?: [];
|
|
} catch (\Exception $e) {
|
|
return [];
|
|
}
|
|
}
|
|
}
|