edited rol logic
Browse files- project/cv_matcher.py +72 -55
- project/gemini_api.py +4 -4
- project/offer.txt +33 -0
- project/prompts/prompt_offer.txt +1 -1
- project/static/index.html +11 -2
project/cv_matcher.py
CHANGED
@@ -218,8 +218,8 @@ class CVMatcher:
|
|
218 |
weighted_experience += similarity * exp['years']
|
219 |
|
220 |
# Get min and max experience from offer
|
221 |
-
min_exp = float(offer_dict.get('experience', {}).get('min', 0))
|
222 |
-
max_exp = float(offer_dict.get('experience', {}).get('max',
|
223 |
|
224 |
# Calculate experience percentage (capped at 1.0)
|
225 |
if min_exp > 0:
|
@@ -293,6 +293,27 @@ class CVMatcher:
|
|
293 |
# Get role experience details
|
294 |
min_exp, max_exp, total_exp, exp_score = self.role_experience_similarity(offer_dict, cv_dict)
|
295 |
role = offer_dict.get("role", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
|
297 |
# Get sector information
|
298 |
sector_similarity = self.sector_similarity(offer_dict, cv_dict)
|
@@ -301,88 +322,84 @@ class CVMatcher:
|
|
301 |
|
302 |
# Get education information
|
303 |
education_score = self.education_final_score(offer_dict, cv_dict)
|
304 |
-
min_education
|
305 |
min_education_level = offer_dict.get("education", {}).get("min", "No especificado")
|
306 |
min_education_field = offer_dict.get("education", {}).get("field", "No especificado")
|
307 |
|
308 |
-
#
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
else:
|
327 |
-
|
|
|
|
|
|
|
|
|
328 |
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
result = {
|
337 |
-
# Scores
|
338 |
"technical_skills_score": int(np.round(100 * tech_score, 2)),
|
339 |
"soft_skills_score": int(np.round(100 * soft_score, 2)),
|
340 |
"role_experience_score": int(np.round(100 * exp_score, 2)),
|
341 |
"education_score": int(np.round(100 * education_score, 2)),
|
342 |
"sector_score": int(np.round(100 * sector_similarity, 2)),
|
343 |
|
344 |
-
# Detailed information
|
345 |
"technical_skills": tech_skills,
|
346 |
"soft_skills": soft_skills,
|
347 |
|
348 |
-
# Role experience details
|
349 |
"role_experience": {
|
350 |
-
"explanation":
|
351 |
-
f"The offer is looking for between {min_exp} and {max_exp} years of experience.",
|
352 |
"details": {
|
353 |
-
"role": role,
|
354 |
-
"min_years": min_exp,
|
355 |
-
"max_years": max_exp,
|
356 |
-
"total_experience": round(total_exp, 1)
|
357 |
}
|
358 |
},
|
359 |
|
360 |
-
# Education details
|
361 |
"education": {
|
362 |
-
"explanation":
|
363 |
-
"details":
|
364 |
-
"minimum_required_level": min_education_level,
|
365 |
-
"minimum_required_field": min_education_field,
|
366 |
-
"equivalent_level_cv": same_level_education[0].get('degree', 'Not available') if same_level_education else 'Not available',
|
367 |
-
"equivalent_field_cv": same_level_education[0].get('field', 'Not available') if same_level_education else 'Not available',
|
368 |
-
"higher_education_degrees": [edu.get('degree', '') for edu in higher_education],
|
369 |
-
"higher_education_fields": [edu.get('field', '') for edu in higher_education],
|
370 |
-
"meets_requirement": education_score >= 0.5
|
371 |
-
}
|
372 |
},
|
373 |
|
374 |
-
# Sector information
|
375 |
"sector": {
|
376 |
-
"explanation": f"The offer's sector is '{offer_sector}' and your main sector is '{cv_sector}'. "
|
377 |
f"The similarity between both sectors is {round(sector_similarity * 100, 1)}%.",
|
378 |
"details": {
|
379 |
-
"offer_sector": offer_sector,
|
380 |
-
"cv_sector": cv_sector,
|
381 |
-
"similarity": round(sector_similarity * 100, 1)
|
382 |
}
|
383 |
}
|
384 |
}
|
385 |
-
|
386 |
|
387 |
return result
|
388 |
|
|
|
218 |
weighted_experience += similarity * exp['years']
|
219 |
|
220 |
# Get min and max experience from offer
|
221 |
+
min_exp = float(offer_dict.get('experience', {}).get('min', 0.0))
|
222 |
+
max_exp = float(offer_dict.get('experience', {}).get('max', 9999.0)) # Default range if max not specified
|
223 |
|
224 |
# Calculate experience percentage (capped at 1.0)
|
225 |
if min_exp > 0:
|
|
|
293 |
# Get role experience details
|
294 |
min_exp, max_exp, total_exp, exp_score = self.role_experience_similarity(offer_dict, cv_dict)
|
295 |
role = offer_dict.get("role", "")
|
296 |
+
|
297 |
+
# --- NUEVA LÓGICA CLARA Y ROBUSTA PARA EL TEXTO DE EXPERIENCIA ---
|
298 |
+
min_exp_raw = offer_dict.get('experience', {}).get('min', 0)
|
299 |
+
max_exp_raw = offer_dict.get('experience', {}).get('max', 9999.0)
|
300 |
+
|
301 |
+
# Convert to float for consistent comparison
|
302 |
+
min_exp = float(min_exp_raw) if min_exp_raw is not None else 0
|
303 |
+
max_exp = float(max_exp_raw) if max_exp_raw is not None else 9999.0
|
304 |
+
|
305 |
+
experience_requirement_text = ""
|
306 |
+
# Caso 1: No se especifica experiencia mínima o es 0.
|
307 |
+
if min_exp == 0:
|
308 |
+
experience_requirement_text = "There's not any experience required for this role."
|
309 |
+
# Caso 2: Se especifica un mínimo pero no un máximo (o máximo muy alto).
|
310 |
+
elif max_exp >= 9999.0:
|
311 |
+
experience_requirement_text = f"The offer is looking for someone with more than {int(min_exp)} years of experience."
|
312 |
+
# Caso 3: Se especifican ambos, mínimo y máximo.
|
313 |
+
else:
|
314 |
+
experience_requirement_text = f"The offer is looking for between {int(min_exp)} and {int(max_exp)} years of experience."
|
315 |
+
|
316 |
+
full_explanation = f"You have approximately {round(total_exp, 1)} years of experience in roles similar to '{role}'. {experience_requirement_text}"
|
317 |
|
318 |
# Get sector information
|
319 |
sector_similarity = self.sector_similarity(offer_dict, cv_dict)
|
|
|
322 |
|
323 |
# Get education information
|
324 |
education_score = self.education_final_score(offer_dict, cv_dict)
|
325 |
+
min_education = float(offer_dict.get("education", {}).get("number", 0))
|
326 |
min_education_level = offer_dict.get("education", {}).get("min", "No especificado")
|
327 |
min_education_field = offer_dict.get("education", {}).get("field", "No especificado")
|
328 |
|
329 |
+
# Get candidate's education and find the highest degree
|
330 |
+
cv_education_list = cv_dict.get("education", [])
|
331 |
+
highest_cv_degree = None
|
332 |
+
if cv_education_list:
|
333 |
+
sorted_cv_education = sorted(cv_education_list, key=lambda x: float(x.get('number', 0)), reverse=True)
|
334 |
+
highest_cv_degree = sorted_cv_education[0]
|
335 |
+
|
336 |
+
education_details = {}
|
337 |
+
education_explanation = ""
|
338 |
+
|
339 |
+
# SCENARIO 1: The offer does NOT specify a minimum education level
|
340 |
+
if min_education == 0:
|
341 |
+
education_explanation = "The offer does not specify a minimum education level. The candidate's highest degree is shown for reference."
|
342 |
+
education_details = {
|
343 |
+
"minimum_required_level": "Not specified",
|
344 |
+
"minimum_required_field": min_education_field if min_education_field != "No especificado" else "Not specified",
|
345 |
+
# USAMOS LAS CLAVES ORIGINALES para no romper el HTML
|
346 |
+
"equivalent_level_cv": highest_cv_degree.get('degree', 'Not available') if highest_cv_degree else 'Not available',
|
347 |
+
"equivalent_field_cv": highest_cv_degree.get('field', 'Not available') if highest_cv_degree else 'Not available',
|
348 |
+
# Devolvemos una lista vacía, el JS lo mostrará como 'None'
|
349 |
+
"higher_education_degrees": [],
|
350 |
+
"meets_requirement": True
|
351 |
+
}
|
352 |
+
# SCENARIO 2: The offer DOES specify a minimum education level
|
353 |
else:
|
354 |
+
same_level_education = [edu for edu in cv_education_list if float(edu.get('number', 0)) == min_education]
|
355 |
+
higher_education = [edu for edu in cv_education_list if float(edu.get('number', 0)) > min_education]
|
356 |
+
|
357 |
+
match_text = "The candidate meets the minimum requirement." if same_level_education or higher_education else "The candidate does not meet the minimum requirement."
|
358 |
+
education_explanation = f"The offer requires at least {min_education_level}. {match_text}"
|
359 |
|
360 |
+
# Find the most relevant degree to show as "equivalent"
|
361 |
+
equivalent_education = same_level_education[0] if same_level_education else (highest_cv_degree if higher_education else {})
|
362 |
+
|
363 |
+
education_details = {
|
364 |
+
"minimum_required_level": min_education_level,
|
365 |
+
"minimum_required_field": min_education_field,
|
366 |
+
"equivalent_level_cv": equivalent_education.get('degree', 'Not available'),
|
367 |
+
"equivalent_field_cv": equivalent_education.get('field', 'Not available'),
|
368 |
+
"higher_education_degrees": [edu.get('degree', '') for edu in higher_education],
|
369 |
+
"meets_requirement": education_score >= 0.5
|
370 |
+
}
|
371 |
+
|
372 |
+
# Format the final return dictionary with all the processed information
|
373 |
result = {
|
|
|
374 |
"technical_skills_score": int(np.round(100 * tech_score, 2)),
|
375 |
"soft_skills_score": int(np.round(100 * soft_score, 2)),
|
376 |
"role_experience_score": int(np.round(100 * exp_score, 2)),
|
377 |
"education_score": int(np.round(100 * education_score, 2)),
|
378 |
"sector_score": int(np.round(100 * sector_similarity, 2)),
|
379 |
|
|
|
380 |
"technical_skills": tech_skills,
|
381 |
"soft_skills": soft_skills,
|
382 |
|
|
|
383 |
"role_experience": {
|
384 |
+
"explanation": full_explanation, # Usamos la variable que acabamos de crear
|
|
|
385 |
"details": {
|
386 |
+
"role": role, "min_years": min_exp, "max_years": max_exp, "total_experience": round(total_exp, 1)
|
|
|
|
|
|
|
387 |
}
|
388 |
},
|
389 |
|
|
|
390 |
"education": {
|
391 |
+
"explanation": education_explanation,
|
392 |
+
"details": education_details
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
393 |
},
|
394 |
|
|
|
395 |
"sector": {
|
396 |
+
"explanation": f"The offer's sector is '{offer_sector}' and your main sector is '{' and '.join(cv_sector) if isinstance(cv_sector, list) else cv_sector}'. "
|
397 |
f"The similarity between both sectors is {round(sector_similarity * 100, 1)}%.",
|
398 |
"details": {
|
399 |
+
"offer_sector": offer_sector, "cv_sector": ' and '.join(cv_sector) if isinstance(cv_sector, list) else cv_sector, "similarity": round(sector_similarity * 100, 1)
|
|
|
|
|
400 |
}
|
401 |
}
|
402 |
}
|
|
|
403 |
|
404 |
return result
|
405 |
|
project/gemini_api.py
CHANGED
@@ -64,10 +64,10 @@ def read_cv(file_path: str) -> str:
|
|
64 |
for b in blocks_sorted:
|
65 |
cv += b[4]
|
66 |
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
|
72 |
return cv
|
73 |
|
|
|
64 |
for b in blocks_sorted:
|
65 |
cv += b[4]
|
66 |
|
67 |
+
if len(cv) > 10000:
|
68 |
+
return -1
|
69 |
+
elif len(cv) < 10:
|
70 |
+
return -2
|
71 |
|
72 |
return cv
|
73 |
|
project/offer.txt
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
En PwC te ofrecemos la posibilidad de iniciar tu carrera profesional trabajando desde tu incorporación para clientes de primer nivel (tanto nacionales como internacionales). Formarás parte de un equipo de trabajo con gran variedad de perfiles profesionales que potenciarán tu aprendizaje.
|
2 |
+
|
3 |
+
Apostamos por jóvenes sin experiencia, con pasión por el mundo de la Consultoría, que quieran desarrollar su carrera profesional con nosotros. Apostamos por ti y tus capacidades para formar parte de un equipo de personas con gran talento.
|
4 |
+
|
5 |
+
Tendrás un completo programa de formación adaptado a tu día a día. A través de La Academia de PwC, nuestra Universidad Corporativa interna, podrás acceder a un amplio catálogo de formaciones técnicas, sectoriales y de habilidades.
|
6 |
+
|
7 |
+
En nuestra línea de Consultoría de Negocio, ayudamos a nuestros clientes a optimizar sus procesos de negocio y a incrementar sus resultados. Ofrecemos servicios de consultoría y asesoramiento empresarial en los ámbitos de Función Financiera, Supply Chain Management, Customer, Consultoría de IT, Data&Analytics y People&Organization.
|
8 |
+
|
9 |
+
Las competencias más valoradas entre los profesionales Junior de PwC son:
|
10 |
+
|
11 |
+
Capacidad de trabajo en equipo.
|
12 |
+
Capacidad de análisis de problemas.
|
13 |
+
Capacidad de aprendizaje
|
14 |
+
Capacidad de comunicación
|
15 |
+
|
16 |
+
Construir relaciones y crear valor forman parte nuestro ADN. ¡Te esperamos!
|
17 |
+
|
18 |
+
Requisitos:
|
19 |
+
|
20 |
+
Buscamos estudiantes de último curso de Grado/Postgrado o perfiles de recién graduados, abarcando un amplio abanico de titulaciones:
|
21 |
+
|
22 |
+
Ingenierías (Industrial, Aeronáutica, Informática, Telecomunicaciones, etc.).
|
23 |
+
Dobles titulaciones ADE + Ingeniería, ADE + Derecho (o similares).
|
24 |
+
Grado en Matemáticas, Estadística, Física.
|
25 |
+
|
26 |
+
Es IMPRESCINDIBLE tener la titulación finalizada (incluyendo el Proyecto Fin de Grado o Máster) para poder incorporarse en agosto/septiembre de 2025. No obstante, ten en cuenta que puedes participar en nuestro proceso de selección sin haber finalizado los estudios. El proceso estará abierto de octubre de 2024 a julio de 2025, aunque te recomendamos que no demores tu participación.
|
27 |
+
|
28 |
+
Otros requisitos:
|
29 |
+
|
30 |
+
Nivel de inglés alto oral y escrito (se evaluará durante el proceso de selección)
|
31 |
+
Valoraremos positivamente un buen expediente académico y carrera cursada según cronología del plan de estudios.
|
32 |
+
Valoraremos prácticas en empresa, estancias internacionales (Erasmus) y conocimiento de otros idiomas.
|
33 |
+
No requerimos experiencia, pero sí ganas de aprender.
|
project/prompts/prompt_offer.txt
CHANGED
@@ -10,7 +10,7 @@ Tareas:
|
|
10 |
3. Extrae todas las habilidades técnicas relevantes.
|
11 |
4. Extrae la experiencia mínima / máxima requerida
|
12 |
- si no hay mínima, establecer la mínima como 0
|
13 |
-
- si no hay máxima, establecer la máxima
|
14 |
5. Extrae la educación mínima requerida y en caso de que haya varias opciones, coger la mínima siguiendo el orden:
|
15 |
|
16 |
1. Educación secundaria/bachillerato
|
|
|
10 |
3. Extrae todas las habilidades técnicas relevantes.
|
11 |
4. Extrae la experiencia mínima / máxima requerida
|
12 |
- si no hay mínima, establecer la mínima como 0
|
13 |
+
- si no hay máxima, establecer la máxima 9999
|
14 |
5. Extrae la educación mínima requerida y en caso de que haya varias opciones, coger la mínima siguiendo el orden:
|
15 |
|
16 |
1. Educación secundaria/bachillerato
|
project/static/index.html
CHANGED
@@ -1303,12 +1303,21 @@
|
|
1303 |
|
1304 |
const data = await response.json();
|
1305 |
console.log('Received data:', data);
|
1306 |
-
|
|
|
1307 |
if (data.error) {
|
1308 |
throw new Error(data.error);
|
1309 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1310 |
|
1311 |
-
//
|
1312 |
setTimeout(() => {
|
1313 |
displayResults(data);
|
1314 |
showNotification('Analysis completed successfully!', 'success');
|
|
|
1303 |
|
1304 |
const data = await response.json();
|
1305 |
console.log('Received data:', data);
|
1306 |
+
|
1307 |
+
// Esta parte ya maneja los errores que envía explícitamente el backend
|
1308 |
if (data.error) {
|
1309 |
throw new Error(data.error);
|
1310 |
}
|
1311 |
+
|
1312 |
+
// --- AÑADE ESTA NUEVA COMPROBACIÓN AQUÍ ---
|
1313 |
+
// Si el objeto de respuesta no contiene las puntuaciones clave,
|
1314 |
+
// asumimos que el PDF no se pudo procesar correctamente.
|
1315 |
+
if (data.technical_skills_score === undefined || data.technical_skills_score === null) {
|
1316 |
+
throw new Error("The PDF could not be read correctly or is empty. Please check the file.");
|
1317 |
+
}
|
1318 |
+
// --- FIN DE LA NUEVA COMPROBACIÓN ---
|
1319 |
|
1320 |
+
// Si todo está bien, mostramos los resultados
|
1321 |
setTimeout(() => {
|
1322 |
displayResults(data);
|
1323 |
showNotification('Analysis completed successfully!', 'success');
|