Proyectos de Subversion LeadersLinked - Services

Rev

Rev 543 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

<?php

declare(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álido
        if (!$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 criterios
        if (!$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 POST
        return new JsonModel([
            'success' => false,
            'message' => 'Invalid request method'
        ]);
    }

    public function getJobDescriptionCompetencies($jobDescriptionId)
    {
        // 🔹 Validación básica del ID
        if (!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 necesario
            return 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 arreglado
        return $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 para 
                el 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 coloques
                sugerencias 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 retornar
        if (!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ón
        return [
            '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 4122

        return 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/datos
        if (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 array
            if (!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 == 0
            if ((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 > 0
            else {
                $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ón
                if ($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ón
        if ($curlError) {
            error_log("cURL Error: " . $curlError);
            return [false, "cURL Error: " . $curlError];
        }

        // Si la API devuelve un código de error (4xx o 5xx), registrarlo
        if ($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álida
        if (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);
    }
}