Proyectos de Subversion LeadersLinked - Services

Rev

Rev 521 | Rev 523 | Ir a la última revisión | 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;

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
            $competencies = $this->analyzeJobDescriptionCompetencies($jobDescriptionId);

            return new JsonModel([
                'success' => true,
                'data' => $competencies
            ]);
        }

        // 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_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[] = [
                    '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']} - {$comp['description']}\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. 

                Retorna una respuesta estructurada en formato JSON como este:

                {
                \"actualizado\": true o false,
                \"competencias\": [
                    {
                    \"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 [
                'success' => true,
                'message' => 'Análisis completado',
                'data' => $decoded
            ];
        }

        // 🔹 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
        ];
    }

    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);
    }
}