{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. Preparación del notebook e inicialización del cliente de OpenAI API" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n", "Cliente inicializado como \n" ] } ], "source": [ "import os\n", "import pandas as pd\n", "import json\n", "import textwrap\n", "from datetime import datetime\n", "from openai import OpenAI\n", "from dotenv import load_dotenv\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}\")\n", "client = OpenAI(api_key=api_key)\n", "print(\"Cliente inicializado como\",client)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Zero-shot named entity recognition" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Empezamos con un caso sencillo extrayendo un texto del CV de ejemplo y sin especificar esquema para el diccionario de datos json:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\"\n", "}\n" ] } ], "source": [ "text = \"Vendedor/a de puesto de mercado - Mercadona\"\n", "# System prompt para reconocimiento de entidades nombradas (NER) de nombres de compañías y títulos de puestos de trabajo\n", "ner_pre_prompt = (\n", " \"Eres un procesador de currículos vitae que extrae nombres de \"\n", " \"compañías/empresas y títulos de puestos de trabajo. Usa formato json en la salida \"\n", " 'con las claves \"empresa\" y \"puesto\".'\n", ")\n", "\n", "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " response_format={\"type\": \"json_object\"}, # De momento no facilitamos esquema. Lo probaremos más adelante.\n", " messages=[\n", " {\"role\": \"system\", \"content\": ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": text}\n", " ]\n", " )\n", "generated_content = response.choices[0].message.content\n", "print(generated_content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ejemplo de reconocimiento de entidades nombradas en un currículo completo. Hemos utilizado un CV de ejemplo no incluido en el repositorio. Para ejecutar el siguiente bloque, es necesario facilitar una ruta válida a un currículo:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Candidato: Mohamed van der Poel Mendieta\n", "Último Puesto Comercial de automoviles\n", "Última formación reglada FP 1 / Técnico medio\n", "3\n", "Idioma EspañolInglésFr ...\n" ] } ], "source": [ "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", "print(cv_text[:150],\"...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Inferencia de entidades nombradas \"empresa\" y \"puesto\" con un modelo de OpenAI (elegimos gpt-4o-mini para reducir los costes y dado que esto sólo es una sencilla prueba de concepto)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"experiencias\": [\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\"\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\"\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\"\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\"\n", " },\n", " {\n", " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n", " \"puesto\": \"Limpieza industrial\"\n", " },\n", " {\n", " \"empresa\": \"Bellota Herramientas\",\n", " \"puesto\": \"Personal de mantenimiento\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " response_format={\"type\": \"json_object\"},\n", " messages=[\n", " {\"role\": \"system\", \"content\": ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": cv_text}\n", " ]\n", " )\n", "generated_content = response.choices[0].message.content\n", "print(generated_content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Procesamiento de fechas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vamos a intentar extraer también las fechas para cada puesto de trabajo. Para ello, añadiremos algunas indicaciones adicionales en relación a los posibles formatos de entrada y al formato de salida. En cuanto a las entradas, asumimos que cada CV puede tener formatos muy distintos para esta información. Para las salidas, queremos un formato que nos facilite posteriormente realizar cálculos con fechas como la duración total, antigüedad con respecto a fecha actual, etc." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Eres un procesador de currículos vitae que extrae títulos de puestos de trabajo, nombres de la\n", "empresa, y períodos de los mismos. Usa formato json en la salida con las claves \"empresa\", \"puesto\"\n", "y \"periodo\". Para el período, contempla cualquier formato de fecha o rango de fechas incluido en el\n", "texto. Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". Otros ejemplos\n", "de formatos de fecha son \"10/2023 - 03/2024\", \"Oct 2023 - Mar 2024\", etc. El contenido para la clave\n", "\"período\" debe ser un string con dos elementos en formato YYYYMM separados por un guion, por ejemplo\n", "\"202310-202403\", o uno en caso de no identificarse fecha de fin.\n" ] } ], "source": [ "explicacion_fechas = (\n", " 'Para el período, contempla cualquier formato de fecha o rango de fechas incluido en el texto. '\n", " 'Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". Otros ejemplos de '\n", " 'formatos de fecha son \"10/2023 - 03/2024\", \"Oct 2023 - Mar 2024\", etc. '\n", " 'El contenido para la clave \"período\" debe ser un string con dos elementos en formato YYYYMM '\n", " 'separados por un guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.'\n", " )\n", "\n", "ner_pre_prompt = (\n", " 'Eres un procesador de currículos vitae que extrae títulos de puestos de trabajo, '\n", " 'nombres de la empresa, y períodos de los mismos. Usa formato json en la salida '\n", " f'con las claves \"empresa\", \"puesto\" y \"periodo\". {explicacion_fechas}'\n", ")\n", "wrapped_ner_pre_prompt = textwrap.fill(ner_pre_prompt, width=100)\n", "print(wrapped_ner_pre_prompt)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\",\n", " \"periodo\": \"202401-202402\"\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"periodo\": \"202310-202403\"\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"periodo\": \"202001-202401\"\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"periodo\": \"202303-202309\"\n", " },\n", " {\n", " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n", " \"puesto\": \"limpieza industrial\",\n", " \"periodo\": \"202012-202305\"\n", " },\n", " {\n", " \"empresa\": \"Bellota Herramientas\",\n", " \"puesto\": \"Personal de mantenimiento\",\n", " \"periodo\": \"202005-202011\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " response_format={\"type\": \"json_object\"},\n", " messages=[\n", " {\"role\": \"system\", \"content\": ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": cv_text}\n", " ]\n", " )\n", "generated_content = response.choices[0].message.content\n", "print(generated_content)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "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", "
empresapuestoperiodo
0AutónomoComercial de automoviles202401-202402
1MercadonaVendedor/a de puesto de mercado202310-202403
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-202401
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-202309
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-202305
5Bellota HerramientasPersonal de mantenimiento202005-202011
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "0 Autónomo Comercial de automoviles \n", "1 Mercadona Vendedor/a de puesto de mercado \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "5 Bellota Herramientas Personal de mantenimiento \n", "\n", " periodo \n", "0 202401-202402 \n", "1 202310-202403 \n", "2 202001-202401 \n", "3 202303-202309 \n", "4 202012-202305 \n", "5 202005-202011 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Convertimos el texto en un objeto JSON\n", "json_object = json.loads(generated_content)\n", "# Convertimos a Pandas dataframe para realizar operaciones\n", "# Aún no hemos especificado el esquema completo (a veces puede ser que el modelo nos dé \"experiencias\" en lugar de \"experiencia\")\n", "df = pd.DataFrame(json_object[\"experiencia\"]) \n", "display(df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Antes de desarrollar el código para la extracción y tratamiento de fechas, vamos a comprobar si el modelo es capaz de procesar correctamente un puesto sin fecha de fin en el período. Vamos a eliminar la fecha de fin en el puesto \"comercial de automóviles\" y guardarlo en '../../ejemplos_cvs/cv_sample_2.txt' (esta ruta no está incluida en el repositorio)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "### Ejemplo original ###\n", "...\n", "Sexo Hombre\n", "Experiencia\n", "Enero 2024 / Febrero 2024\n", "Comercial de automoviles - Autónomo\n", "...\n", "\n", "### Ejemplo modificado ###\n", "...\n", "Sexo Hombre\n", "Experiencia\n", "Enero 2024\n", "Comercial de automoviles - Autónomo\n", "...\n" ] } ], "source": [ "cv_sample_2_path = '../../ejemplos_cvs/cv_sample_2.txt'\n", "with open(cv_sample_2_path, 'r') as file:\n", " cv_text_2 = file.read()\n", "print(f\"### Ejemplo original ###\\n...\\n{cv_text[301:386]}\\n...\")\n", "print(f\"\\n### Ejemplo modificado ###\\n...\\n{cv_text_2[301:371]}\\n...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Volvemos a pedir la inferencia con el CV modificado:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\",\n", " \"periodo\": \"202401\"\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"periodo\": \"202310-202404\"\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"periodo\": \"202001-202401\"\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"periodo\": \"202303-202309\"\n", " },\n", " {\n", " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n", " \"puesto\": \"limpieza industrial\",\n", " \"periodo\": \"202012-202305\"\n", " },\n", " {\n", " \"empresa\": \"Bellota Herramientas\",\n", " \"puesto\": \"Personal de mantenimiento\",\n", " \"periodo\": \"202005-202011\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " response_format={\"type\": \"json_object\"},\n", " messages=[\n", " {\"role\": \"system\", \"content\": ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": cv_text_2} # Sin fecha de fin en la última experiencia\n", " ]\n", " )\n", "generated_content = response.choices[0].message.content\n", "print(generated_content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vemos que el modelo gpt-4o-mini parece suficientemente solvente procesando e interpretando datos no estructurados como fechas. En un caso de uso real en el que dispongamos de muchos ficheros de entrada, podríamos entrenar un modelo de \"named entity recognition\" más sofisticado para asegurar mayor precisión. \n", "\n", "
A continuación, procedemos a tratar las fechas para definir un parámetro de duración del puesto de trabajo: " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "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", "
empresapuestoperiodo
0AutónomoComercial de automoviles202401
1MercadonaVendedor/a de puesto de mercado202310-202404
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-202401
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-202309
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-202305
5Bellota HerramientasPersonal de mantenimiento202005-202011
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "0 Autónomo Comercial de automoviles \n", "1 Mercadona Vendedor/a de puesto de mercado \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "5 Bellota Herramientas Personal de mantenimiento \n", "\n", " periodo \n", "0 202401 \n", "1 202310-202404 \n", "2 202001-202401 \n", "3 202303-202309 \n", "4 202012-202305 \n", "5 202005-202011 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Convertimos el texto en un objeto JSON\n", "json_object = json.loads(generated_content)\n", "# Convertimos a Pandas dataframe para realizar operaciones\n", "df_experiencia = pd.DataFrame(json_object[\"experiencia\"])\n", "display(df_experiencia)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "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", "
empresapuestoperiodofec_iniciofec_finalduracion
0AutónomoComercial de automoviles2024012024-01-012024-12-0811
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-016
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-0148
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-016
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-0129
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-016
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "0 Autónomo Comercial de automoviles \n", "1 Mercadona Vendedor/a de puesto de mercado \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "5 Bellota Herramientas Personal de mantenimiento \n", "\n", " periodo fec_inicio fec_final duracion \n", "0 202401 2024-01-01 2024-12-08 11 \n", "1 202310-202404 2023-10-01 2024-04-01 6 \n", "2 202001-202401 2020-01-01 2024-01-01 48 \n", "3 202303-202309 2023-03-01 2023-09-01 6 \n", "4 202012-202305 2020-12-01 2023-05-01 29 \n", "5 202005-202011 2020-05-01 2020-11-01 6 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Función 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_experiencia[['fec_inicio', 'fec_final']] = df_experiencia['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_experiencia['fec_inicio'] = df_experiencia['fec_inicio'].dt.date\n", "df_experiencia['fec_final'] = df_experiencia['fec_final'].dt.date\n", "\n", "# Añadimos una columna con la duración en meses\n", "df_experiencia['duracion'] = df_experiencia.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", "display(df_experiencia)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_experiencia.to_pickle('../pkl/df_experiencia.pkl') # Guardamos pickle para usarlo en el siguiente notebook" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. NER con sequema para \"structured output\" y llamada a función" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Explicar lo que necesitamos en el prompt y poner \"json_object\" en \"response_format\" parece más suficiente para obtener buenos resultados la mayoría de las veces. Sin embargo, nos podemos encontrar con problemas como, por ejemplo, que el modelo no siempre nos dé la misma palabra como clave de primer nivel (a veces puede poner \"experiencia\", otras veces \"experiencias\", \"roles\"...). Se podría intentar explicar esto con lenguaje natural en el prompt, pero es más sencillo definir un esquema y definirlo como función.\n", "\n", "Sin embargo, para asegurar que el modelo siempre responda con un formato consistente, podemos definir un esquema:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Eres un procesador de currículos vitae que extrae títulos de puestos de trabajo, nombres de la\n", "empresa, y períodos de los mismos. Usa formato json en la salida con las claves \"empresa\", \"puesto\"\n", "y \"periodo\". Para el período, contempla cualquier formato de fecha o rango de fechas incluido en el\n", "texto. Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". El contenido\n", "para la clave \"período\" debe ser un string con dos elementos en formato YYYYMM separados por un\n", "guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.\n" ] } ], "source": [ "explicacion_fechas = (\n", " 'Para el período, contempla cualquier formato de fecha o rango de fechas incluido en el texto. '\n", " 'Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". '\n", " 'El contenido para la clave \"período\" debe ser un string con dos elementos en formato YYYYMM '\n", " 'separados por un guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.'\n", " )\n", "\n", "ner_pre_prompt = (\n", " 'Eres un procesador de currículos vitae que extrae títulos de puestos de trabajo, '\n", " 'nombres de la empresa, y períodos de los mismos. Usa formato json en la salida '\n", " f'con las claves \"empresa\", \"puesto\" y \"periodo\". {explicacion_fechas}'\n", ")\n", "\n", "# Guardamos el prompt para el reconocimiento de entidades nombradas en un archivo de texto\n", "with open('../prompts/ner_pre_prompt.txt', 'w', encoding='utf-8') as file:\n", " file.write(ner_pre_prompt)\n", "\n", "wrapped_ner_pre_prompt = textwrap.fill(ner_pre_prompt, width=100)\n", "print(wrapped_ner_pre_prompt)\n", "cv_sample_2_path = '../../ejemplos_cvs/cv_sample_2.txt'\n", "with open(cv_sample_2_path, 'r') as file:\n", " cv_text_2 = file.read()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Datos estructurados:\n", " {\n", " \"records\": [\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\",\n", " \"periodo\": \"202401-202402\"\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"periodo\": \"202310-202403\"\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"periodo\": \"202001-202401\"\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"periodo\": \"202303-202309\"\n", " },\n", " {\n", " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n", " \"puesto\": \"limpieza industrial\",\n", " \"periodo\": \"202012-202305\"\n", " },\n", " {\n", " \"empresa\": \"Bellota Herramientas\",\n", " \"puesto\": \"Personal de mantenimiento\",\n", " \"periodo\": \"202005-202011\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "# Definimos el esquema en formato JSON\n", "schema = {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"records\": {\n", " \"type\": \"array\",\n", " \"items\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"empresa\": {\"type\": \"string\"},\n", " \"puesto\": {\"type\": \"string\"},\n", " \"periodo\": {\n", " \"type\": \"string\",\n", " \"description\": \"Formato 'YYYYMM-YYYYMM' o simplemente 'YYYYMM' si no aparece fecha de fin.\"\n", " }\n", " },\n", " \"required\": [\"empresa\", \"puesto\", \"periodo\"]\n", " }\n", " }\n", " },\n", " \"required\": [\"records\"]\n", "}\n", "\n", "# Llamamos a la API, incluyendo el esquema deseado en el parámetro 'functions'\n", "response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " messages=[\n", " {\"role\": \"system\", \"content\": ner_pre_prompt},\n", " {\"role\": \"user\", \"content\": cv_text}\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\": schema\n", " }\n", " ],\n", " function_call=\"auto\"\n", ")\n", "\n", "# Extraemos de la respuesta sólo los datos de la funció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(\"Datos estructurados:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n", "else:\n", " print(\"No se han podido extraer datos estructurados.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. NER con esquema en fichero .JSON" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Para desarrollar el código ejecutable más adelante, vamos a utilizar un fichero .json externo con el esquema, lo que facilita el control de versiones y simplifica el código:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Datos estructurados:\n", " {\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Autónomo\",\n", " \"puesto\": \"Comercial de automoviles\",\n", " \"periodo\": \"202401-202402\"\n", " },\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor/a de puesto de mercado\",\n", " \"periodo\": \"202310-202403\"\n", " },\n", " {\n", " \"empresa\": \"AGRISOLUTIONS\",\n", " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n", " \"periodo\": \"202001-202401\"\n", " },\n", " {\n", " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n", " \"puesto\": \"Camarero/a de barra\",\n", " \"periodo\": \"202303-202309\"\n", " },\n", " {\n", " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n", " \"puesto\": \"limpieza industrial\",\n", " \"periodo\": \"202012-202305\"\n", " },\n", " {\n", " \"empresa\": \"Bellota Herramientas\",\n", " \"puesto\": \"Personal de mantenimiento\",\n", " \"periodo\": \"202005-202011\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "# Cargamos el esquema:\n", "with open('../json/ner_schema.json', 'r', encoding='utf-8') as schema_file:\n", " 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", "def extraer_datos_cv(pre_prompt, schema, cv, temperature=0.5):\n", " response = client.chat.completions.create(\n", " model=\"gpt-4o-mini\",\n", " temperature=temperature,\n", " messages=[\n", " {\"role\": \"system\", \"content\": pre_prompt},\n", " {\"role\": \"user\", \"content\": 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\": 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", " return structured_output\n", " else:\n", " return {\"error\": f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\"}\n", " else:\n", " return {\"error\": f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\"}\n", " \n", "datos_estructurados_cv = extraer_datos_cv(ner_pre_prompt, schema, cv_text)\n", "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv, indent=4, ensure_ascii=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pruebas adicionales" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "En las siguientes pruebas, experimentamos con modificaciones del parámetro de temperatura en casos extremos de textos atípicos. El objetivo principal es asegurar que el agente extraiga toda la información válida posible pero, a la vez, evite \"alucinar\" cuando reciba datos confusos. Un parámetro muy alto de temperatura puede producir algunas alucinaciones en casos muy excepcionales, por lo que usaremos un parámetro muy \"conservador\". En cualquier caso, las pruebas son suficientes para estar muy \"cómodos\" con la efectividad del modelo gpt-4o-mini en esta tarea: tiene un rendimiento muy sólido." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Currículum \"minimalista\":" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Datos estructurados:\n", " {\n", " \"experiencia\": [\n", " {\n", " \"empresa\": \"Mercadona\",\n", " \"puesto\": \"Vendedor\",\n", " \"periodo\": \"\"\n", " },\n", " {\n", " \"empresa\": \"Bar de tapas\",\n", " \"puesto\": \"Camarero\",\n", " \"periodo\": \"\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "cv_text_mini = \"Soy un vendedor de puesto de mercado en Mercadona. Antes trabajé como camarero en un bar de tapas.\"\n", "datos_estructurados_cv_mini = extraer_datos_cv(ner_pre_prompt, schema, cv_text_mini, temperature=0.1)\n", "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_mini, indent=4, ensure_ascii=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Texto inválido:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Datos estructurados:\n", " {\n", " \"error\": \"No se han podido extraer datos estructurados: None\"\n", "}\n" ] } ], "source": [ "cv_text_hal = (\n", " \"El rápido zorro marrón salta sobre el perezoso perro. El perro ladra al zorro. \"\n", " \"Los dos animales se miran fijamente. Es una escena común en el bosque. Me gusta el bosque.\"\n", ")\n", "\n", "datos_estructurados_cv_hal = extraer_datos_cv(ner_pre_prompt, schema, cv_text_hal, temperature=0.1)\n", "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_hal, indent=4, ensure_ascii=False))" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Datos estructurados:\n", " {\n", " \"error\": \"No se han podido extraer datos estructurados: None\"\n", "}\n" ] } ], "source": [ "cv_text_hal = (\n", " \"El rápido zorro marrón salta sobre el perezoso perro. El perro ladra al zorro. \"\n", " \"Los dos animales se miran fijamente. Es una escena común en el bosque. Me gusta el bosque.\"\n", ")\n", "\n", "datos_estructurados_cv_hal = extraer_datos_cv(ner_pre_prompt, schema, cv_text_hal, temperature=2)\n", "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_hal, indent=4, ensure_ascii=False))" ] } ], "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 }