From d924765b94653e88556a3c3d7cfdde2fca76b255 Mon Sep 17 00:00:00 2001 From: jeremy bayse Date: Sun, 19 Apr 2026 21:24:56 +0200 Subject: [PATCH] feat(ai): refactor AI prompts and add bypass option to Job Positions --- .../Controllers/JobPositionController.php | 4 ++ app/Models/JobPosition.php | 3 +- app/Services/AIAnalysisService.php | 58 +++++++++---------- config/ai.php | 34 +++++++++++ ...ass_base_prompt_to_job_positions_table.php | 28 +++++++++ .../js/Pages/Admin/JobPositions/Index.vue | 14 +++++ 6 files changed, 111 insertions(+), 30 deletions(-) create mode 100644 config/ai.php create mode 100644 database/migrations/2026_04_19_171723_add_ai_bypass_base_prompt_to_job_positions_table.php diff --git a/app/Http/Controllers/JobPositionController.php b/app/Http/Controllers/JobPositionController.php index a54ef66..4ec1dd7 100644 --- a/app/Http/Controllers/JobPositionController.php +++ b/app/Http/Controllers/JobPositionController.php @@ -28,6 +28,7 @@ class JobPositionController extends Controller 'description' => 'required|string', 'requirements' => 'nullable|array', 'ai_prompt' => 'nullable|string', + 'ai_bypass_base_prompt' => 'boolean', 'tenant_id' => 'nullable|exists:tenants,id', 'quiz_ids' => 'nullable|array', 'quiz_ids.*' => 'exists:quizzes,id', @@ -38,6 +39,7 @@ class JobPositionController extends Controller 'description' => $request->description, 'requirements' => $request->requirements, 'ai_prompt' => $request->ai_prompt, + 'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'), 'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id, ]); @@ -55,6 +57,7 @@ class JobPositionController extends Controller 'description' => 'required|string', 'requirements' => 'nullable|array', 'ai_prompt' => 'nullable|string', + 'ai_bypass_base_prompt' => 'boolean', 'tenant_id' => 'nullable|exists:tenants,id', 'quiz_ids' => 'nullable|array', 'quiz_ids.*' => 'exists:quizzes,id', @@ -65,6 +68,7 @@ class JobPositionController extends Controller 'description' => $request->description, 'requirements' => $request->requirements, 'ai_prompt' => $request->ai_prompt, + 'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'), 'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id, ]); diff --git a/app/Models/JobPosition.php b/app/Models/JobPosition.php index 7e05de9..2fa8913 100644 --- a/app/Models/JobPosition.php +++ b/app/Models/JobPosition.php @@ -9,13 +9,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Traits\BelongsToTenant; -#[Fillable(['title', 'description', 'requirements', 'ai_prompt', 'gemini_cache_id', 'gemini_cache_expires_at', 'tenant_id'])] +#[Fillable(['title', 'description', 'requirements', 'ai_prompt', 'ai_bypass_base_prompt', 'gemini_cache_id', 'gemini_cache_expires_at', 'tenant_id'])] class JobPosition extends Model { use HasFactory, BelongsToTenant; protected $casts = [ 'requirements' => 'array', + 'ai_bypass_base_prompt' => 'boolean', 'gemini_cache_expires_at' => 'datetime', ]; diff --git a/app/Services/AIAnalysisService.php b/app/Services/AIAnalysisService.php index f9eedba..5b7ad9e 100644 --- a/app/Services/AIAnalysisService.php +++ b/app/Services/AIAnalysisService.php @@ -80,42 +80,42 @@ class AIAnalysisService $provider = $provider ?: env('AI_DEFAULT_PROVIDER', 'ollama'); $job = $candidate->jobPosition; - $jobTitle = $job->title; - $jobDesc = $job->description; - $requirements = implode(", ", $job->requirements ?? []); - - // Static Part: The job context and instructions (Cacheable) - $staticPrompt = "Tu es un expert en recrutement technique spécialisé dans l'infrastructure et la cybersécurité. Ton rôle est d'analyser le profil d'un candidat pour le poste de '{$jobTitle}'."; - if (!$job->ai_prompt) { - $staticPrompt .= " Attache une grande importance aux compétences techniques et à l'expérience, mais aussi à la capacité d'intégration et à la motivation. - - DESCRIPTION DU POSTE: - {$jobDesc} - - COMPÉTENCES REQUISES: - {$requirements} - - 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)"; + // --- BYPASS LOGIC --- + if ($job->ai_bypass_base_prompt && !empty($job->ai_prompt)) { + $staticPrompt = $job->ai_prompt; + // We still append the JSON requirement to ensure the frontend doesn't crash, + // unless the user specifically asked for "pure" takeover. + // Most users want to control the "logic" not the "serialization format". + if (!str_contains(strtolower($staticPrompt), 'json')) { + $staticPrompt .= "\n\nRéponds UNIQUEMENT en JSON pur. Format attendu:\n" . config('ai.defaults.json_format'); + } } else { - $staticPrompt .= " + // --- STANDARD LOGIC --- + // Base instructions from config + $baseInstruction = config('ai.defaults.base_instruction'); + $jsonFormat = config('ai.defaults.json_format'); + + $staticPrompt = "{$baseInstruction} Ton rôle est d'analyser le profil d'un candidat pour le poste de '{$job->title}'.\n\n"; - CONTEXTE DU POSTE: - {$jobDesc} + $staticPrompt .= "DESCRIPTION DU POSTE:\n{$job->description}\n\n"; - COMPÉTENCES REQUISES: - {$requirements} + if (!empty($job->requirements)) { + $staticPrompt .= "COMPÉTENCES REQUISES:\n" . implode(", ", $job->requirements) . "\n\n"; + } + + if (!$job->ai_prompt) { + // Default generalist analysis instructions + $staticPrompt .= "CONSIGNES D'ANALYSE:\n" . config('ai.defaults.analysis_instructions') . "\n\n"; + } else { + // Specific instructions from the job position + $staticPrompt .= "CONSIGNES D'ANALYSE SPÉCIFIQUES:\n" . $job->ai_prompt . "\n\n"; + } - CONSIGNES D'ANALYSE SPÉCIFIQUES: - " . $job->ai_prompt; + $staticPrompt .= "FORMAT DE RÉPONSE ATTENDU:\n{$jsonFormat}\n"; } - $staticPrompt .= "\n\nRéponds UNIQUEMENT en JSON pur, sans texte avant ou après. Assure-toi que le JSON est valide."; + $staticPrompt .= "\nRéponds UNIQUEMENT en JSON pur, sans texte avant ou après. Assure-toi que le JSON est valide."; // Dynamic Part: The candidate data (Not cached) $dynamicPrompt = "CONTENU DU CV DU CANDIDAT:\n{$cvText}\n\nCONTENU DE LA LETTRE DE MOTIVATION:\n" . ($letterText ?? "Non fournie"); diff --git a/config/ai.php b/config/ai.php new file mode 100644 index 0000000..e04d035 --- /dev/null +++ b/config/ai.php @@ -0,0 +1,34 @@ + [ + 'base_instruction' => "Tu es un expert en recrutement expérimenté. Ton rôle est d'analyser le profil d'un candidat avec impartialité et précision.", + + 'analysis_instructions' => "Attache une grande importance à l'adéquation entre le parcours du candidat et les besoins du poste, tant sur le plan technique que sur les savoir-être.", + + 'json_format' => "Fournis une analyse structurée en JSON avec les clés suivantes impérativement: + - match_score: note de 0 à 100 (nombre entier) + - 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)", + ], + + 'providers' => [ + 'default' => env('AI_DEFAULT_PROVIDER', 'ollama'), + 'ollama' => [ + 'url' => env('OLLAMA_URL', 'http://localhost:11434/api/generate'), + 'model' => env('OLLAMA_MODEL', 'mistral'), + ], + // ... + ] +]; diff --git a/database/migrations/2026_04_19_171723_add_ai_bypass_base_prompt_to_job_positions_table.php b/database/migrations/2026_04_19_171723_add_ai_bypass_base_prompt_to_job_positions_table.php new file mode 100644 index 0000000..a841d62 --- /dev/null +++ b/database/migrations/2026_04_19_171723_add_ai_bypass_base_prompt_to_job_positions_table.php @@ -0,0 +1,28 @@ +boolean('ai_bypass_base_prompt')->default(false)->after('ai_prompt'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('job_positions', function (Blueprint $table) { + $table->dropColumn('ai_bypass_base_prompt'); + }); + } +}; diff --git a/resources/js/Pages/Admin/JobPositions/Index.vue b/resources/js/Pages/Admin/JobPositions/Index.vue index 04118a9..2163f27 100644 --- a/resources/js/Pages/Admin/JobPositions/Index.vue +++ b/resources/js/Pages/Admin/JobPositions/Index.vue @@ -22,6 +22,7 @@ const form = useForm({ description: '', requirements: [], ai_prompt: '', + ai_bypass_base_prompt: false, tenant_id: '', quiz_ids: [], }); @@ -33,6 +34,7 @@ const openModal = (position = null) => { form.description = position.description; form.requirements = position.requirements || []; form.ai_prompt = position.ai_prompt || ''; + form.ai_bypass_base_prompt = !!position.ai_bypass_base_prompt; form.tenant_id = position.tenant_id || ''; form.quiz_ids = position.quizzes ? position.quizzes.map(q => q.id) : []; } else { @@ -204,6 +206,18 @@ const removeRequirement = (index) => { placeholder="Ex: Sois particulièrement attentif à l'expérience sur des projets SaaS à forte charge. Favorise les candidats ayant travaillé en environnement Agile." > + +
+ +