feat: implement user management system and integrate AI services with Ollama and Gemini

This commit is contained in:
jeremy bayse
2026-06-21 00:12:37 +02:00
parent 648749a69a
commit b85c2ba5a1
17 changed files with 1074 additions and 7 deletions
+145
View File
@@ -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;
}
}