parser = new Parser(); } /** * Analyze a candidate against their assigned Job Position. */ public function analyze(Candidate $candidate) { if (!$candidate->job_position_id) { throw new \Exception("Le candidat n'est associé à aucune fiche de poste."); } $candidate->load(['documents', 'jobPosition']); $cvText = $this->extractTextFromDocument($candidate->documents->where('type', 'cv')->first()); $letterText = $this->extractTextFromDocument($candidate->documents->where('type', 'cover_letter')->first()); if (!$cvText) { throw new \Exception("Impossible d'extraire le texte du CV."); } return $this->callAI($candidate, $cvText, $letterText); } /** * Extract text from a PDF document. */ protected function extractTextFromDocument(?Document $document): ?string { if (!$document || !Storage::disk('local')->exists($document->file_path)) { return null; } try { $pdf = $this->parser->parseFile(Storage::disk('local')->path($document->file_path)); return $pdf->getText(); } catch (\Exception $e) { Log::error("PDF Extraction Error: " . $e->getMessage()); return null; } } /** * Call the AI API (using a placeholder for now, or direct Http call). */ protected function callAI(Candidate $candidate, string $cvText, ?string $letterText) { $jobTitle = $candidate->jobPosition->title; $jobDesc = $candidate->jobPosition->description; $requirements = implode(", ", $candidate->jobPosition->requirements ?? []); $prompt = "Tu es un expert en recrutement technique. Analyse le CV (et la lettre de motivation si présente) d'un candidat pour le poste de '{$jobTitle}'. DESCRIPTION DU POSTE: {$jobDesc} COMPÉTENCES REQUISES: {$requirements} CONTENU DU CV: {$cvText} CONTENU DE LA LETTRE DE MOTIVATION: " . ($letterText ?? "Non fournie") . " Fournis une analyse structurée en JSON avec les clés suivantes: - match_score: note de 0 à 100 - summary: résumé de 3-4 phrases sur le profil - strengths: liste des points forts par rapport au poste - gaps: liste des compétences manquantes ou points de vigilance - verdict: une conclusion (Favorable, Très Favorable, Réservé, Défavorable) Réponds UNIQUEMENT en JSON pur."; // For now, I'll use a mocked response or try to use a generic endpoint if configured. // I'll check if the user has an Ollama endpoint. $ollamaUrl = config('services.ollama.url', 'http://localhost:11434/api/generate'); try { $response = Http::timeout(120)->post($ollamaUrl, [ 'model' => 'mistral', 'prompt' => $prompt, 'stream' => false, 'format' => 'json' ]); if ($response->successful()) { return json_decode($response->json('response'), true); } else { Log::warning("AI Provider Error: HTTP " . $response->status() . " - " . $response->body()); } } catch (\Exception $e) { Log::error("AI Connection Failed (Ollama): " . $e->getMessage()); } // Fallback for demo if Ollama is not running return [ 'match_score' => 75, 'summary' => "Analyse simulée (IA non connectée). Le candidat semble avoir une solide expérience mais certains points techniques doivent être vérifiés.", 'strengths' => ["Expérience pertinente", "Bonne présentation"], 'gaps' => ["Compétences spécifiques à confirmer"], 'verdict' => "Favorable" ]; } }