147 lines
5.8 KiB
PHP
147 lines
5.8 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 MistralService
|
|
{
|
|
protected ?string $apiKey;
|
|
protected string $model;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->apiKey = config('services.mistral.key');
|
|
$this->model = config('services.mistral.model', 'pixtral-12b-2409');
|
|
}
|
|
|
|
/**
|
|
* Analyse un document joint via l'API Mistral AI 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 Mistral AI.
|
|
*/
|
|
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 Mistral AI 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 Mistral AI n'est pas configurée dans l'environnement.");
|
|
}
|
|
|
|
if ($mimeType === 'application/pdf') {
|
|
throw new \Exception("Le modèle Mistral AI 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
|
|
$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.
|
|
|
|
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://api.mistral.ai/v1/chat/completions";
|
|
|
|
Log::info("Début de l'analyse OCR Mistral AI pour le fichier : {$fileName}");
|
|
|
|
$response = Http::withHeaders([
|
|
'Authorization' => 'Bearer ' . $this->apiKey,
|
|
'Content-Type' => 'application/json',
|
|
])->post($url, [
|
|
'model' => $this->model,
|
|
'messages' => [
|
|
[
|
|
'role' => 'user',
|
|
'content' => [
|
|
[
|
|
'type' => 'text',
|
|
'text' => $prompt
|
|
],
|
|
[
|
|
'type' => 'image_url',
|
|
'image_url' => "data:{$mimeType};base64,{$base64Data}"
|
|
]
|
|
]
|
|
]
|
|
],
|
|
'response_format' => [
|
|
'type' => 'json_object'
|
|
],
|
|
'temperature' => 0.0
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
Log::error("Échec de l'appel Mistral AI API : " . $response->body());
|
|
throw new \Exception("Erreur de communication avec l'API Mistral AI : " . $response->status());
|
|
}
|
|
|
|
$data = $response->json();
|
|
$textResult = $data['choices'][0]['message']['content'] ?? null;
|
|
|
|
if (!$textResult) {
|
|
Log::error("Mistral AI 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 Mistral AI : " . $textResult);
|
|
throw new \Exception("Le format des données retournées par l'OCR Mistral est invalide.");
|
|
}
|
|
|
|
Log::info("OCR Mistral AI réussi pour le fichier : {$fileName}");
|
|
|
|
return $decoded;
|
|
}
|
|
}
|