File manager - Edit - /var/www/ratemypay_dev/app/Services/OldJDWizardAIService.php
Back
<?php namespace App\Services; use App\Models\JobDescription; use App\Models\JobDescriptionSection; use App\Models\JobDescriptionValidationCheck; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use RuntimeException; class JDWizardAIService { // OpenAI Responses API endpoint private const RESPONSES_API = 'https://api.openai.com/v1/responses'; // Prompt IDs private const PROMPT_GENERATE_JD = 'pmpt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; private const PROMPT_REGENERATE_SECTION = 'pmpt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; private const PROMPT_VALIDATE_JD = 'pmpt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; private const PROMPT_SUGGEST_COMPETENCIES = 'pmpt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; private int $timeoutSeconds = 60; /** * Generate all five core JD sections. * * Returns an array keyed by section_type: * ['role_summary' => '...', 'key_responsibilities' => '...', ...] * * @throws RuntimeException */ public function generateJD(JobDescription $jobDescription): array { $payload = $this->buildJDPayload($jobDescription); Log::info('[JDWizardAIService] generateJD called', ['job_description_id' => $jobDescription->id]); $response = $this->callAI(self::PROMPT_GENERATE_JD, $payload); $sections = $this->parseJsonResponse($response); foreach (JobDescriptionSection::SECTION_TYPES as $type) { if (empty($sections[$type])) { throw new RuntimeException("AI response missing section: {$type}"); } } Log::info('[JDWizardAIService] generateJD completed', ['job_description_id' => $jobDescription->id]); return $sections; } /** * Regenerate a single section with an optional user instruction. * * @throws RuntimeException */ public function regenerateSection( JobDescription $jobDescription, string $sectionType, string $instruction = '' ): string { $this->assertValidSectionType($sectionType); $jobDescription->loadMissing('sections'); $payload = $this->buildJDPayload($jobDescription); $payload['target_section'] = $sectionType; $payload['instruction'] = $instruction; $payload['existing_sections'] = $jobDescription->sections ->where('is_core', true) ->mapWithKeys(fn ($s) => [$s->section_type => $s->content]) ->toArray(); Log::info('[JDWizardAIService] regenerateSection called', [ 'job_description_id' => $jobDescription->id, 'section_type' => $sectionType, ]); $content = $this->callAI(self::PROMPT_REGENERATE_SECTION, $payload); Log::info('[JDWizardAIService] regenerateSection completed', [ 'job_description_id' => $jobDescription->id, 'section_type' => $sectionType, ]); return trim($content); } /** * Run validation and alignment checks against the JD. * * The AI must return JSON in this exact shape: * { * "passed": true|false, // overall: true only if ALL checks passed * "score": 84, // overall score 0-100 * "checks": [ * { * "check_key": "inclusive_language", * "section_type": "core_competencies", * "status": "pass"|"fail"|"warning", * "score": 90, * "passed": true, * "message": "Language is inclusive and bias-free." * }, * ... * ], * "suggestions": ["...", "..."] * } * * @throws RuntimeException */ public function validateJD(JobDescription $jobDescription): array { $jobDescription->loadMissing('sections'); $payload = [ 'job_title' => $jobDescription->job_title, 'department' => $jobDescription->department, 'level' => $jobDescription->level, 'employment_type' => $jobDescription->employment_type, 'location' => $jobDescription->location, 'sections' => $jobDescription->sections ->where('is_core', true) ->mapWithKeys(fn ($s) => [$s->section_type => $s->content]) ->toArray(), // Each check must include section_type so the front-end // knows which section to navigate to on "review" 'checks_required' => array_map( fn ($checkKey) => [ 'check_key' => $checkKey, 'section_type' => JobDescriptionSection::sectionTypeForCheck($checkKey), ], JobDescriptionValidationCheck::CHECK_KEYS ), ]; Log::info('[JDWizardAIService] validateJD called', ['job_description_id' => $jobDescription->id]); $response = $this->callAI(self::PROMPT_VALIDATE_JD, $payload); $result = $this->parseJsonResponse($response); // Validate the shape we depend on if (!isset($result['checks']) || !is_array($result['checks'])) { throw new RuntimeException('AI validation response missing "checks" array.'); } Log::info('[JDWizardAIService] validateJD completed', [ 'job_description_id' => $jobDescription->id, 'score' => $result['score'] ?? null, 'passed' => $result['passed'] ?? null, ]); return $result; } /** * Suggest competencies for a given job title / level. * Returns a flat array of competency strings. * * @throws RuntimeException */ public function suggestCompetencies(JobDescription $jobDescription): array { $payload = [ 'job_title' => $jobDescription->job_title, 'department' => $jobDescription->department, 'level' => $jobDescription->level, 'employment_type' => $jobDescription->employment_type, ]; Log::info('[JDWizardAIService] suggestCompetencies called', ['job_description_id' => $jobDescription->id]); $response = $this->callAI(self::PROMPT_SUGGEST_COMPETENCIES, $payload); $parsed = $this->parseJsonResponse($response); $competencies = $parsed['competencies'] ?? []; Log::info('[JDWizardAIService] suggestCompetencies completed', [ 'job_description_id' => $jobDescription->id, 'count' => count($competencies), ]); return $competencies; } // Private Helpers /** * Call the OpenAI Responses API using a stored prompt ID. * Retries up to 3 times with exponential back-off. * * @throws RuntimeException */ private function callAI(string $promptId, array $payload): string { $maxRetries = 3; $lastException = null; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { try { $response = Http::withToken(config('services.openai.key')) ->timeout($this->timeoutSeconds) ->post(self::RESPONSES_API, [ 'prompt' => ['id' => $promptId], 'input' => json_encode($payload), ]); if ($response->successful()) { $content = collect($response->json('output')) ->flatMap(fn ($item) => $item['content'] ?? []) ->firstWhere('type', 'output_text')['text'] ?? null; if (empty($content)) { throw new RuntimeException('AI returned an empty output_text block.'); } return $content; } Log::warning("[JDWizardAIService] HTTP error on attempt {$attempt}", [ 'prompt_id' => $promptId, 'status' => $response->status(), 'body' => $response->body(), ]); throw new RuntimeException( "AI provider returned HTTP {$response->status()}: {$response->body()}" ); } catch (RuntimeException $e) { $lastException = $e; if ($attempt < $maxRetries) { $sleepSeconds = $attempt * 2; Log::info("[JDWizardAIService] Retrying in {$sleepSeconds}s (attempt {$attempt}/{$maxRetries})"); sleep($sleepSeconds); } } } Log::error('[JDWizardAIService] All retry attempts exhausted', [ 'prompt_id' => $promptId, 'error' => $lastException?->getMessage(), ]); throw new RuntimeException( "AI service failed after {$maxRetries} attempts: " . $lastException?->getMessage() ); } /** * Parse a JSON string from the AI, stripping any accidental markdown fences. * * @throws RuntimeException */ private function parseJsonResponse(string $raw): array { $clean = preg_replace('/^```(?:json)?\s*/i', '', trim($raw)); $clean = preg_replace('/\s*```$/', '', $clean); $decoded = json_decode(trim($clean), true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('[JDWizardAIService] JSON parse failure', [ 'raw' => $raw, 'error' => json_last_error_msg(), ]); throw new RuntimeException('Failed to parse AI response as JSON: ' . json_last_error_msg()); } return $decoded; } /** * Common job payload for generate / regenerate calls. */ private function buildJDPayload(JobDescription $jobDescription): array { return [ 'job_title' => $jobDescription->job_title, 'department' => $jobDescription->department, 'level' => $jobDescription->level, 'location' => $jobDescription->location, 'employment_type' => $jobDescription->employment_type, ]; } /** * Guard against unsupported section types. * * @throws RuntimeException */ private function assertValidSectionType(string $type): void { if (!in_array($type, JobDescriptionSection::SECTION_TYPES, true)) { throw new RuntimeException( "Invalid section type: {$type}. Must be one of: " . implode(', ', JobDescriptionSection::SECTION_TYPES) ); } } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.30 | Generation time: 0.13 |
proxy
|
phpinfo
|
Settings