AI Analysis: add support for multiple providers (OpenAI, Claude, Gemini)
This commit is contained in:
@@ -15,14 +15,14 @@ class AIAnalysisController extends Controller
|
||||
$this->aiService = $aiService;
|
||||
}
|
||||
|
||||
public function analyze(Candidate $candidate)
|
||||
public function analyze(Request $request, Candidate $candidate)
|
||||
{
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$analysis = $this->aiService->analyze($candidate);
|
||||
$analysis = $this->aiService->analyze($candidate, $request->provider);
|
||||
|
||||
// Persist the analysis on the candidate profile
|
||||
$candidate->update([
|
||||
|
||||
@@ -21,7 +21,7 @@ class AIAnalysisService
|
||||
/**
|
||||
* Analyze a candidate against their assigned Job Position.
|
||||
*/
|
||||
public function analyze(Candidate $candidate)
|
||||
public function analyze(Candidate $candidate, ?string $provider = null)
|
||||
{
|
||||
if (!$candidate->job_position_id) {
|
||||
throw new \Exception("Le candidat n'est associé à aucune fiche de poste.");
|
||||
@@ -36,7 +36,7 @@ class AIAnalysisService
|
||||
throw new \Exception("Impossible d'extraire le texte du CV.");
|
||||
}
|
||||
|
||||
return $this->callAI($candidate, $cvText, $letterText);
|
||||
return $this->callAI($candidate, $cvText, $letterText, $provider);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,8 +75,10 @@ class AIAnalysisService
|
||||
/**
|
||||
* Call the AI API (using a placeholder for now, or direct Http call).
|
||||
*/
|
||||
protected function callAI(Candidate $candidate, string $cvText, ?string $letterText)
|
||||
protected function callAI(Candidate $candidate, string $cvText, ?string $letterText, ?string $provider = null)
|
||||
{
|
||||
$provider = $provider ?: env('AI_DEFAULT_PROVIDER', 'ollama');
|
||||
|
||||
$jobTitle = $candidate->jobPosition->title;
|
||||
$jobDesc = $candidate->jobPosition->description;
|
||||
$requirements = implode(", ", $candidate->jobPosition->requirements ?? []);
|
||||
@@ -105,10 +107,17 @@ class AIAnalysisService
|
||||
- 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.
|
||||
|
||||
|
||||
return match ($provider) {
|
||||
'openai' => $this->callOpenAI($prompt),
|
||||
'anthropic' => $this->callAnthropic($prompt),
|
||||
'gemini' => $this->callGemini($prompt),
|
||||
default => $this->callOllama($prompt),
|
||||
};
|
||||
}
|
||||
|
||||
protected function callOllama(string $prompt)
|
||||
{
|
||||
$ollamaUrl = env('OLLAMA_URL', 'http://localhost:11434/api/generate');
|
||||
$ollamaModel = env('OLLAMA_MODEL', 'mistral');
|
||||
|
||||
@@ -123,16 +132,97 @@ class AIAnalysisService
|
||||
if ($response->successful()) {
|
||||
return json_decode($response->json('response'), true);
|
||||
} else {
|
||||
Log::warning("AI Provider Error: HTTP " . $response->status() . " - " . $response->body());
|
||||
Log::warning("AI Provider Error (Ollama): 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 $this->getSimulatedAnalysis();
|
||||
}
|
||||
|
||||
protected function callOpenAI(string $prompt)
|
||||
{
|
||||
$apiKey = env('OPENAI_API_KEY');
|
||||
if (!$apiKey) return $this->getSimulatedAnalysis();
|
||||
|
||||
try {
|
||||
$response = Http::withToken($apiKey)->timeout(60)->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => 'gpt-4o',
|
||||
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||||
'response_format' => ['type' => 'json_object']
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return json_decode($response->json('choices.0.message.content'), true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("OpenAI Analysis Failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->getSimulatedAnalysis();
|
||||
}
|
||||
|
||||
protected function callAnthropic(string $prompt)
|
||||
{
|
||||
$apiKey = env('ANTHROPIC_API_KEY');
|
||||
if (!$apiKey) return $this->getSimulatedAnalysis();
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'x-api-key' => $apiKey,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
'content-type' => 'application/json'
|
||||
])->timeout(60)->post('https://api.anthropic.com/v1/messages', [
|
||||
'model' => 'claude-3-5-sonnet-20240620',
|
||||
'max_tokens' => 1024,
|
||||
'messages' => [['role' => 'user', 'content' => $prompt]]
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$content = $response->json('content.0.text');
|
||||
return json_decode($this->extractJson($content), true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Anthropic Analysis Failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->getSimulatedAnalysis();
|
||||
}
|
||||
|
||||
protected function callGemini(string $prompt)
|
||||
{
|
||||
$apiKey = env('GEMINI_API_KEY');
|
||||
if (!$apiKey) return $this->getSimulatedAnalysis();
|
||||
|
||||
try {
|
||||
$response = Http::timeout(60)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" . $apiKey, [
|
||||
'contents' => [['parts' => [['text' => $prompt]]]],
|
||||
'generationConfig' => ['responseMimeType' => 'application/json']
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$text = $response->json('candidates.0.content.parts.0.text');
|
||||
return json_decode($this->extractJson($text), true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Gemini Analysis Failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->getSimulatedAnalysis();
|
||||
}
|
||||
|
||||
private function extractJson($string)
|
||||
{
|
||||
preg_match('/\{.*\}/s', $string, $matches);
|
||||
return $matches[0] ?? '{}';
|
||||
}
|
||||
|
||||
private function getSimulatedAnalysis()
|
||||
{
|
||||
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.",
|
||||
'summary' => "Analyse simulée (IA non connectée ou erreur API). 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"
|
||||
|
||||
Reference in New Issue
Block a user