Refactoring AI candidate analysis: UI improvements, data normalization, provider management and real-time score clamping
This commit is contained in:
@@ -23,8 +23,8 @@ class AIAnalysisController extends Controller
|
||||
}
|
||||
|
||||
// Restriction: Une analyse tous les 7 jours maximum par candidat
|
||||
// Le super_admin peut outrepasser cette restriction via le paramètre 'force'
|
||||
$shouldCheckRestriction = !($request->force && auth()->user()->isSuperAdmin());
|
||||
// Tout admin peut outrepasser cette restriction s'il utilise l'option 'force'
|
||||
$shouldCheckRestriction = !$request->input('force', false);
|
||||
|
||||
if ($shouldCheckRestriction && $candidate->ai_analysis && isset($candidate->ai_analysis['analyzed_at'])) {
|
||||
$lastAnalysis = Carbon::parse($candidate->ai_analysis['analyzed_at']);
|
||||
|
||||
@@ -149,7 +149,7 @@ class AttemptController extends Controller
|
||||
$this->authorizeAdmin();
|
||||
|
||||
$request->validate([
|
||||
'score' => 'required|numeric|min:0'
|
||||
'score' => 'required|numeric|min:0|max:' . $answer->question->points
|
||||
]);
|
||||
|
||||
$answer->update(['score' => $request->score]);
|
||||
|
||||
@@ -103,12 +103,12 @@ class CandidateController extends Controller
|
||||
'jobPositions' => \App\Models\JobPosition::all(),
|
||||
'ai_config' => [
|
||||
'default' => env('AI_DEFAULT_PROVIDER', 'ollama'),
|
||||
'enabled_providers' => array_filter([
|
||||
'ollama' => true, // Toujours dispo car local ou simulé
|
||||
'providers' => array_keys(array_filter([
|
||||
'ollama' => true,
|
||||
'openai' => !empty(env('OPENAI_API_KEY')),
|
||||
'anthropic' => !empty(env('ANTHROPIC_API_KEY')),
|
||||
'gemini' => !empty(env('GEMINI_API_KEY')),
|
||||
], function($v) { return $v; })
|
||||
])),
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
@@ -151,7 +151,19 @@ class AIAnalysisService
|
||||
if (isset($data['score_global']) && !isset($data['match_score'])) {
|
||||
$normalized['match_score'] = $data['score_global'];
|
||||
}
|
||||
|
||||
if (isset($data['score']) && !isset($data['match_score'])) {
|
||||
$normalized['match_score'] = $data['score'];
|
||||
}
|
||||
|
||||
if (isset($data['points_forts']) && !isset($data['strengths'])) {
|
||||
$normalized['strengths'] = $data['points_forts'];
|
||||
}
|
||||
|
||||
if (isset($data['points_faibles']) && !isset($data['gaps'])) {
|
||||
$normalized['gaps'] = $data['points_faibles'];
|
||||
}
|
||||
|
||||
if (isset($data['recommandation']) && !isset($data['verdict'])) {
|
||||
$normalized['verdict'] = $data['recommandation'];
|
||||
}
|
||||
@@ -159,14 +171,63 @@ class AIAnalysisService
|
||||
if (isset($data['synthese']) && !isset($data['summary'])) {
|
||||
$normalized['summary'] = $data['synthese'];
|
||||
}
|
||||
|
||||
// List-specific normalization (handle list of objects or strings)
|
||||
$cleanList = function($list) {
|
||||
if (!is_array($list)) return [];
|
||||
return array_map(function($item) {
|
||||
if (is_array($item)) {
|
||||
$type = $item['type'] ?? $item['title'] ?? $item['category'] ?? null;
|
||||
$desc = $item['description'] ?? $item['value'] ?? $item['content'] ?? null;
|
||||
if ($type && $desc) return "{$type} : {$desc}";
|
||||
if ($desc) return $desc;
|
||||
if ($type) return $type;
|
||||
return json_encode($item);
|
||||
}
|
||||
return (string) $item;
|
||||
}, $list);
|
||||
};
|
||||
|
||||
if (isset($normalized['strengths'])) {
|
||||
$normalized['strengths'] = $cleanList($normalized['strengths']);
|
||||
}
|
||||
|
||||
if (isset($data['points_vigilance']) && !isset($data['gaps'])) {
|
||||
// Handle if points_vigilance is a list of objects (as in user's prompt)
|
||||
if (is_array($data['points_vigilance']) && isset($data['points_vigilance'][0]) && is_array($data['points_vigilance'][0])) {
|
||||
$normalized['gaps'] = array_map(fn($i) => ($i['type'] ?? '') . ': ' . ($i['description'] ?? ''), $data['points_vigilance']);
|
||||
} else {
|
||||
$normalized['gaps'] = $data['points_vigilance'];
|
||||
if (isset($normalized['gaps'])) {
|
||||
$normalized['gaps'] = $cleanList($normalized['gaps']);
|
||||
}
|
||||
|
||||
if (isset($normalized['elements_bloquants'])) {
|
||||
$normalized['elements_bloquants'] = $cleanList($normalized['elements_bloquants']);
|
||||
}
|
||||
|
||||
// Ensure match_score is a numeric value and handle common AI formatting quirks
|
||||
if (isset($normalized['match_score'])) {
|
||||
$scoreValue = $normalized['match_score'];
|
||||
|
||||
if (is_string($scoreValue)) {
|
||||
// If AI returns something like "18/20", take the first part
|
||||
if (str_contains($scoreValue, '/')) {
|
||||
$scoreValue = explode('/', $scoreValue)[0];
|
||||
}
|
||||
// Convert comma to dot for European decimals
|
||||
$scoreValue = str_replace(',', '.', $scoreValue);
|
||||
// Keep only digits and the first decimal point
|
||||
$scoreValue = preg_replace('/[^0-9.]/', '', $scoreValue);
|
||||
}
|
||||
|
||||
$num = (float)$scoreValue;
|
||||
|
||||
// If the AI returned a ratio beneath 1 (e.g. 0.85 for 85%), scale it up
|
||||
if ($num > 0 && $num < 1.1 && !is_int($normalized['match_score'])) {
|
||||
// But be careful: a score of "1" might honestly be 1/100
|
||||
// but 0.95 is almost certainly a ratio.
|
||||
if ($num < 1 || str_contains((string)$normalized['match_score'], '.')) {
|
||||
$num *= 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at 100
|
||||
$normalized['match_score'] = (int) min(100, round($num));
|
||||
}
|
||||
|
||||
// Ensure default keys exist even if empty
|
||||
@@ -175,6 +236,9 @@ class AIAnalysisService
|
||||
$normalized['verdict'] = $normalized['verdict'] ?? "Indéterminé";
|
||||
$normalized['strengths'] = $normalized['strengths'] ?? [];
|
||||
$normalized['gaps'] = $normalized['gaps'] ?? [];
|
||||
$normalized['scores_detailles'] = $normalized['scores_detailles'] ?? null;
|
||||
$normalized['elements_bloquants'] = $normalized['elements_bloquants'] ?? [];
|
||||
$normalized['questions_entretien_suggerees'] = $normalized['questions_entretien_suggerees'] ?? [];
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user