feat: implement user management system and integrate AI services with Ollama and Gemini
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GeminiService
|
||||
{
|
||||
protected ?string $apiKey;
|
||||
protected string $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.gemini.key');
|
||||
$this->model = config('services.gemini.model', 'gemini-1.5-flash');
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse un document joint via l'API Gemini et extrait les détails structurés du devis.
|
||||
*/
|
||||
public function analyzeQuote(Attachment $attachment): array
|
||||
{
|
||||
$disk = Storage::disk('public');
|
||||
if (!$disk->exists($attachment->file_path)) {
|
||||
throw new \Exception("Le fichier joint n'existe pas sur le disque.");
|
||||
}
|
||||
|
||||
$path = $disk->path($attachment->file_path);
|
||||
$fileContent = file_get_contents($path);
|
||||
$mimeType = $disk->mimeType($attachment->file_path);
|
||||
|
||||
return $this->analyzeContent($fileContent, $mimeType, $attachment->file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse directement un fichier téléchargé temporairement via l'API Gemini.
|
||||
*/
|
||||
public function analyzeUploadedFile(\Illuminate\Http\UploadedFile $file): array
|
||||
{
|
||||
$fileContent = file_get_contents($file->getRealPath());
|
||||
$mimeType = $file->getMimeType();
|
||||
|
||||
return $this->analyzeContent($fileContent, $mimeType, $file->getClientOriginalName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute l'appel à l'API Gemini pour extraire les données du devis.
|
||||
*/
|
||||
protected function analyzeContent(string $fileContent, string $mimeType, string $fileName): array
|
||||
{
|
||||
if (empty($this->apiKey)) {
|
||||
throw new \Exception("La clé API Gemini n'est pas configurée dans l'environnement.");
|
||||
}
|
||||
|
||||
// Encodage Base64 pour l'API Gemini
|
||||
$base64Data = base64_encode($fileContent);
|
||||
|
||||
// Prompt strict demandant un format JSON conforme aux attentes du formulaire
|
||||
$prompt = "Tu es un assistant administratif spécialisé dans l'analyse de devis informatiques et de prestations.
|
||||
Analyse le document joint (devis) et extrais de façon très précise les informations suivantes au format JSON uniquement.
|
||||
|
||||
Structure JSON attendue :
|
||||
{
|
||||
\"title\": \"Un titre descriptif pour le devis (ex: Achat matériel informatique ou Prestation d'installation)\",
|
||||
\"date_devis\": \"La date d'émission du devis au format YYYY-MM-DD (si non trouvée, renvoie null)\",
|
||||
\"supplier\": \"Le nom de l'entreprise émettrice / fournisseur\",
|
||||
\"quote_number\": \"Le numéro de devis du fournisseur\",
|
||||
\"tva_rate\": 20.0, // le taux de TVA standard constaté dans le document (valeur numérique, ex: 20 ou 5.5)
|
||||
\"total_ht\": 1250.00, // Le montant total HT du devis (nombre décimal)
|
||||
\"items\": [
|
||||
{
|
||||
\"type\": \"Matériel|Licence|Prestation|Infrastructure|Autre\", // Choisis la catégorie la plus adaptée parmi ces 5 valeurs exactes
|
||||
\"designation\": \"La description de l'article ou de la prestation\",
|
||||
\"quantite\": 1, // La quantité (entier ou décimal)
|
||||
\"prix_unitaire\": 120.50 // Le prix unitaire HT (nombre décimal)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Règles importantes :
|
||||
1. Extrais TOUTES les lignes d'articles réelles présentes dans le devis. Ne résume pas ou ne groupe pas les lignes si elles sont détaillées.
|
||||
2. Pour chaque article, détermine la catégorie la plus adaptée ('Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre').
|
||||
3. Ne réponds QUE par le JSON brut, valide, sans aucun texte explicatif avant ou après, et sans bloc de code markdown.
|
||||
4. Assure-toi que les montants et quantités sont des nombres.";
|
||||
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$this->model}:generateContent?key={$this->apiKey}";
|
||||
|
||||
Log::info("Début de l'analyse OCR Gemini pour le fichier : {$fileName}");
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
])->post($url, [
|
||||
'contents' => [
|
||||
[
|
||||
'parts' => [
|
||||
['text' => $prompt],
|
||||
[
|
||||
'inlineData' => [
|
||||
'mimeType' => $mimeType,
|
||||
'data' => $base64Data
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'generationConfig' => [
|
||||
'responseMimeType' => 'application/json',
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error("Échec de l'appel Gemini API : " . $response->body());
|
||||
throw new \Exception("Erreur de communication avec l'API Gemini : " . $response->status());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$textResult = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
|
||||
if (!$textResult) {
|
||||
Log::error("Gemini n'a renvoyé aucun contenu exploitable.");
|
||||
throw new \Exception("L'analyse du devis n'a retourné aucun résultat.");
|
||||
}
|
||||
|
||||
$decoded = json_decode($textResult, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error("Impossible de parser le JSON retourné par Gemini : " . $textResult);
|
||||
throw new \Exception("Le format des données retournées par l'OCR est invalide.");
|
||||
}
|
||||
|
||||
Log::info("OCR Gemini réussi pour le fichier : {$fileName}");
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OllamaService
|
||||
{
|
||||
protected string $url;
|
||||
protected string $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->url = rtrim(config('services.ollama.url', 'http://localhost:11434'), '/');
|
||||
$this->model = config('services.ollama.model', 'glm-ocr');
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse un document joint via l'API Ollama et extrait les détails structurés du devis.
|
||||
*/
|
||||
public function analyzeQuote(Attachment $attachment): array
|
||||
{
|
||||
$disk = Storage::disk('public');
|
||||
if (!$disk->exists($attachment->file_path)) {
|
||||
throw new \Exception("Le fichier joint n'existe pas sur le disque.");
|
||||
}
|
||||
|
||||
$path = $disk->path($attachment->file_path);
|
||||
$fileContent = file_get_contents($path);
|
||||
$mimeType = $disk->mimeType($attachment->file_path);
|
||||
|
||||
return $this->analyzeContent($fileContent, $mimeType, $attachment->file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse directement un fichier téléchargé temporairement via l'API Ollama.
|
||||
*/
|
||||
public function analyzeUploadedFile(\Illuminate\Http\UploadedFile $file): array
|
||||
{
|
||||
$fileContent = file_get_contents($file->getRealPath());
|
||||
$mimeType = $file->getMimeType();
|
||||
|
||||
return $this->analyzeContent($fileContent, $mimeType, $file->getClientOriginalName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute l'appel à l'API Ollama pour extraire les données du devis.
|
||||
*/
|
||||
protected function analyzeContent(string $fileContent, string $mimeType, string $fileName): array
|
||||
{
|
||||
if ($mimeType === 'application/pdf') {
|
||||
throw new \Exception("Le modèle local (Ollama/GLM-OCR) ne prend en charge que les images (PNG, JPG, JPEG, WEBP). Pour analyser un fichier PDF, veuillez utiliser Gemini ou convertir votre document PDF en image.");
|
||||
}
|
||||
|
||||
// Encodage Base64
|
||||
$base64Data = base64_encode($fileContent);
|
||||
|
||||
// Prompt strict demandant un format JSON conforme aux attentes du formulaire sans inclure de placeholders dans la structure JSON
|
||||
$prompt = "Tu es un assistant administratif spécialisé dans l'analyse de devis.
|
||||
Analyse le document joint pour en extraire très précisément les informations requises et retourne-les au format JSON uniquement.
|
||||
|
||||
Instructions d'extraction :
|
||||
- title : Donne un titre descriptif et court basé sur l'objet du devis (ex: 'Achat matériel' ou 'Prestation installation').
|
||||
- date_devis : Extrais la date d'émission au format YYYY-MM-DD (renvoie null si absente).
|
||||
- supplier : Extrais le nom de l'entreprise émettrice / fournisseur.
|
||||
- quote_number : Extrais le numéro du devis.
|
||||
- tva_rate : Détermine le taux de TVA standard constaté (nombre décimal, ex: 20.0).
|
||||
- total_ht : Extrais le montant total HT du devis (nombre décimal).
|
||||
- items : Liste de TOUTES les lignes d'articles réelles détaillées. Pour chaque article :
|
||||
* type : Choisis une valeur parmi ces 5 valeurs exactes uniquement : 'Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre'.
|
||||
* designation : La description / libellé de la ligne d'article.
|
||||
* quantite : La quantité (nombre).
|
||||
* prix_unitaire : Le prix unitaire HT (nombre).
|
||||
|
||||
Retourne UNIQUEMENT le JSON brut suivant cette structure exacte, sans commentaires, sans texte explicatif, et sans bloc markdown :
|
||||
{
|
||||
\"title\": \"\",
|
||||
\"date_devis\": \"\",
|
||||
\"supplier\": \"\",
|
||||
\"quote_number\": \"\",
|
||||
\"tva_rate\": 20.0,
|
||||
\"total_ht\": 0.0,
|
||||
\"items\": [
|
||||
{
|
||||
\"type\": \"\",
|
||||
\"designation\": \"\",
|
||||
\"quantite\": 1,
|
||||
\"prix_unitaire\": 0.0
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
$url = "{$this->url}/api/generate";
|
||||
|
||||
Log::info("Début de l'analyse OCR Ollama (GLM-OCR) pour le fichier : {$fileName}");
|
||||
|
||||
// Appel API Ollama
|
||||
$response = Http::withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(120)->post($url, [
|
||||
'model' => $this->model,
|
||||
'prompt' => $prompt,
|
||||
'images' => [$base64Data],
|
||||
'stream' => false,
|
||||
'format' => 'json',
|
||||
'options' => [
|
||||
'temperature' => 0.0,
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error("Échec de l'appel Ollama API : " . $response->body());
|
||||
throw new \Exception("Erreur de communication avec Ollama : " . $response->status());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$textResult = $data['response'] ?? null;
|
||||
|
||||
if (!$textResult) {
|
||||
Log::error("Ollama n'a renvoyé aucun contenu exploitable.");
|
||||
throw new \Exception("L'analyse du devis n'a retourné aucun résultat.");
|
||||
}
|
||||
|
||||
// Nettoyer si du markdown entoure le JSON (parfois Ollama insiste malgré format: json)
|
||||
$textResult = trim($textResult);
|
||||
if (str_starts_with($textResult, '```')) {
|
||||
$textResult = preg_replace('/^```(?:json)?\s*/i', '', $textResult);
|
||||
$textResult = preg_replace('/```$/', '', $textResult);
|
||||
$textResult = trim($textResult);
|
||||
}
|
||||
|
||||
$decoded = json_decode($textResult, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error("Impossible de parser le JSON retourné par Ollama : " . $textResult);
|
||||
throw new \Exception("Le format des données retournées par l'OCR Ollama est invalide.");
|
||||
}
|
||||
|
||||
Log::info("OCR Ollama réussi pour le fichier : {$fileName}");
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user