Rev 543 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?phpdeclare(strict_types=1);namespace LeadersLinked\Controller;use Smalot\PdfParser\Parser;use Laminas\Mvc\Controller\AbstractActionController;use Laminas\View\Model\ViewModel;use Laminas\View\Model\JsonModel;use Laminas\Db\Adapter\AdapterInterface;use Laminas\Db\Sql\Select;use Laminas\Db\Adapter\Adapter;use Laminas\Db\Sql\Sql;use Laminas\Db\ResultSet\HydratingResultSet;use Laminas\Hydrator\ArraySerializableHydrator;use LeadersLinked\Mapper\CompetencyMapper;use LeadersLinked\Mapper\JobDescriptionMapper;use LeadersLinked\Mapper\JobDescriptionCompetencyMapper;use LeadersLinked\Mapper\LocationMapper;use LeadersLinked\Mapper\QueryMapper;use ArrayObject;use Laminas\Db\Sql\Expression;class RecruitmentCreateJobDescriptionController extends AbstractActionController{/**** @var \Laminas\Db\Adapter\AdapterInterface*/private $adapter;/**** @var \LeadersLinked\Cache\CacheInterface*/private $cache;/**** @var \Laminas\Log\LoggerInterface*/private $logger;/**** @var array*/private $config;/**** @var \Laminas\Mvc\I18n\Translator*/private $translator;/**** @param \Laminas\Db\Adapter\AdapterInterface $adapter* @param \LeadersLinked\Cache\CacheInterface $cache* @param \Laminas\Log\LoggerInterface LoggerInterface $logger* @param array $config* @param \Laminas\Mvc\I18n\Translator $translator*/public function __construct($adapter, $cache, $logger, $config, $translator){$this->adapter = $adapter;$this->cache = $cache;$this->logger = $logger;$this->config = $config;$this->translator = $translator;}public function indexAction(){$request = $this->getRequest();// 🔹 Obtener el ID desde la ruta (URL) con el formato /endpoint/:id$jobDescriptionId = $this->params()->fromRoute('id');// 🔹 Verificar si el ID es válidoif (!$jobDescriptionId) {return new JsonModel(['success' => false,'message' => 'Missing vacancy ID']);}// 🔹 Obtener usuario y permisos ACL$currentUserPlugin = $this->plugin('currentUserPlugin');$currentUser = $currentUserPlugin->getUser();$acl = $this->getEvent()->getViewModel()->getVariable('acl');// 🔹 Verificar si el usuario tiene permiso para extraer criteriosif (!$acl->isAllowed($currentUser->usertype_id, 'recruitment-ai/job-description')) {return new JsonModel(['success' => false,'message' => 'Access denied']);}if ($request->isGet()) {// Llamar a la función que obtiene los archivos de la vacante$data = $this->analyzeJobDescriptionCompetencies($jobDescriptionId);return new JsonModel(['success' => true,'data' => $data]);}// Si el método no es GET ni POSTreturn new JsonModel(['success' => false,'message' => 'Invalid request method']);}public function getJobDescriptionCompetencies($jobDescriptionId){// 🔹 Validación básica del IDif (!is_numeric($jobDescriptionId) || $jobDescriptionId <= 0) {return null;}try {// 🔹 Crear el QueryMapper$queryMapper = QueryMapper::getInstance($this->adapter);$select = $queryMapper->getSql()->select();// 🔹 FROM JobDescriptionMapper$select->from(['jd' => JobDescriptionMapper::_TABLE]);$select->columns(['name','functions','objectives']);// 🔹 JOIN con JobDescriptionCompetencyMapper$select->join(['jdc' => JobDescriptionCompetencyMapper::_TABLE],'jd.id = jdc.job_description_id',[] // No seleccionamos nada directamente de esta tabla);// 🔹 JOIN con CompetencyMapper$select->join(['c' => CompetencyMapper::_TABLE],'jdc.competency_id = c.id',['competency_id' => 'id','competency_name' => 'name','competency_description' => 'description']);// 🔹 WHERE por ID de descripción de trabajo$select->where->equalTo('jd.id', $jobDescriptionId);// 🔹 Ejecutar la consulta$statement = $queryMapper->getSql()->prepareStatementForSqlObject($select);$resultSet = $statement->execute();// 🔹 Procesar resultados$hydrator = new ArraySerializableHydrator();$hydratingResultSet = new HydratingResultSet($hydrator);$hydratingResultSet->initialize($resultSet);// 🔹 Agrupar resultados por competencias$jobInfo = null;$competencies = [];foreach ($hydratingResultSet as $row) {if (!$jobInfo) {$jobInfo = ['name' => $row['name'],'functions' => $row['functions'],'objectives' => $row['objectives']];}$competencies[] = ['id' => $row['competency_id'],'name' => $row['competency_name'],'description' => $row['competency_description']];}if (!$jobInfo) {return null;}// 🔹 Construir el texto formateado$formattedText ="### Información del Trabajo\n" ."Nombre: {$jobInfo['name']}\n" ."Funciones: {$jobInfo['functions']}\n" ."Objetivos: {$jobInfo['objectives']}\n\n" ."### Competencias requeridas:\n";foreach ($competencies as $index => $comp) {$formattedText .= ($index + 1) . ". {$comp['name']} - descripcion: {$comp['description']} - id: {$comp['id']}\n";}return $formattedText;} catch (\Exception $e) {// Aquí podrías loggear el error si es necesarioreturn null;}}function cleanContent($text){// Eliminar la palabra "json"$text = str_replace("json", "", $text);// Eliminar los saltos de línea \n$text = str_replace("\n", "", $text);// Eliminar los acentos invertidos (```)$text = str_replace("```", "", $text);// Retornar el contenido arregladoreturn $text;}public function analyzeJobDescriptionCompetencies($jobDescriptionId){// 🔹 Obtener texto de la descripción de cargo y competencias$descriptionText = $this->getJobDescriptionCompetencies($jobDescriptionId);if (!$descriptionText) {return ['success' => false,'message' => 'No se encontró la descripción del cargo.','data' => null];}// 🔹 Crear el mensaje para OpenAI$messages = [['role' => 'system','content' => "Eres un experto en talento humano, análisis de perfiles laborales y gestión de competencias parael mercado laboral actual."],['role' => 'user','content' => "A continuación te proporciono una descripción de un cargo con sus funciones, objetivos y competencias requeridas.Analiza si las competencias listadas están actualizadas para los requerimientos actuales de la industria y el trabajo moderno.Si es necesario añade nuevas competencias sugeridas para el cargo y cambia la descripción de las que se te proprcionaron en base al puesto (no coloquessugerencias en la descripcion, coloca las funciones de la misma únicamente).Retorna una respuesta estructurada en formato JSON como este:{\"is_updated\": true o false,\"list_competencies\": [{\"has_id\": \"{'bool': true o false, 'nro': 'id', si no tiene 0}\",\"name\": \"nombre de la competencia\",\"description\": \"recomendación o análisis de si está o no actualizada, o si falta complementar\"}]}Texto a analizar:\"\"\"$descriptionText\"\"\""]];// 🔹 Consultar OpenAI$response = $this->analyzeCvWithAi($messages);// 🔹 Validar y retornarif (!isset($response)) {return ['success' => false,'message' => 'Error al consultar la API de OpenAI','data' => $response['message'] ?? null];}// 🔹 Intentar extraer JSON del mensaje generado$reply = $this->cleanContent($response['choices'][0]['message']['content'] ?? '{}');if (!$reply) {return ['success' => false,'message' => 'No se obtuvo una respuesta válida de la IA.','data' => null];}// 🔹 Intentar decodificar respuesta JSON (por si OpenAI responde directamente con JSON)$decoded = json_decode($reply, true);if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {//return $decoded;return $this->insertOrUpdateCompetenciesFromAIResponse($jobDescriptionId, $decoded['list_competencies']);}// 🔹 Si no fue posible decodificar, devolver contenido bruto para revisiónreturn ['success' => false,'message' => 'No se pudo decodificar el JSON de la respuesta','data' => $reply];}public function generateUuid(){$data = random_bytes(16);// Establecer las versiones y variantes de UUID según la RFC 4122$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // versión 4$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // variante RFC 4122return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));}public function insertOrUpdateCompetenciesFromAIResponse($jobDescriptionId, array $aiCompetencies, $competencyTypeId = 1){if (!is_numeric($jobDescriptionId) || $jobDescriptionId <= 0) {return ['success' => false,'message' => 'ID de descripción inválido','data' => []];}// 🔹 Validar que $aiCompetencies sea un array no vacío y de objetos/datosif (empty($aiCompetencies) || !is_array($aiCompetencies) || count($aiCompetencies) === 0) {return ['success' => false,'message' => 'No se proporcionaron competencias para procesar.','data' => []];}$adapter = $this->adapter;$sql = new Sql($adapter);$results = [];foreach ($aiCompetencies as $comp) {// 🔹 Validar que cada elemento sea un arrayif (!is_array($comp)) {continue; // Ignoramos elementos inválidos}// 🔹 Preparar datos$name = trim($comp['name'] ?? '');$description = trim($comp['description'] ?? '');$hasIdRaw = str_replace("'", '"', $comp['has_id'] ?? '{"bool": false, "nro": 0}');$hasIdData = json_decode($hasIdRaw, true);if (!$name || !$description || !is_array($hasIdData)) {continue; // Datos incompletos o corruptos, ignoramos}$competencyId = null;$status = '';// 🔹 Crear nueva competencia si nro == 0if ((int) ($hasIdData['nro'] ?? 0) === 0) {$uuid = $this->generateUuid();$insert = $sql->insert(CompetencyMapper::_TABLE)->values(['uuid' => $uuid,'name' => $name,'description' => $description,'competency_type_id' => $competencyTypeId,'status' => 'a','added_on' => date('Y-m-d H:i:s'),'updated_on' => date('Y-m-d H:i:s')]);$stmt = $sql->prepareStatementForSqlObject($insert)->execute();if ($stmt->getAffectedRows() === 0) {return ['success' => false,'message' => "Error al insertar la competencia '{$name}'",'data' => []];}$competencyId = $adapter->getDriver()->getLastGeneratedValue();$status = 'created';}// 🔹 Actualizar competencia existente si nro > 0else {$competencyId = (int) $hasIdData['nro'];$select = $sql->select(CompetencyMapper::_TABLE)->where(['id' => $competencyId]);$existing = $sql->prepareStatementForSqlObject($select)->execute()->current();if (!$existing) {return ['success' => false,'message' => "ID de competencia {$competencyId} no encontrado",'data' => []];}// Solo actualizar si cambia el nombre o la descripciónif ($existing['name'] !== $name || $existing['description'] !== $description) {$update = $sql->update(CompetencyMapper::_TABLE)->set(['name' => $name,'description' => $description,'updated_on' => date('Y-m-d H:i:s')])->where(['id' => $competencyId]);$stmt = $sql->prepareStatementForSqlObject($update)->execute();if ($stmt->getAffectedRows() === 0) {return ['success' => false,'message' => "Error al actualizar la competencia '{$name}' (ID {$competencyId})",'data' => []];}}$status = 'updated';}// 🔹 Vincular competencia con job description$linkCheck = $sql->select(JobDescriptionCompetencyMapper::_TABLE)->where(['job_description_id' => $jobDescriptionId,'competency_id' => $competencyId]);$existsLink = $sql->prepareStatementForSqlObject($linkCheck)->execute()->current();if (!$existsLink) {$linkInsert = $sql->insert(JobDescriptionCompetencyMapper::_TABLE)->values(['job_description_id' => $jobDescriptionId,'competency_id' => $competencyId]);$stmt = $sql->prepareStatementForSqlObject($linkInsert)->execute();if ($stmt->getAffectedRows() === 0) {return ['success' => false,'message' => "Error al vincular competencia '{$name}' con la descripción {$jobDescriptionId}",'data' => []];}}// 🔹 Guardar resultado exitoso$results[] = ['name' => $name,'competency_id' => $competencyId,'status' => $status];}return ['success' => true,'message' => 'Procesamiento completado exitosamente.','data' => $results];}function callExternalApi($url, $payload, $headers){$ch = curl_init($url);curl_setopt($ch, CURLOPT_CAINFO, '/etc/apache2/ssl/cacert.pem');curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);curl_setopt($ch, CURLOPT_POST, true);curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);$response = curl_exec($ch);$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);$curlError = curl_error($ch);curl_close($ch);// Verificar si hubo un error en la peticiónif ($curlError) {error_log("cURL Error: " . $curlError);return [false, "cURL Error: " . $curlError];}// Si la API devuelve un código de error (4xx o 5xx), registrarloif ($httpCode >= 400) {error_log("Error HTTP {$httpCode}: " . $response);return [false, "Error HTTP {$httpCode}: " . $response];}// Intentar decodificar la respuesta JSON$decodedResponse = json_decode($response, true);// Verificar si la respuesta es válidaif (json_last_error() !== JSON_ERROR_NONE) {error_log("Error al decodificar JSON: " . json_last_error_msg());return [false, "Error al decodificar JSON: " . json_last_error_msg()];}return $decodedResponse;}function analyzeCvWithAi(array $messages){$apiKey = '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';$url = "https://api.openai.com/v1/chat/completions";$payload = json_encode(['model' => 'gpt-4o-mini','messages' => $messages,'temperature' => 0.7]);$headers = ['Content-Type: application/json',"Authorization: Bearer $apiKey"];return $this->callExternalApi($url, $payload, $headers);}}