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."
>