Proyectos de Subversion LeadersLinked - Services

Rev

Rev 524 | Rev 526 | Ir a la última revisión | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
512 ariadna 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace LeadersLinked\Controller;
6
 
7
use Smalot\PdfParser\Parser;
8
 
9
use Laminas\Mvc\Controller\AbstractActionController;
10
use Laminas\View\Model\ViewModel;
11
use Laminas\View\Model\JsonModel;
12
use Laminas\Db\Adapter\AdapterInterface;
13
use Laminas\Db\Sql\Select;
14
use Laminas\Db\Adapter\Adapter;
15
use Laminas\Db\Sql\Sql;
16
use Laminas\Db\ResultSet\HydratingResultSet;
17
use Laminas\Hydrator\ArraySerializableHydrator;
516 ariadna 18
use LeadersLinked\Mapper\CompetencyMapper;
512 ariadna 19
use LeadersLinked\Mapper\JobDescriptionMapper;
516 ariadna 20
use LeadersLinked\Mapper\JobDescriptionCompetencyMapper;
512 ariadna 21
use LeadersLinked\Mapper\LocationMapper;
22
use LeadersLinked\Mapper\QueryMapper;
23
use ArrayObject;
24
 
25
class RecruitmentCreateJobDescriptionController extends AbstractActionController
26
{
27
    /**
28
     *
29
     * @var \Laminas\Db\Adapter\AdapterInterface
30
     */
31
    private $adapter;
32
 
33
    /**
34
     *
35
     * @var \LeadersLinked\Cache\CacheInterface
36
     */
37
    private $cache;
38
 
39
 
40
    /**
41
     *
42
     * @var \Laminas\Log\LoggerInterface
43
     */
44
    private $logger;
45
 
46
    /**
47
     *
48
     * @var array
49
     */
50
    private $config;
51
 
52
 
53
    /**
54
     *
55
     * @var \Laminas\Mvc\I18n\Translator
56
     */
57
    private $translator;
58
 
59
 
60
    /**
61
     *
62
     * @param \Laminas\Db\Adapter\AdapterInterface $adapter
63
     * @param \LeadersLinked\Cache\CacheInterface $cache
64
     * @param \Laminas\Log\LoggerInterface LoggerInterface $logger
65
     * @param array $config
66
     * @param \Laminas\Mvc\I18n\Translator $translator
67
     */
68
    public function __construct($adapter, $cache, $logger, $config, $translator)
69
    {
70
        $this->adapter      = $adapter;
71
        $this->cache        = $cache;
72
        $this->logger       = $logger;
73
        $this->config       = $config;
74
        $this->translator   = $translator;
75
    }
76
 
77
    public function indexAction()
78
    {
79
        $request = $this->getRequest();
80
 
81
        // 🔹 Obtener el ID desde la ruta (URL) con el formato /endpoint/:id
516 ariadna 82
        $jobDescriptionId = $this->params()->fromRoute('id');
512 ariadna 83
 
84
        // 🔹 Verificar si el ID es válido
516 ariadna 85
        if (!$jobDescriptionId) {
512 ariadna 86
            return new JsonModel([
87
                'success' => false,
88
                'message' => 'Missing vacancy ID'
89
            ]);
90
        }
91
 
92
        // 🔹 Obtener usuario y permisos ACL
93
        $currentUserPlugin = $this->plugin('currentUserPlugin');
94
        $currentUser = $currentUserPlugin->getUser();
95
        $acl = $this->getEvent()->getViewModel()->getVariable('acl');
96
 
97
        // 🔹 Verificar si el usuario tiene permiso para extraer criterios
516 ariadna 98
        if (!$acl->isAllowed($currentUser->usertype_id, 'recruitment-ai/job-description')) {
512 ariadna 99
            return new JsonModel([
100
                'success' => false,
101
                'message' => 'Access denied'
102
            ]);
103
        }
104
 
105
        if ($request->isGet()) {
106
 
107
            // Llamar a la función que obtiene los archivos de la vacante
523 ariadna 108
            $data = $this->analyzeJobDescriptionCompetencies($jobDescriptionId);
512 ariadna 109
 
110
            return new JsonModel([
111
                'success' => true,
523 ariadna 112
                'data' => $data
512 ariadna 113
            ]);
114
        }
115
 
516 ariadna 116
        // Si el método no es GET ni POST
117
        return new JsonModel([
118
            'success' => false,
119
            'message' => 'Invalid request method'
120
        ]);
121
    }
512 ariadna 122
 
516 ariadna 123
    public function getJobDescriptionCompetencies($jobDescriptionId)
124
    {
125
        // 🔹 Validación básica del ID
126
        if (!is_numeric($jobDescriptionId) || $jobDescriptionId <= 0) {
127
            return null;
128
        }
512 ariadna 129
 
516 ariadna 130
        try {
131
            // 🔹 Crear el QueryMapper
132
            $queryMapper = QueryMapper::getInstance($this->adapter);
133
            $select = $queryMapper->getSql()->select();
512 ariadna 134
 
516 ariadna 135
            // 🔹 FROM JobDescriptionMapper
136
            $select->from(['jd' => JobDescriptionMapper::_TABLE]);
137
            $select->columns([
138
                'name',
139
                'functions',
140
                'objectives'
141
            ]);
142
 
143
            // 🔹 JOIN con JobDescriptionCompetencyMapper
144
            $select->join(
145
                ['jdc' => JobDescriptionCompetencyMapper::_TABLE],
146
                'jd.id = jdc.job_description_id',
147
                [] // No seleccionamos nada directamente de esta tabla
148
            );
149
 
150
            // 🔹 JOIN con CompetencyMapper
151
            $select->join(
152
                ['c' => CompetencyMapper::_TABLE],
153
                'jdc.competency_id = c.id',
154
                [
155
                    'competency_name' => 'name',
156
                    'competency_description' => 'description'
157
                ]
158
            );
159
 
160
            // 🔹 WHERE por ID de descripción de trabajo
161
            $select->where->equalTo('jd.id', $jobDescriptionId);
162
 
163
            // 🔹 Ejecutar la consulta
164
            $statement = $queryMapper->getSql()->prepareStatementForSqlObject($select);
165
            $resultSet = $statement->execute();
166
 
167
            // 🔹 Procesar resultados
168
            $hydrator = new ArraySerializableHydrator();
169
            $hydratingResultSet = new HydratingResultSet($hydrator);
170
            $hydratingResultSet->initialize($resultSet);
171
 
172
            // 🔹 Agrupar resultados por competencias
173
            $jobInfo = null;
174
            $competencies = [];
175
 
176
            foreach ($hydratingResultSet as $row) {
177
                if (!$jobInfo) {
178
                    $jobInfo = [
179
                        'name' => $row['name'],
180
                        'functions' => $row['functions'],
181
                        'objectives' => $row['objectives']
182
                    ];
183
                }
184
 
185
                $competencies[] = [
186
                    'name' => $row['competency_name'],
187
                    'description' => $row['competency_description']
188
                ];
512 ariadna 189
            }
190
 
516 ariadna 191
            if (!$jobInfo) {
192
                return null;
193
            }
512 ariadna 194
 
516 ariadna 195
            // 🔹 Construir el texto formateado
196
            $formattedText =
197
                "### Información del Trabajo\n" .
198
                "Nombre: {$jobInfo['name']}\n" .
199
                "Funciones: {$jobInfo['functions']}\n" .
200
                "Objetivos: {$jobInfo['objectives']}\n\n" .
201
                "### Competencias requeridas:\n";
202
 
203
            foreach ($competencies as $index => $comp) {
204
                $formattedText .= ($index + 1) . ". {$comp['name']} - {$comp['description']}\n";
512 ariadna 205
            }
516 ariadna 206
 
207
            return $formattedText;
208
        } catch (\Exception $e) {
209
            // Aquí podrías loggear el error si es necesario
210
            return null;
512 ariadna 211
        }
212
    }
518 ariadna 213
 
214
    function cleanContent($text)
215
    {
216
        // Eliminar la palabra "json"
217
        $text = str_replace("json", "", $text);
218
 
219
        // Eliminar los saltos de línea \n
220
        $text = str_replace("\n", "", $text);
221
 
222
        // Eliminar los acentos invertidos (```)
223
        $text = str_replace("```", "", $text);
224
 
225
        // Retornar el contenido arreglado
226
        return $text;
227
    }
228
 
229
    public function analyzeJobDescriptionCompetencies($jobDescriptionId)
230
    {
231
        // 🔹 Obtener texto de la descripción de cargo y competencias
232
        $descriptionText = $this->getJobDescriptionCompetencies($jobDescriptionId);
233
 
234
        if (!$descriptionText) {
235
            return [
236
                'success' => false,
237
                'message' => 'No se encontró la descripción del cargo.',
238
                'data' => null
239
            ];
240
        }
241
 
242
        // 🔹 Crear el mensaje para OpenAI
243
        $messages = [
244
            [
245
                'role' => 'system',
246
                'content' => "Eres un experto en talento humano, análisis de perfiles laborales y gestión de competencias para
247
                el mercado laboral actual."
248
            ],
249
            [
250
                'role' => 'user',
251
                'content' => "A continuación te proporciono una descripción de un cargo con sus funciones, objetivos y competencias requeridas.
252
                Analiza si las competencias listadas están actualizadas para los requerimientos actuales de la industria y el trabajo moderno.
525 ariadna 253
                Si es necesario añade nuevas competencias y cambia la descripción de las que se te proprcionaron en base al puesto (no coloques
254
                sugerencias en la descripcion, coloca las funciones de la misma únicamente).
518 ariadna 255
 
256
                Retorna una respuesta estructurada en formato JSON como este:
257
 
258
                {
523 ariadna 259
                \"is_updated\": true o false,
260
                \"list_competencies\": [
518 ariadna 261
                    {
520 ariadna 262
                    \"name\": \"nombre de la competencia\",
263
                    \"description\": \"recomendación o análisis de si está o no actualizada, o si falta complementar\"
518 ariadna 264
                    }
265
                ]
266
                }
267
 
268
                Texto a analizar:
269
                \"\"\"$descriptionText\"\"\"
270
                "
271
            ]
272
        ];
273
 
274
        // 🔹 Consultar OpenAI
275
        $response = $this->analyzeCvWithAi($messages);
276
 
522 ariadna 277
        // 🔹 Validar y retornar
278
        if (!isset($response)) {
279
            return [
280
                'success' => false,
281
                'message' => 'Error al consultar la API de OpenAI',
282
                'data' => $response['message'] ?? null
283
            ];
284
        }
285
 
286
        // 🔹 Intentar extraer JSON del mensaje generado
287
        $reply = $this->cleanContent($response['choices'][0]['message']['content'] ?? '{}');
288
 
289
        if (!$reply) {
290
            return [
291
                'success' => false,
292
                'message' => 'No se obtuvo una respuesta válida de la IA.',
293
                'data' => null
294
            ];
295
        }
296
 
297
        // 🔹 Intentar decodificar respuesta JSON (por si OpenAI responde directamente con JSON)
298
        $decoded = json_decode($reply, true);
299
 
300
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
524 ariadna 301
            return  $decoded;
522 ariadna 302
        }
