146 lines
5.5 KiB
PHP
146 lines
5.5 KiB
PHP
<?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;
|
|
}
|
|
}
|