{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. Preparación del notebook" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n" ] } ], "source": [ "import os\n", "import pandas as pd\n", "import json\n", "import textwrap\n", "from scipy import spatial\n", "from datetime import datetime\n", "from openai import OpenAI\n", "from dotenv import load_dotenv\n", "\n", "from IPython.display import display # Sólo para la ejecución en Jupyter\n", "\n", "load_dotenv(\"../../../../../../../apis/.env\")\n", "api_key = os.getenv(\"OPENAI_API_KEY\")\n", "unmasked_chars = 8\n", "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n", "print(f\"API key: {masked_key}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Funciones de procesamiento de datos y cálculo de puntuación" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class ProcesadorCV:\n", "\n", " def __init__(self, api_key, cv_text, job_text, ner_pre_prompt, ner_schema,\n", " inference_model=\"gpt-4o-mini\", embeddings_model=\"text-embedding-3-small\"):\n", " \"\"\"\n", " Inicializa una instancia de la clase con los parámetros proporcionados.\n", "\n", " Args:\n", " api_key (str): La clave de API para autenticar con el cliente OpenAI.\n", " cv_text (str): contenido del CV en formato de texto.\n", " job_text (str): título de la oferta de trabajo a evaluar.\n", " ner_pre_prompt (str): instrucción de \"reconocimiento de entidades nombradas\" (NER) para el modelo en lenguaje natural.\n", " ner_schema (dict): esquema para la llamada con \"structured outputs\" al modelo de OpenAI.\n", " inference_model (str, opcional): El modelo de inferencia a utilizar. Por defecto es \"gpt-4o-mini\".\n", " embeddings_model (str, opcional): El modelo de embeddings a utilizar. Por defecto es \"text-embedding-3-small\".\n", "\n", " Atributos:\n", " inference_model (str): Almacena el modelo de inferencia seleccionado.\n", " embeddings_model (str): Almacena el modelo de embeddings seleccionado.\n", " client (OpenAI): Instancia del cliente OpenAI inicializada con la clave de API proporcionada.\n", " cv (str): Almacena el texto del currículum vitae proporcionado.\n", "\n", " \"\"\"\n", " self.inference_model = inference_model\n", " self.embeddings_model = embeddings_model\n", " self.ner_pre_prompt = ner_pre_prompt\n", " self.ner_schema = ner_schema\n", " self.client = OpenAI(api_key=api_key)\n", " self.cv = cv_text\n", " self.job_text = job_text\n", " print(\"Cliente inicializado como\",self.client)\n", "\n", " def extraer_datos_cv(self, temperature=0.5):\n", " \"\"\"\n", " Extrae datos estructurados de un CV con OpenAI API.\n", " Args:\n", " pre_prompt (str): instrucción para el modelo en lenguaje natural.\n", " schema (dict): esquema de los parámetros que se espera extraer del CV.\n", " temperature (float, optional): valor de temperatura para el modelo de lenguaje. Por defecto es 0.5.\n", " Returns:\n", " pd.DataFrame: DataFrame con los datos estructurados extraídos del CV.\n", " Raises:\n", " ValueError: si no se pueden extraer datos estructurados del CV.\n", " \"\"\"\n", " response = self.client.chat.completions.create(\n", " model=self.inference_model,\n", " temperature=temperature,\n", " messages=[\n", " {\"role\": \"system\", \"content\": self.ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": self.cv}\n", " ],\n", " functions=[\n", " {\n", " \"name\": \"extraer_datos_cv\",\n", " \"description\": \"Extrae tabla con títulos de puesto de trabajo, nombres de empresa y períodos de un CV.\",\n", " \"parameters\": self.ner_schema\n", " }\n", " ],\n", " function_call=\"auto\"\n", " )\n", "\n", " if response.choices[0].message.function_call:\n", " function_call = response.choices[0].message.function_call\n", " structured_output = json.loads(function_call.arguments)\n", " if structured_output.get(\"experiencia\"):\n", " df_cv = pd.DataFrame(structured_output[\"experiencia\"]) \n", " return df_cv\n", " else:\n", " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n", " else:\n", " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n", " \n", "\n", " def procesar_periodos(self, df): \n", " \"\"\"\n", " Procesa los períodos en un DataFrame y añade columnas con las fechas de inicio, fin y duración en meses. \n", " Si no hay fecha de fin, se considera la fecha actual.\n", " Args:\n", " df (pandas.DataFrame): DataFrame que contiene una columna 'periodo' con períodos en formato 'YYYYMM-YYYYMM' o 'YYYYMM'.\n", " Returns:\n", " pandas.DataFrame: DataFrame con columnas adicionales 'fec_inicio', 'fec_final' y 'duracion'.\n", " - 'fec_inicio' (datetime.date): Fecha de inicio del período.\n", " - 'fec_final' (datetime.date): Fecha de fin del período.\n", " - 'duracion' (int): Duración del período en meses.\n", " \"\"\"\n", " # Función lambda para procesar el período\n", " def split_periodo(periodo):\n", " dates = periodo.split('-')\n", " start_date = datetime.strptime(dates[0], \"%Y%m\")\n", " if len(dates) > 1:\n", " end_date = datetime.strptime(dates[1], \"%Y%m\")\n", " else:\n", " end_date = datetime.now()\n", " return start_date, end_date\n", "\n", " df[['fec_inicio', 'fec_final']] = df['periodo'].apply(lambda x: pd.Series(split_periodo(x)))\n", "\n", " # Formateamos las fechas para mostrar mes, año, y el primer día del mes (dado que el día es irrelevante y no se suele especificar)\n", " df['fec_inicio'] = df['fec_inicio'].dt.date\n", " df['fec_final'] = df['fec_final'].dt.date\n", "\n", " # Añadimos una columna con la duración en meses\n", " df['duracion'] = df.apply(\n", " lambda row: (row['fec_final'].year - row['fec_inicio'].year) * 12 + \n", " row['fec_final'].month - row['fec_inicio'].month, \n", " axis=1\n", " )\n", "\n", " return df\n", "\n", "\n", " def calcular_embeddings(self, df, column='puesto', model_name='text-embedding-3-small'):\n", " \"\"\"\n", " Calcula los embeddings de una columna de un dataframe con OpenAI API.\n", " Args:\n", " cv_df (pandas.DataFrame): DataFrame con los datos de los CV.\n", " column (str, optional): Nombre de la columna que contiene los datos a convertir en embeddings. Por defecto es 'puesto'.\n", " model_name (str, optional): Nombre del modelo de embeddings. Por defecto es 'text-embedding-3-small'.\n", " \"\"\"\n", " df['embeddings'] = df[column].apply(\n", " lambda puesto: self.client.embeddings.create(\n", " input=puesto, \n", " model=model_name\n", " ).data[0].embedding\n", " )\n", " return df\n", "\n", "\n", " def calcular_distancias(self, df, column='embeddings', model_name='text-embedding-3-small'):\n", " \"\"\"\n", " Calcula la distancia coseno entre los embeddings del texto y los incluidos en una columna del dataframe.\n", " Params:\n", " df (pandas.DataFrame): DataFrame que contiene los embeddings.\n", " column (str, optional): nombre de la columna del DataFrame que contiene los embeddings. Por defecto, 'embeddings'.\n", " model_name (str, optional): modelo de embeddings de la API de OpenAI. Por defecto \"text-embedding-3-small\".\n", " Returns:\n", " pandas.DataFrame: DataFrame ordenado de menor a mayor distancia, con las distancias en una nueva columna.\n", " \"\"\"\n", " response = self.client.embeddings.create(\n", " input=self.job_text,\n", " model=model_name\n", " )\n", " emb_compare = response.data[0].embedding\n", "\n", " df['distancia'] = df[column].apply(lambda emb: spatial.distance.cosine(emb, emb_compare))\n", " df.drop(columns=[column], inplace=True)\n", " df.sort_values(by='distancia', ascending=True, inplace=True)\n", " return df\n", "\n", "\n", " def calcular_puntuacion(self, df, req_experience, positions_cap=4, dist_threshold_low=0.6, dist_threshold_high=0.7):\n", " \"\"\"\n", " Calcula la puntuación de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n", "\n", " Params:\n", " df (pandas.DataFrame): datos de un CV incluyendo diferentes experiencias incluyendo duracies y distancia previamente calculadas sobre los embeddings de un puesto de trabajo\n", " req_experience (float): experiencia requerida en meses para el puesto de trabajo (valor de referencia para calcular una puntuación entre 0 y 100 en base a diferentes experiencias)\n", " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n", " dist_threshold_low (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV se considera \"equivalente\" al de la oferta.\n", " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no puntúa.\n", " \n", " Returns:\n", " pandas.DataFrame: DataFrame original añadiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n", " float: Puntuación total entre 0 y 100.\n", " \"\"\"\n", " # A efectos de puntuación, computamos para cada puesto como máximo el número total de meses de experiencia requeridos\n", " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n", " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia mínima y 1 la máxima\n", " df['adjusted_distance'] = df['distancia'].apply(\n", " lambda x: 0 if x <= dist_threshold_low else (\n", " 1 if x >= dist_threshold_high else (x - dist_threshold_low) / (dist_threshold_high - dist_threshold_low)\n", " )\n", " )\n", " # Cada puesto puntúa en base a su duración y a la inversa de la distancia (a menor distancia, mayor puntuación)\n", " df['position_score'] = round(((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100), 2)\n", " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuación 0), y ordenamos por puntuación\n", " df.loc[df['distancia'] >= dist_threshold_high, 'position_score'] = 0\n", " df = df.sort_values(by='position_score', ascending=False)\n", " # Nos quedamos con los puestos con mayor puntuación (positions_cap)\n", " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n", " # Totalizamos (no debería superar 100 nunca, pero ponemos un límite para asegurar) y redondeamos a dos decimales\n", " total_score = round(min(df['position_score'].sum(), 100), 2)\n", " return df, total_score\n", " \n", " def filtra_experiencia_relevante(self, df):\n", " \"\"\"\n", " Filtra las experiencias relevantes del dataframe y las devuelve en formato diccionario.\n", " Args:\n", " df (pandas.DataFrame): DataFrame con la información completa de experiencia.\n", " Returns:\n", " dict: Diccionario con las experiencias relevantes.\n", " \"\"\"\n", " df_experiencia = df[df['position_score'] > 0].copy()\n", " df_experiencia.drop(columns=['periodo', 'fec_inicio', 'fec_final', \n", " 'distancia', 'duration_capped', 'adjusted_distance'], inplace=True)\n", " experiencia_dict = df_experiencia.to_dict(orient='list')\n", " return experiencia_dict\n", " \n", " def procesar_cv_completo(self, req_experience, positions_cap, dist_threshold_low, dist_threshold_high):\n", " \"\"\"\n", " Procesa un CV y calcula la puntuación final.\n", " Args:\n", " req_experience (int, optional): Experiencia requerida en meses para el puesto de trabajo.\n", " positions_cap (int, optional): Número máximo de puestos a considerar para la puntuación.\n", " dist_threshold_low (float, optional): Distancia límite para considerar un puesto equivalente.\n", " dist_threshold_high (float, optional): Distancia límite para considerar un puesto no relevante.\n", " Returns:\n", " pd.DataFrame: DataFrame con las puntuaciones individuales contribuidas por cada puesto.\n", " float: Puntuación total entre 0 y 100.\n", " \"\"\"\n", " df_datos_estructurados_cv = self.extraer_datos_cv()\n", " df_datos_estructurados_cv = self.procesar_periodos(df_datos_estructurados_cv)\n", " df_con_embeddings = self.calcular_embeddings(df_datos_estructurados_cv)\n", " df_con_distancias = self.calcular_distancias(df_con_embeddings)\n", " df_puntuaciones, puntuacion = self.calcular_puntuacion(df_con_distancias,\n", " req_experience=req_experience,\n", " positions_cap=positions_cap,\n", " dist_threshold_low=dist_threshold_low,\n", " dist_threshold_high=dist_threshold_high)\n", " dict_experiencia = self.filtra_experiencia_relevante(df_puntuaciones)\n", " return df_puntuaciones, puntuacion, dict_experiencia" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Proceso completo de cálculo de puntuación" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "En el siguiente bloque, podemos introducir cualquier texto de oferta, un CV, y obtener las puntuaciones y el DataFrame con los cálculos:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cliente inicializado como \n", "Puntuación: 89.0/100\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduraciondistanciaduration_cappedadjusted_distanceposition_score
1MercadonaVendedor/a de puesto de mercado202310-2024032023-10-012024-03-0150.5650.0041.67
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-0160.5960.1840.87
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-01480.62120.946.47
0AutónomoComercial de automoviles202401-2024022024-01-012024-02-0110.6311.000.00
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-0160.6561.000.00
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-01290.70121.000.00
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "1 Mercadona Vendedor/a de puesto de mercado \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "0 Autónomo Comercial de automoviles \n", "5 Bellota Herramientas Personal de mantenimiento \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "\n", " periodo fec_inicio fec_final duracion distancia \\\n", "1 202310-202403 2023-10-01 2024-03-01 5 0.56 \n", "3 202303-202309 2023-03-01 2023-09-01 6 0.59 \n", "2 202001-202401 2020-01-01 2024-01-01 48 0.62 \n", "0 202401-202402 2024-01-01 2024-02-01 1 0.63 \n", "5 202005-202011 2020-05-01 2020-11-01 6 0.65 \n", "4 202012-202305 2020-12-01 2023-05-01 29 0.70 \n", "\n", " duration_capped adjusted_distance position_score \n", "1 5 0.00 41.67 \n", "3 6 0.18 40.87 \n", "2 12 0.94 6.47 \n", "0 1 1.00 0.00 \n", "5 6 1.00 0.00 \n", "4 12 1.00 0.00 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "{'empresa': ['Mercadona', 'GASTROTEKA ORDIZIA 1990', 'AGRISOLUTIONS'], 'puesto': ['Vendedor/a de puesto de mercado', 'Camarero/a de barra', 'AUXILIAR DE MANTENIMIENTO INDUSTRIAL'], 'duracion': [5, 6, 48], 'position_score': [41.67, 40.87, 6.47]}\n" ] } ], "source": [ "# Definimos la oferta de trabajo:\n", "job_text = \"Cajero supermercado Dia\"\n", "\n", "# Cargamos el esquema:\n", "with open('../json/ner_schema.json', 'r', encoding='utf-8') as schema_file:\n", " ner_schema = json.load(schema_file)\n", "\n", "# Cargamos el CV:\n", "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un currículo de ejemplo\n", "with open(cv_sample_path, 'r') as file:\n", " cv_text = file.read()\n", "\n", "# Cargamos el prompt para NER:\n", "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as file:\n", " ner_pre_prompt = file.read()\n", "\n", "procesador_cvs = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, ner_schema)\n", "req_experience = 12 # Experiencia requerida en meses\n", "positions_cap=4 # Número máximo de puestos a considerar\n", "dist_threshold_low=0.58 # Distancia límite para considerar un puesto equivalente\n", "dist_threshold_high=0.62 # Distancia límite para considerar un puesto no relevante\n", "df_puntuaciones, puntuacion, dict_experiencia = procesador_cvs.procesar_cv_completo(req_experience=req_experience,\n", " positions_cap=positions_cap,\n", " dist_threshold_low=dist_threshold_low,\n", " dist_threshold_high=dist_threshold_high\n", " )\n", "\n", "print(f\"Puntuación: {puntuacion:.1f}/100\")\n", "pd.options.display.float_format = '{:,.2f}'.format\n", "display(df_puntuaciones)\n", "pd.reset_option('display.float_format')\n", "print(dict_experiencia)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Llamada final al modelo de lenguaje" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cliente inicializado como \n" ] } ], "source": [ "client = OpenAI(api_key=api_key)\n", "print(\"Cliente inicializado como\",client)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Definimos un esquema para la respuesta final:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "response_schema = {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"puntuacion\": {\"type\": \"number\"},\n", " \"experiencia\": {\n", " \"type\": \"array\",\n", " \"items\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"empresa\": {\"type\": \"string\"},\n", " \"puesto\": {\"type\": \"string\"},\n", " \"duracion\": {\"type\": \"integer\"}\n", " },\n", " \"required\": [\"empresa\", \"puesto\", \"duracion\"]\n", " }\n", " },\n", " \"descripcion de la experiencia\": {\"type\": \"string\"}\n", " },\n", " \"required\": [\"puntuacion\", \"experiencia relevante\", \"descripcion de la experiencia\"]\n", "}\n", "\n", "# Guardamos el esquema en un fichero JSON\n", "with open('../json/response_schema.json', 'w', encoding='utf-8') as f:\n", " json.dump(response_schema, f, ensure_ascii=False, indent=4)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'type': 'object', 'properties': {'puntuacion': {'type': 'number'}, 'experiencia': {'type': 'array', 'items': {'type': 'object', 'properties': {'empresa': {'type': 'string'}, 'puesto': {'type': 'string'}, 'duracion': {'type': 'integer'}}, 'required': ['empresa', 'puesto', 'duracion']}}, 'descripcion de la experiencia': {'type': 'string'}}, 'required': ['puntuacion', 'experiencia relevante', 'descripcion de la experiencia']}\n" ] } ], "source": [ "# Recuperamos el esquema desde el fichero JSON guardado (para comprobar que funciona, ya que el código final utilizará el fichero)\n", "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n", " response_schema = json.load(f)\n", "\n", "print(response_schema)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creamos un \"system prompt\" (instrucción general) y un \"user prompt\" (instrucción con contexto específico: puntuación y datos estructurados) para la inferencia final:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "system_prompt = (\"Eres un procesador de currículos vitae que recibe una oferta de trabajo un currículum vitae filtrado \"\n", " \"la experiencia relevante previa, una puntuación precalculada para el currículo entre 0 y 100, \"\n", " \"y un parámetro de experiencia requerida en meses. \"\n", " \"La puntuación se ha calculado mediante un algoritmo que usa distancias de embeddings entre cada uno de los puestos \"\n", " \"y la definición de la oferta, así como la duración de cada puesto y su relación con el parámetro de experiencia requerida. \"\n", " \"Devuelves un objeto con el esquema predefinido,\"\n", " \"incluyendo exactamente la misma puntuación proporcionada, el listado de experiencia proporcionado \"\n", " \"y además devuelves un breve texto explicativo sobre la experiencia del candidato y \"\n", " \"por qué ha obtenido la puntuación dada. Es importante que el texto explicativo sea coherente con la puntuación. \"\n", " \"Por ejemplo, si la puntuación es mayor que 80, el texto explicativo debe hacer énfasis en las experiencias pasadas \"\n", " \"y la duración de las mismas que han llevado a esa puntuación. \"\n", " \"Cuando menciones algo en relación a la duración de la experiencia, asegúrate de convertirlo a años si es mayor que 12 meses.\"\n", " )\n", "\n", "user_prompt = (\"El título de la oferta de trabajo es: {job}.\" \n", " \"La experiencia requerida en meses es: {req_experience}.\" \n", " \"La puntuacion es {puntuacion}, \"\n", " \"La experiencia relevante es: {exp}. \"\n", " \"Explica por qué se ha obtenido la puntuación\"\n", " )\n", "\n", "\n", "# Los guardamos en ficheros de texto para simplificar el código y facilitar su mantenimiento y edición:\n", "with open('../prompts/system_prompt.txt', 'w', encoding='utf-8') as f:\n", " f.write(system_prompt)\n", "\n", "with open('../prompts/user_prompt.txt', 'w', encoding='utf-8') as f:\n", " f.write(user_prompt)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "### System prompt ###\n", "Eres un procesador de currículos vitae que recibe una oferta de trabajo un currículum vitae filtrado la experiencia\n", "relevante previa, una puntuación precalculada para el currículo entre 0 y 100, y un parámetro de experiencia requerida\n", "en meses. La puntuación se ha calculado mediante un algoritmo que usa distancias de embeddings entre cada uno de los\n", "puestos y la definición de la oferta, así como la duración de cada puesto y su relación con el parámetro de experiencia\n", "requerida. Devuelves un objeto con el esquema predefinido,incluyendo exactamente la misma puntuación proporcionada, el\n", "listado de experiencia proporcionado y además devuelves un breve texto explicativo sobre la experiencia del candidato y\n", "por qué ha obtenido la puntuación dada. Es importante que el texto explicativo sea coherente con la puntuación. Por\n", "ejemplo, si la puntuación es mayor que 80, el texto explicativo debe hacer énfasis en las experiencias pasadas y la\n", "duración de las mismas que han llevado a esa puntuación. Cuando menciones algo en relación a la duración de la\n", "experiencia, asegúrate de convertirlo a años si es mayor que 12 meses.\n", "\n", "### User prompt ###\n", "El título de la oferta de trabajo es: Cajero supermercado Dia.La experiencia requerida en meses es: 12.La puntuacion es\n", "89.01, La experiencia relevante es: {'empresa': ['Mercadona', 'GASTROTEKA ORDIZIA 1990', 'AGRISOLUTIONS'], 'puesto':\n", "['Vendedor/a de puesto de mercado', 'Camarero/a de barra', 'AUXILIAR DE MANTENIMIENTO INDUSTRIAL'], 'duracion': [5, 6,\n", "48], 'position_score': [41.67, 40.87, 6.47]}. Explica por qué se ha obtenido la puntuación\n" ] } ], "source": [ "# Recuperamos los ficheros guardados para comprobar que están bien:\n", "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n", " system_prompt = f.read()\n", "\n", "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n", " user_prompt = f.read()\n", "\n", "print(\"### System prompt ###\")\n", "print(textwrap.fill(system_prompt, width=120))\n", "# En el caso del prompt del usuario, el texto contiene variables que serán reemplazadas por los valores correspondientes.\n", "# Por ejemplo, usamos las definidas en este notebook para visualizar el texto que finalmente recibirá el modelo.\n", "print(\"\\n### User prompt ###\")\n", "user_prompt_con_contexto = user_prompt.format(job=job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n", "print(textwrap.fill(user_prompt_con_contexto, width=120))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Respuesta:\n", " {\n", " \"puntuacion\": 89.01,\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"duracion\": 5\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"duracion\": 6\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"duracion\": 48\n", " }\n", " ],\n", " \"descripcion de la experiencia\": \"El candidato ha acumulado una sólida experiencia en atención al cliente y manejo de operaciones de caja, especialmente a través de su trabajo en Mercadona como Vendedor/a de puesto de mercado durante 5 meses. Además, su paso por GASTROTEKA ORDIZIA 1990 como Camarero/a de barra durante 6 meses le ha permitido desarrollar habilidades interpersonales y de servicio al cliente. Por último, su experiencia de 48 meses en AGRISOLUTIONS como Auxiliar de Mantenimiento Industrial, aunque no directamente relacionada con el puesto de cajero, demuestra una sólida ética de trabajo y capacidad para adaptarse a diferentes entornos laborales. La combinación de estas experiencias ha llevado a una puntuación alta de 89.01, reflejando una adecuada preparación para el puesto.\"\n", "}\n", "Descripción de la experiencia:\n", "El candidato ha acumulado una sólida experiencia en atención al cliente y manejo de operaciones de caja, especialmente a\n", "través de su trabajo en Mercadona como Vendedor/a de puesto de mercado durante 5 meses. Además, su paso por GASTROTEKA\n", "ORDIZIA 1990 como Camarero/a de barra durante 6 meses le ha permitido desarrollar habilidades interpersonales y de\n", "servicio al cliente. Por último, su experiencia de 48 meses en AGRISOLUTIONS como Auxiliar de Mantenimiento Industrial,\n", "aunque no directamente relacionada con el puesto de cajero, demuestra una sólida ética de trabajo y capacidad para\n", "adaptarse a diferentes entornos laborales. La combinación de estas experiencias ha llevado a una puntuación alta de\n", "89.01, reflejando una adecuada preparación para el puesto.\n" ] } ], "source": [ "messages = [\n", " {\n", " \"role\": \"system\",\n", " \"content\": system_prompt\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": user_prompt.format(job=job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n", " }\n", "]\n", "\n", "functions = [\n", " {\n", " \"name\": \"respuesta_formateada\",\n", " \"description\": \"Devuelve el objeto con puntuacion, experiencia y descripcion de la experiencia\",\n", " \"parameters\": response_schema\n", " }\n", "]\n", "\n", "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " temperature=0.5,\n", " messages=messages,\n", " functions=functions,\n", " function_call={\"name\": \"respuesta_formateada\"}\n", ")\n", "\n", "if response.choices[0].message.function_call:\n", " function_call = response.choices[0].message.function_call\n", " structured_output = json.loads(function_call.arguments)\n", " print(\"Respuesta:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n", " wrapped_description = textwrap.fill(structured_output['descripcion de la experiencia'], width=120)\n", " print(f\"Descripción de la experiencia:\\n{wrapped_description}\")\n", "else:\n", " print(\"Error:\", response.choices[0].message.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Prueba final del código completo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Una vez comprobado el proceso completo, podemos encapsular todo el código en la clase definida al inicio de este notebook. Finalmente, guardaremos el módulo en un fichero .py al que llamará la interfaz de usuario a diseñar en el próximo notebook." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class ProcesadorCV:\n", "\n", " def __init__(self, api_key, cv_text, job_text, ner_pre_prompt, system_prompt, user_prompt, ner_schema, response_schema,\n", " inference_model=\"gpt-4o-mini\", embeddings_model=\"text-embedding-3-small\"):\n", " \"\"\"\n", " Inicializa una instancia de la clase con los parámetros proporcionados.\n", "\n", " Args:\n", " api_key (str): La clave de API para autenticar con el cliente OpenAI.\n", " cv_text (str): contenido del CV en formato de texto.\n", " job_text (str): título de la oferta de trabajo a evaluar.\n", " ner_pre_prompt (str): instrucción de \"reconocimiento de entidades nombradas\" (NER) para el modelo en lenguaje natural.\n", " system_prompt (str): instrucción en lenguaje natural para la salida estructurada final.\n", " user_prompt (str): instrucción con los parámetros y datos calculados en el preprocesamiento.\n", " ner_schema (dict): esquema para la llamada con \"structured outputs\" al modelo de OpenAI para NER.\n", " response_schema (dict): esquema para la respuesta final de la aplicación.\n", " inference_model (str, opcional): El modelo de inferencia a utilizar. Por defecto es \"gpt-4o-mini\".\n", " embeddings_model (str, opcional): El modelo de embeddings a utilizar. Por defecto es \"text-embedding-3-small\".\n", "\n", " Atributos:\n", " inference_model (str): Almacena el modelo de inferencia seleccionado.\n", " embeddings_model (str): Almacena el modelo de embeddings seleccionado.\n", " client (OpenAI): Instancia del cliente OpenAI inicializada con la clave de API proporcionada.\n", " cv (str): Almacena el texto del currículum vitae proporcionado.\n", "\n", " \"\"\"\n", " self.inference_model = inference_model\n", " self.embeddings_model = embeddings_model\n", " self.ner_pre_prompt = ner_pre_prompt\n", " self.user_prompt = user_prompt\n", " self.system_prompt = system_prompt\n", " self.ner_schema = ner_schema\n", " self.response_schema = response_schema\n", " self.client = OpenAI(api_key=api_key)\n", " self.cv = cv_text\n", " self.job_text = job_text\n", " print(\"Cliente inicializado como\",self.client)\n", "\n", " def extraer_datos_cv(self, temperature=0.5):\n", " \"\"\"\n", " Extrae datos estructurados de un CV con OpenAI API.\n", " Args:\n", " pre_prompt (str): instrucción para el modelo en lenguaje natural.\n", " schema (dict): esquema de los parámetros que se espera extraer del CV.\n", " temperature (float, optional): valor de temperatura para el modelo de lenguaje. Por defecto es 0.5.\n", " Returns:\n", " pd.DataFrame: DataFrame con los datos estructurados extraídos del CV.\n", " Raises:\n", " ValueError: si no se pueden extraer datos estructurados del CV.\n", " \"\"\"\n", " response = self.client.chat.completions.create(\n", " model=self.inference_model,\n", " temperature=temperature,\n", " messages=[\n", " {\"role\": \"system\", \"content\": self.ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": self.cv}\n", " ],\n", " functions=[\n", " {\n", " \"name\": \"extraer_datos_cv\",\n", " \"description\": \"Extrae tabla con títulos de puesto de trabajo, nombres de empresa y períodos de un CV.\",\n", " \"parameters\": self.ner_schema\n", " }\n", " ],\n", " function_call=\"auto\"\n", " )\n", "\n", " if response.choices[0].message.function_call:\n", " function_call = response.choices[0].message.function_call\n", " structured_output = json.loads(function_call.arguments)\n", " if structured_output.get(\"experiencia\"):\n", " df_cv = pd.DataFrame(structured_output[\"experiencia\"]) \n", " return df_cv\n", " else:\n", " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n", " else:\n", " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n", " \n", "\n", " def procesar_periodos(self, df): \n", " \"\"\"\n", " Procesa los períodos en un DataFrame y añade columnas con las fechas de inicio, fin y duración en meses. \n", " Si no hay fecha de fin, se considera la fecha actual.\n", " Args:\n", " df (pandas.DataFrame): DataFrame que contiene una columna 'periodo' con períodos en formato 'YYYYMM-YYYYMM' o 'YYYYMM'.\n", " Returns:\n", " pandas.DataFrame: DataFrame con columnas adicionales 'fec_inicio', 'fec_final' y 'duracion'.\n", " - 'fec_inicio' (datetime.date): Fecha de inicio del período.\n", " - 'fec_final' (datetime.date): Fecha de fin del período.\n", " - 'duracion' (int): Duración del período en meses.\n", " \"\"\"\n", " # Función lambda para procesar el período\n", " def split_periodo(periodo):\n", " dates = periodo.split('-')\n", " start_date = datetime.strptime(dates[0], \"%Y%m\")\n", " if len(dates) > 1:\n", " end_date = datetime.strptime(dates[1], \"%Y%m\")\n", " else:\n", " end_date = datetime.now()\n", " return start_date, end_date\n", "\n", " df[['fec_inicio', 'fec_final']] = df['periodo'].apply(lambda x: pd.Series(split_periodo(x)))\n", "\n", " # Formateamos las fechas para mostrar mes, año, y el primer día del mes (dado que el día es irrelevante y no se suele especificar)\n", " df['fec_inicio'] = df['fec_inicio'].dt.date\n", " df['fec_final'] = df['fec_final'].dt.date\n", "\n", " # Añadimos una columna con la duración en meses\n", " df['duracion'] = df.apply(\n", " lambda row: (row['fec_final'].year - row['fec_inicio'].year) * 12 + \n", " row['fec_final'].month - row['fec_inicio'].month, \n", " axis=1\n", " )\n", "\n", " return df\n", "\n", "\n", " def calcular_embeddings(self, df, column='puesto', model_name='text-embedding-3-small'):\n", " \"\"\"\n", " Calcula los embeddings de una columna de un dataframe con OpenAI API.\n", " Args:\n", " cv_df (pandas.DataFrame): DataFrame con los datos de los CV.\n", " column (str, optional): Nombre de la columna que contiene los datos a convertir en embeddings. Por defecto es 'puesto'.\n", " model_name (str, optional): Nombre del modelo de embeddings. Por defecto es 'text-embedding-3-small'.\n", " \"\"\"\n", " df['embeddings'] = df[column].apply(\n", " lambda puesto: self.client.embeddings.create(\n", " input=puesto, \n", " model=model_name\n", " ).data[0].embedding\n", " )\n", " return df\n", "\n", "\n", " def calcular_distancias(self, df, column='embeddings', model_name='text-embedding-3-small'):\n", " \"\"\"\n", " Calcula la distancia coseno entre los embeddings del texto y los incluidos en una columna del dataframe.\n", " Params:\n", " df (pandas.DataFrame): DataFrame que contiene los embeddings.\n", " column (str, optional): nombre de la columna del DataFrame que contiene los embeddings. Por defecto, 'embeddings'.\n", " model_name (str, optional): modelo de embeddings de la API de OpenAI. Por defecto \"text-embedding-3-small\".\n", " Returns:\n", " pandas.DataFrame: DataFrame ordenado de menor a mayor distancia, con las distancias en una nueva columna.\n", " \"\"\"\n", " response = self.client.embeddings.create(\n", " input=self.job_text,\n", " model=model_name\n", " )\n", " emb_compare = response.data[0].embedding\n", "\n", " df['distancia'] = df[column].apply(lambda emb: spatial.distance.cosine(emb, emb_compare))\n", " df.drop(columns=[column], inplace=True)\n", " df.sort_values(by='distancia', ascending=True, inplace=True)\n", " return df\n", "\n", "\n", " def calcular_puntuacion(self, df, req_experience, positions_cap=4, dist_threshold_low=0.6, dist_threshold_high=0.7):\n", " \"\"\"\n", " Calcula la puntuación de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n", "\n", " Params:\n", " df (pandas.DataFrame): datos de un CV incluyendo diferentes experiencias incluyendo duracies y distancia previamente calculadas sobre los embeddings de un puesto de trabajo\n", " req_experience (float): experiencia requerida en meses para el puesto de trabajo (valor de referencia para calcular una puntuación entre 0 y 100 en base a diferentes experiencias)\n", " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n", " dist_threshold_low (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV se considera \"equivalente\" al de la oferta.\n", " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no puntúa.\n", " \n", " Returns:\n", " pandas.DataFrame: DataFrame original añadiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n", " float: Puntuación total entre 0 y 100.\n", " \"\"\"\n", " # A efectos de puntuación, computamos para cada puesto como máximo el número total de meses de experiencia requeridos\n", " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n", " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia mínima y 1 la máxima\n", " df['adjusted_distance'] = df['distancia'].apply(\n", " lambda x: 0 if x <= dist_threshold_low else (\n", " 1 if x >= dist_threshold_high else (x - dist_threshold_low) / (dist_threshold_high - dist_threshold_low)\n", " )\n", " )\n", " # Cada puesto puntúa en base a su duración y a la inversa de la distancia (a menor distancia, mayor puntuación)\n", " df['position_score'] = round(((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100), 2)\n", " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuación 0), y ordenamos por puntuación\n", " df.loc[df['distancia'] >= dist_threshold_high, 'position_score'] = 0\n", " df = df.sort_values(by='position_score', ascending=False)\n", " # Nos quedamos con los puestos con mayor puntuación (positions_cap)\n", " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n", " # Totalizamos (no debería superar 100 nunca, pero ponemos un límite para asegurar) y redondeamos a dos decimales\n", " total_score = round(min(df['position_score'].sum(), 100), 2)\n", " return df, total_score\n", " \n", " def filtra_experiencia_relevante(self, df):\n", " \"\"\"\n", " Filtra las experiencias relevantes del dataframe y las devuelve en formato diccionario.\n", " Args:\n", " df (pandas.DataFrame): DataFrame con la información completa de experiencia.\n", " Returns:\n", " dict: Diccionario con las experiencias relevantes.\n", " \"\"\"\n", " df_experiencia = df[df['position_score'] > 0].copy()\n", " df_experiencia.drop(columns=['periodo', 'fec_inicio', 'fec_final', \n", " 'distancia', 'duration_capped', 'adjusted_distance'], inplace=True)\n", " experiencia_dict = df_experiencia.to_dict(orient='list')\n", " return experiencia_dict\n", " \n", " def llamada_final(self, req_experience, puntuacion, dict_experiencia):\n", " \"\"\"\n", " Realiza la llamada final al modelo de lenguaje para generar la respuesta final.\n", " Args:\n", " req_experience (int): Experiencia requerida en meses para el puesto de trabajo.\n", " puntuacion (float): Puntuación total del CV.\n", " dict_experiencia (dict): Diccionario con las experiencias relevantes.\n", " Returns:\n", " dict: Diccionario con la respuesta final.\n", " \"\"\"\n", " messages = [\n", " {\n", " \"role\": \"system\",\n", " \"content\": self.system_prompt\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": self.user_prompt.format(job=self.job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n", " }\n", " ]\n", "\n", " functions = [\n", " {\n", " \"name\": \"respuesta_formateada\",\n", " \"description\": \"Devuelve el objeto con puntuacion, experiencia y descripcion de la experiencia\",\n", " \"parameters\": self.response_schema\n", " }\n", " ]\n", "\n", " response = self.client.chat.completions.create(\n", " model=self.inference_model,\n", " temperature=0.5,\n", " messages=messages,\n", " functions=functions,\n", " function_call={\"name\": \"respuesta_formateada\"}\n", " )\n", "\n", " if response.choices[0].message.function_call:\n", " function_call = response.choices[0].message.function_call\n", " structured_output = json.loads(function_call.arguments)\n", " print(\"Respuesta:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n", " wrapped_description = textwrap.fill(structured_output['descripcion de la experiencia'], width=120)\n", " print(f\"Descripción de la experiencia:\\n{wrapped_description}\")\n", " return structured_output\n", " else:\n", " raise ValueError(f\"Error. No se ha podido generar respuesta:\\n {response.choices[0].message.content}\")\n", " \n", " def procesar_cv_completo(self, req_experience, positions_cap, dist_threshold_low, dist_threshold_high):\n", " \"\"\"\n", " Procesa un CV y calcula la puntuación final.\n", " Args:\n", " req_experience (int, optional): Experiencia requerida en meses para el puesto de trabajo.\n", " positions_cap (int, optional): Número máximo de puestos a considerar para la puntuación.\n", " dist_threshold_low (float, optional): Distancia límite para considerar un puesto equivalente.\n", " dist_threshold_high (float, optional): Distancia límite para considerar un puesto no relevante.\n", " Returns:\n", " pd.DataFrame: DataFrame con las puntuaciones individuales contribuidas por cada puesto.\n", " float: Puntuación total entre 0 y 100.\n", " \"\"\"\n", " df_datos_estructurados_cv = self.extraer_datos_cv()\n", " df_datos_estructurados_cv = self.procesar_periodos(df_datos_estructurados_cv)\n", " df_con_embeddings = self.calcular_embeddings(df_datos_estructurados_cv)\n", " df_con_distancias = self.calcular_distancias(df_con_embeddings)\n", " df_puntuaciones, puntuacion = self.calcular_puntuacion(df_con_distancias,\n", " req_experience=req_experience,\n", " positions_cap=positions_cap,\n", " dist_threshold_low=dist_threshold_low,\n", " dist_threshold_high=dist_threshold_high)\n", " dict_experiencia = self.filtra_experiencia_relevante(df_puntuaciones)\n", " dict_respuesta = self.llamada_final(req_experience, puntuacion, dict_experiencia)\n", " return dict_respuesta" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cliente inicializado como \n", "Respuesta:\n", " {\n", " \"puntuacion\": 68.6,\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"duracion\": 48\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"duracion\": 5\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"duracion\": 6\n", " },\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\",\n", " \"duracion\": 1\n", " }\n", " ],\n", " \"descripcion de la experiencia\": \"El candidato cuenta con una experiencia total de aproximadamente 4 años en diferentes roles, aunque su experiencia más relevante para el puesto de cajero en supermercado es limitada. Ha trabajado como vendedor en Mercadona y en un puesto de mercado, lo que le ha proporcionado habilidades de atención al cliente y manejo de efectivo, aunque la duración de estos puestos es relativamente corta. Además, su experiencia como auxiliar de mantenimiento industrial y en el sector de la hostelería, aunque no directamente relacionada, le ha otorgado habilidades valiosas en el trato con el público y en la gestión de situaciones de presión. La puntuación de 68.6 refleja una experiencia que, aunque no cumple con los 24 meses requeridos, muestra un potencial en el área de atención al cliente y ventas.\"\n", "}\n", "Descripción de la experiencia:\n", "El candidato cuenta con una experiencia total de aproximadamente 4 años en diferentes roles, aunque su experiencia más\n", "relevante para el puesto de cajero en supermercado es limitada. Ha trabajado como vendedor en Mercadona y en un puesto\n", "de mercado, lo que le ha proporcionado habilidades de atención al cliente y manejo de efectivo, aunque la duración de\n", "estos puestos es relativamente corta. Además, su experiencia como auxiliar de mantenimiento industrial y en el sector de\n", "la hostelería, aunque no directamente relacionada, le ha otorgado habilidades valiosas en el trato con el público y en\n", "la gestión de situaciones de presión. La puntuación de 68.6 refleja una experiencia que, aunque no cumple con los 24\n", "meses requeridos, muestra un potencial en el área de atención al cliente y ventas.\n" ] } ], "source": [ "# Parámetros de ejecución:\n", "job_text = \"Cajero supermercado Dia\"\n", "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un currículo de ejemplo\n", "with open(cv_sample_path, 'r') as file:\n", " cv_text = file.read()\n", "# Prompts:\n", "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as f:\n", " ner_pre_prompt = f.read()\n", "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n", " system_prompt = f.read()\n", "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n", " user_prompt = f.read()\n", "# Esquemas JSON:\n", "with open('../json/ner_schema.json', 'r', encoding='utf-8') as f:\n", " ner_schema = json.load(f)\n", "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n", " response_schema = json.load(f)\n", "\n", "\n", "procesador_cvs_prueba_final = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n", " system_prompt, user_prompt, ner_schema, response_schema)\n", "req_experience = 24 # Experiencia requerida en meses\n", "positions_cap=4 # Número máximo de puestos a considerar\n", "dist_threshold_low=0.55 # Distancia límite para considerar un puesto equivalente\n", "dist_threshold_high=0.65 # Distancia límite para considerar un puesto no relevante\n", "dict_respuesta = procesador_cvs_prueba_final.procesar_cv_completo(req_experience=req_experience,\n", " positions_cap=positions_cap,\n", " dist_threshold_low=dist_threshold_low,\n", " dist_threshold_high=dist_threshold_high\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Probamos con otro ejemplo:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cliente inicializado como \n", "Respuesta:\n", " {\n", " \"puntuacion\": 100,\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Talking to Chatbots, by Reddgr\",\n", " \"puesto\": \"Web Publisher and Generative AI Researcher\",\n", " \"duracion\": 206\n", " },\n", " {\n", " \"empresa\": \"IBM\",\n", " \"puesto\": \"Relationship Manager | Cognitive Solutions SaaS\",\n", " \"duracion\": 43\n", " },\n", " {\n", " \"empresa\": \"Acoustic\",\n", " \"puesto\": \"Principal Consultant | Martech SaaS\",\n", " \"duracion\": 35\n", " },\n", " {\n", " \"empresa\": \"IBM\",\n", " \"puesto\": \"Engagement Manager, in support of Acoustic | B2B SaaS Retail Analytics\",\n", " \"duracion\": 10\n", " },\n", " {\n", " \"empresa\": \"IBM\",\n", " \"puesto\": \"Engagement Manager | B2B SaaS Retail Analytics\",\n", " \"duracion\": 9\n", " },\n", " {\n", " \"empresa\": \"MBD Analytics\",\n", " \"puesto\": \"Business Intelligence Consultant\",\n", " \"duracion\": 10\n", " }\n", " ],\n", " \"descripcion de la experiencia\": \"El candidato ha obtenido una puntuación perfecta de 100 gracias a su extensa y relevante experiencia en el campo de la inteligencia artificial generativa y tecnologías relacionadas. Con más de 17 años de experiencia acumulada, ha trabajado en puestos clave como Web Publisher y Generative AI Researcher en 'Talking to Chatbots, by Reddgr', donde su enfoque en la investigación de IA generativa ha sido fundamental. Además, su trayectoria en IBM, donde ocupó roles en soluciones cognitivas y análisis minorista, ha reforzado su conocimiento en SaaS y su capacidad para gestionar relaciones con clientes en entornos tecnológicos avanzados. La combinación de estas experiencias, junto con su sólida formación en consultoría y análisis de datos, lo posiciona como un candidato excepcionalmente calificado para el puesto.\"\n", "}\n", "Descripción de la experiencia:\n", "El candidato ha obtenido una puntuación perfecta de 100 gracias a su extensa y relevante experiencia en el campo de la\n", "inteligencia artificial generativa y tecnologías relacionadas. Con más de 17 años de experiencia acumulada, ha trabajado\n", "en puestos clave como Web Publisher y Generative AI Researcher en 'Talking to Chatbots, by Reddgr', donde su enfoque en\n", "la investigación de IA generativa ha sido fundamental. Además, su trayectoria en IBM, donde ocupó roles en soluciones\n", "cognitivas y análisis minorista, ha reforzado su conocimiento en SaaS y su capacidad para gestionar relaciones con\n", "clientes en entornos tecnológicos avanzados. La combinación de estas experiencias, junto con su sólida formación en\n", "consultoría y análisis de datos, lo posiciona como un candidato excepcionalmente calificado para el puesto.\n" ] } ], "source": [ "# Parámetros de ejecución:\n", "job_text = \"Generative AI engineer\"\n", "cv_sample_path = '../../ejemplos_cvs/DavidGR_cv.txt' # Ruta al fichero de texto con un currículo de ejemplo\n", "with open(cv_sample_path, 'r') as file:\n", " cv_text = file.read()\n", "# Prompts:\n", "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as f:\n", " ner_pre_prompt = f.read()\n", "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n", " system_prompt = f.read()\n", "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n", " user_prompt = f.read()\n", "# Esquemas JSON:\n", "with open('../json/ner_schema.json', 'r', encoding='utf-8') as f:\n", " ner_schema = json.load(f)\n", "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n", " response_schema = json.load(f)\n", "\n", "\n", "procesador_cvs_prueba_final = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n", " system_prompt, user_prompt, ner_schema, response_schema)\n", "req_experience = 48 # Experiencia requerida en meses\n", "positions_cap=10 # Número máximo de puestos a considerar\n", "dist_threshold_low=0.5 # Distancia límite para considerar un puesto equivalente\n", "dist_threshold_high=0.7 # Distancia límite para considerar un puesto no relevante\n", "dict_respuesta = procesador_cvs_prueba_final.procesar_cv_completo(req_experience=req_experience,\n", " positions_cap=positions_cap,\n", " dist_threshold_low=dist_threshold_low,\n", " dist_threshold_high=dist_threshold_high\n", " )" ] } ], "metadata": { "kernelspec": { "display_name": "base", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5" } }, "nbformat": 4, "nbformat_minor": 2 }