303
 
304
        // 🔹 Si no fue posible decodificar, devolver contenido bruto para revisión
305
        return [
306
            'success' => false,
307
            'message' => 'No se pudo decodificar el JSON de la respuesta',
308
            'data' => $reply
309
        ];
518 ariadna 310
    }
311
 
312
    function callExternalApi($url, $payload, $headers)
313
    {
314
        $ch = curl_init($url);
315
        curl_setopt($ch, CURLOPT_CAINFO, '/etc/apache2/ssl/cacert.pem');
316
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
317
        curl_setopt($ch, CURLOPT_POST, true);
318
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
319
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
320
 
321
        $response = curl_exec($ch);
322
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
323
        $curlError = curl_error($ch);
324
 
325
        curl_close($ch);
326
 
327
        // Verificar si hubo un error en la petición
328
        if ($curlError) {
329
            error_log("cURL Error: " . $curlError);
330
            return [false, "cURL Error: " . $curlError];
331
        }
332
 
333
        // Si la API devuelve un código de error (4xx o 5xx), registrarlo
334
        if ($httpCode >= 400) {
335
            error_log("Error HTTP {$httpCode}: " . $response);
336
            return [false, "Error HTTP {$httpCode}: " . $response];
337
        }
338
 
339
        // Intentar decodificar la respuesta JSON
340
        $decodedResponse = json_decode($response, true);
341
 
342
        // Verificar si la respuesta es válida
343
        if (json_last_error() !== JSON_ERROR_NONE) {
344
            error_log("Error al decodificar JSON: " . json_last_error_msg());
345
            return [false, "Error al decodificar JSON: " . json_last_error_msg()];
346
        }
347
 
348
        return $decodedResponse;
349
    }
350
 
351
    function analyzeCvWithAi(array $messages)
352
    {
353
        $apiKey = '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';
354
        $url = "https://api.openai.com/v1/chat/completions";
355
 
356
        $payload = json_encode([
357
            'model' => 'gpt-4o-mini',
358
            'messages' => $messages,
359
            'temperature' => 0.7
360
        ]);
361
 
362
        $headers = [
363
            'Content-Type: application/json',
364
            "Authorization: Bearer $apiKey"
365
        ];
366
 
367
        return $this->callExternalApi($url, $payload, $headers);
368
    }
512 ariadna 369
}