diff --git a/deno.lock b/deno.lock index 9a4248c..0ba155c 100644 --- a/deno.lock +++ b/deno.lock @@ -21,6 +21,7 @@ "jsr:@zod/zod@^4.3.5": "4.3.5", "npm:@supabase/supabase-js@2": "2.90.1", "npm:@supabase/supabase-js@^2.90.1": "2.90.1", + "npm:@toon-format/toon@^2.1.0": "2.1.0", "npm:@types/bun@^1.3.5": "1.3.5", "npm:deno@^2.6.4": "2.6.4", "npm:openai@6.16.0": "6.16.0_zod@3.25.76", @@ -186,6 +187,9 @@ "@supabase/storage-js" ] }, + "@toon-format/toon@2.1.0": { + "integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg==" + }, "@types/bun@1.3.5": { "integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==", "dependencies": [ @@ -519,8 +523,7 @@ "minipass", "minizlib", "yallist" - ], - "deprecated": true + ] }, "tr46@0.0.3": { "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" @@ -591,6 +594,7 @@ "packageJson": { "dependencies": [ "npm:@supabase/supabase-js@^2.90.1", + "npm:@toon-format/toon@^2.1.0", "npm:@types/bun@^1.3.5", "npm:deno@^2.6.4", "npm:openai@^6.16.0", diff --git a/notebooks/background-openai.ipynb b/notebooks/background-openai.ipynb index 4450372..e6bfa6f 100644 --- a/notebooks/background-openai.ipynb +++ b/notebooks/background-openai.ipynb @@ -1,575 +1,577 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "bb61ec88", - "metadata": {}, - "outputs": [], - "source": [ - "import { load } from \"jsr:@std/dotenv\";\n", - "import OpenAI from \"jsr:@openai/openai\";\n", - "\n", - "const env = await load({\n", - " export: true,\n", - "});\n", - "\n", - "const openai = new OpenAI();\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "8baca5f2", - "metadata": {}, - "outputs": [], - "source": [ - "import { createClient } from \"@supabase/supabase-js\";\n", - "const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n", - "const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n", - "const supabase = createClient(supabaseUrl, supabaseKey);\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "923b8e08", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Esperando a que la IA termine con la asignatura fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4...\n", - "Conexión WebSocket establecida. La celda está esperando cambios...\n" - ] + "cell_type": "code", + "execution_count": 1, + "id": "bb61ec88", + "metadata": {}, + "outputs": [], + "source": [ + "import { load } from \"jsr:@std/dotenv\";\n", + "import OpenAI from \"jsr:@openai/openai\";\n", + "\n", + "const env = await load({\n", + " export: true,\n", + "});\n", + "\n", + "const openai = new OpenAI();\n" + ] }, { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", - "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", - "\u001b[1;31mClick here for more info. \n", - "\u001b[1;31mView Jupyter log for further details." - ] - } - ], - "source": [ - "const ID_ASIGNATURA = \"fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4\";\n", - "console.log(\n", - " `Esperando a que la IA termine con la asignatura ${ID_ASIGNATURA}...`,\n", - ");\n", - "\n", - "if (!supabase) {\n", - " console.log(\"No hay supabase\");\n", - "} else {\n", - " // 2. Armas la \"trampa\" (el listener)\n", - " await new Promise((resolve) => {\n", - " const canal = supabase\n", - " .channel(\"test-asignatura\")\n", - " .on(\n", - " \"postgres_changes\",\n", - " {\n", - " event: \"UPDATE\",\n", - " schema: \"public\",\n", - " table: \"asignaturas\",\n", - " filter: `id=eq.${ID_ASIGNATURA}`,\n", - " },\n", - " (payload) => {\n", - " const nuevoEstado = payload.new.estado;\n", - "\n", - " if (nuevoEstado !== \"generando\") {\n", - " console.log(\n", - " `¡Proceso completado! El nuevo estado es: ${nuevoEstado}`,\n", - " );\n", - "\n", - " supabase.removeChannel(canal);\n", - " resolve(true); // ¡Aquí destrabamos la celda de Jupyter!\n", - " } else {\n", - " console.log(\"Hubo un update, pero sigue generando...\");\n", - " }\n", - " },\n", - " )\n", - " .subscribe((status) => {\n", - " if (status === \"SUBSCRIBED\") {\n", - " console.log(\n", - " \"Conexión WebSocket establecida. La celda está esperando cambios...\",\n", - " );\n", - " }\n", - " });\n", - " });\n", - "\n", - " console.log(\"Script finalizado correctamente.\");\n", - "}\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a1509d8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iniciando conexión abierta y sin filtros...\n", - "Conectado. Esperando cualquier cambio en la tabla asignaturas...\n" - ] + "cell_type": "code", + "execution_count": 2, + "id": "8baca5f2", + "metadata": {}, + "outputs": [], + "source": [ + "import { createClient } from \"@supabase/supabase-js\";\n", + "const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n", + "const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n", + "const supabase = createClient(supabaseUrl, supabaseKey);\n" + ] }, { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", - "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", - "\u001b[1;31mClick here for more info. \n", - "\u001b[1;31mView Jupyter log for further details." - ] - } - ], - "source": [ - "console.log(`Iniciando conexión abierta y sin filtros...`);\n", - "\n", - "await new Promise((resolve) => {\n", - " const canal = supabase\n", - " .channel(\"test-global-asignaturas\")\n", - " .on(\n", - " \"postgres_changes\",\n", - " {\n", - " event: \"*\", // Escuchamos absolutamente TODO\n", - " schema: \"public\",\n", - " table: \"asignaturas\",\n", - " },\n", - " (payload) => {\n", - " // Imprimimos todo el objeto tal como llega del servidor\n", - " console.log(\"🚨 ¡EVENTO RECIBIDO EN CRUDO!\", payload);\n", - "\n", - " // Cerramos la conexión para liberar la celda\n", - " supabase.removeChannel(canal);\n", - " resolve(true);\n", - " },\n", - " )\n", - " .subscribe((status, err) => {\n", - " if (status === \"SUBSCRIBED\") {\n", - " console.log(\n", - " \"Conectado. Esperando cualquier cambio en la tabla asignaturas...\",\n", - " );\n", - " }\n", - " if (err) {\n", - " console.error(\"Error en el socket:\", err);\n", - " }\n", - " });\n", - "});\n", - "\n", - "console.log(\"Script finalizado.\");\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3602b930", - "metadata": {}, - "outputs": [], - "source": [ - "import(\"https://esm.sh/@supabase/supabase-js\").then(({ createClient }) => {\n", - " const supabase = createClient(\n", - " \"https://bxkskdxwppdlplrkidcz.supabase.co\",\n", - " \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a3NrZHh3cHBkbHBscmtpZGN6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwMzU2NDMsImV4cCI6MjA4NzYxMTY0M30.RIyvmB8Ij_qrsiMn0f65awiG5VaCUA5dvb117wuU3jo\",\n", - " );\n", - "\n", - " supabase.channel(\"prueba-consola\")\n", - " .on(\"postgres_changes\", {\n", - " event: \"*\",\n", - " schema: \"public\",\n", - " table: \"asignaturas\",\n", - " }, (payload) => {\n", - " console.log(\"🔥 ¡LLEGÓ EL EVENTO DESDE SUPABASE!\", payload);\n", - " })\n", - " .subscribe((status) => console.log(\"Estado de la conexión:\", status));\n", - "});\n" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "26b3bb36", - "metadata": {}, - "outputs": [], - "source": [ - "// plan Crear un plan en la base (sin datos) estado generando\n", - "const { data: plan, error } = await supabase.from(\"planes_estudio\").insert({\n", - " \"carrera_id\": \"8208da08-d549-4359-8865-9d806bc54f19\",\n", - " \"estructura_id\": \"69fb2b77-5a95-47e0-bf1f-389d384200e4\",\n", - " \"estado_actual_id\": \"18f49b67-8077-4371-be6e-2019a3be3562\",\n", - " \"nombre\": \"Ingeniearía en Cifbernética y Ciberseguridad (2025)\",\n", - " \"nivel\": \"Doctorado\",\n", - " \"tipo_ciclo\": \"Semestre\",\n", - " \"numero_ciclos\": 8,\n", - " \"activo\": true,\n", - " \"tipo_origen\": \"IA\",\n", - " \"creado_por\": \"11111111-1111-1111-1111-111111111111\",\n", - " \"actualizado_por\": \"11111111-1111-1111-1111-111111111111\",\n", - "}).select(\"*\").single();\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8abb080e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m" + "cell_type": "code", + "execution_count": 4, + "id": "923b8e08", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Esperando a que la IA termine con la asignatura fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4...\n", + "Conexión WebSocket establecida. La celda está esperando cambios...\n" + ] + }, + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "const ID_ASIGNATURA = \"fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4\";\n", + "console.log(\n", + " `Esperando a que la IA termine con la asignatura ${ID_ASIGNATURA}...`,\n", + ");\n", + "\n", + "if (!supabase) {\n", + " console.log(\"No hay supabase\");\n", + "} else {\n", + " // 2. Armas la \"trampa\" (el listener)\n", + " await new Promise((resolve) => {\n", + " const canal = supabase\n", + " .channel(\"test-asignatura\")\n", + " .on(\n", + " \"postgres_changes\",\n", + " {\n", + " event: \"UPDATE\",\n", + " schema: \"public\",\n", + " table: \"asignaturas\",\n", + " filter: `id=eq.${ID_ASIGNATURA}`,\n", + " },\n", + " (payload) => {\n", + " const nuevoEstado = payload.new.estado;\n", + "\n", + " if (nuevoEstado !== \"generando\") {\n", + " console.log(\n", + " `¡Proceso completado! El nuevo estado es: ${nuevoEstado}`,\n", + " );\n", + "\n", + " supabase.removeChannel(canal);\n", + " resolve(true); // ¡Aquí destrabamos la celda de Jupyter!\n", + " } else {\n", + " console.log(\"Hubo un update, pero sigue generando...\");\n", + " }\n", + " },\n", + " )\n", + " .subscribe((status) => {\n", + " if (status === \"SUBSCRIBED\") {\n", + " console.log(\n", + " \"Conexión WebSocket establecida. La celda está esperando cambios...\",\n", + " );\n", + " }\n", + " });\n", + " });\n", + "\n", + " console.log(\"Script finalizado correctamente.\");\n", + "}\n" ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plan.id;\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a382bdc6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{\n", - " id: \u001b[32m\"resp_0f1906be7e83879e00699895d7f1cc8193a960e6ccdab1d377\"\u001b[39m,\n", - " object: \u001b[32m\"response\"\u001b[39m,\n", - " created_at: \u001b[33m1771607511\u001b[39m,\n", - " status: \u001b[32m\"queued\"\u001b[39m,\n", - " background: \u001b[33mtrue\u001b[39m,\n", - " completed_at: \u001b[1mnull\u001b[22m,\n", - " error: \u001b[1mnull\u001b[22m,\n", - " frequency_penalty: \u001b[33m0\u001b[39m,\n", - " incomplete_details: \u001b[1mnull\u001b[22m,\n", - " instructions: \u001b[1mnull\u001b[22m,\n", - " max_output_tokens: \u001b[1mnull\u001b[22m,\n", - " max_tool_calls: \u001b[1mnull\u001b[22m,\n", - " model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n", - " output: [],\n", - " parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n", - " presence_penalty: \u001b[33m0\u001b[39m,\n", - " previous_response_id: \u001b[1mnull\u001b[22m,\n", - " prompt_cache_key: \u001b[1mnull\u001b[22m,\n", - " prompt_cache_retention: \u001b[1mnull\u001b[22m,\n", - " reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n", - " safety_identifier: \u001b[1mnull\u001b[22m,\n", - " service_tier: \u001b[32m\"auto\"\u001b[39m,\n", - " store: \u001b[33mtrue\u001b[39m,\n", - " temperature: \u001b[33m1\u001b[39m,\n", - " text: {\n", - " format: {\n", - " type: \u001b[32m\"json_schema\"\u001b[39m,\n", - " description: \u001b[1mnull\u001b[22m,\n", - " name: \u001b[32m\"response_schema\"\u001b[39m,\n", - " schema: {\n", - " type: \u001b[32m\"object\"\u001b[39m,\n", - " properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n", - " required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n", - " additionalProperties: \u001b[33mfalse\u001b[39m\n", - " },\n", - " strict: \u001b[33mtrue\u001b[39m\n", - " },\n", - " verbosity: \u001b[32m\"medium\"\u001b[39m\n", - " },\n", - " tool_choice: \u001b[32m\"auto\"\u001b[39m,\n", - " tools: [],\n", - " top_logprobs: \u001b[33m0\u001b[39m,\n", - " top_p: \u001b[33m1\u001b[39m,\n", - " truncation: \u001b[32m\"disabled\"\u001b[39m,\n", - " usage: \u001b[1mnull\u001b[22m,\n", - " user: \u001b[1mnull\u001b[22m,\n", - " metadata: {\n", - " tabla: \u001b[32m\"planes_estudio\"\u001b[39m,\n", - " id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m\n", - " },\n", - " output_text: \u001b[32m\"\"\u001b[39m\n", - "}" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "let response = await openai.responses.create({\n", - " model: \"gpt-5-mini\",\n", - " input: \"Hola mundo\",\n", - " metadata: {\n", - " tabla: \"planes_estudio\",\n", - " id: plan.id,\n", - " },\n", - " text: {\n", - " format: {\n", - " type: \"json_schema\",\n", - " name: \"response_schema\",\n", - " schema: {\n", - " type: \"object\",\n", - " properties: {\n", - " \"ai-message\": {\n", - " type: \"string\",\n", - " description:\n", - " \"Un mensaje de texto que la IA generará para el usuario final. Este mensaje debe ser claro y directo, proporcionando la información solicitada por el usuario o indicando que no se puede procesar la solicitud si no está relacionada con planes de estudio o asignaturas.\",\n", - " },\n", - " \"suggested-topic\": {\n", - " type: \"string\",\n", - " description:\n", - " \"Un tema de investigación sugerido por la IA que sea relevante para el plan de estudio o asignatura mencionada en la solicitud del usuario. Este tema debe ser específico y relacionado con el área de estudio correspondiente.\",\n", - " },\n", - " },\n", - " required: [\"ai-message\", \"suggested-topic\"],\n", - " additionalProperties: false,\n", - " },\n", - " },\n", - " },\n", - " background: true,\n", - "});\n", - "\n", - "response;\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b2470ed", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Current status: queued\n", - "Current status: in_progress\n", - "Current status: in_progress\n" - ] }, { - "data": { - "text/plain": [ - "{\n", - " id: \u001b[32m\"resp_0f1906be7e83879e00699895d7f1cc8193a960e6ccdab1d377\"\u001b[39m,\n", - " object: \u001b[32m\"response\"\u001b[39m,\n", - " created_at: \u001b[33m1771607511\u001b[39m,\n", - " status: \u001b[32m\"completed\"\u001b[39m,\n", - " background: \u001b[33mtrue\u001b[39m,\n", - " billing: { payer: \u001b[32m\"developer\"\u001b[39m },\n", - " completed_at: \u001b[33m1771607519\u001b[39m,\n", - " error: \u001b[1mnull\u001b[22m,\n", - " frequency_penalty: \u001b[33m0\u001b[39m,\n", - " incomplete_details: \u001b[1mnull\u001b[22m,\n", - " instructions: \u001b[1mnull\u001b[22m,\n", - " max_output_tokens: \u001b[1mnull\u001b[22m,\n", - " max_tool_calls: \u001b[1mnull\u001b[22m,\n", - " model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n", - " output: [\n", - " {\n", - " id: \u001b[32m\"rs_0f1906be7e83879e00699895d976548193ad40e9aae0b1b4f2\"\u001b[39m,\n", - " type: \u001b[32m\"reasoning\"\u001b[39m,\n", - " summary: []\n", - " },\n", - " {\n", - " id: \u001b[32m\"msg_0f1906be7e83879e00699895ddf62881938811097ba62e8724\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " status: \u001b[32m\"completed\"\u001b[39m,\n", - " content: [\n", - " {\n", - " type: \u001b[32m\"output_text\"\u001b[39m,\n", - " annotations: [],\n", - " logprobs: [],\n", - " text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿Cómo puedo ayudarte hoy? Si te refieres al clásico “Hola mundo” en programación, puedo mostrarte ejemplos en varios lenguajes, explicar su historia o ayudarte a crear tu primer programa. ¿Qué prefieres?\",\"suggested-topic\":\"El uso del programa “Hola mundo” como herramienta pedagógica en la enseñanza de la programación: impacto en la comprensión inicial, variaciones por lenguaje y mejores prácticas didácticas.\"}'\u001b[39m\n", - " }\n", - " ],\n", - " role: \u001b[32m\"assistant\"\u001b[39m\n", - " }\n", - " ],\n", - " parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n", - " presence_penalty: \u001b[33m0\u001b[39m,\n", - " previous_response_id: \u001b[1mnull\u001b[22m,\n", - " prompt_cache_key: \u001b[1mnull\u001b[22m,\n", - " prompt_cache_retention: \u001b[1mnull\u001b[22m,\n", - " reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n", - " safety_identifier: \u001b[1mnull\u001b[22m,\n", - " service_tier: \u001b[32m\"default\"\u001b[39m,\n", - " store: \u001b[33mtrue\u001b[39m,\n", - " temperature: \u001b[33m1\u001b[39m,\n", - " text: {\n", - " format: {\n", - " type: \u001b[32m\"json_schema\"\u001b[39m,\n", - " description: \u001b[1mnull\u001b[22m,\n", - " name: \u001b[32m\"response_schema\"\u001b[39m,\n", - " schema: {\n", - " type: \u001b[32m\"object\"\u001b[39m,\n", - " properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n", - " required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n", - " additionalProperties: \u001b[33mfalse\u001b[39m\n", - " },\n", - " strict: \u001b[33mtrue\u001b[39m\n", - " },\n", - " verbosity: \u001b[32m\"medium\"\u001b[39m\n", - " },\n", - " tool_choice: \u001b[32m\"auto\"\u001b[39m,\n", - " tools: [],\n", - " top_logprobs: \u001b[33m0\u001b[39m,\n", - " top_p: \u001b[33m1\u001b[39m,\n", - " truncation: \u001b[32m\"disabled\"\u001b[39m,\n", - " usage: {\n", - " input_tokens: \u001b[33m167\u001b[39m,\n", - " input_tokens_details: { cached_tokens: \u001b[33m0\u001b[39m },\n", - " output_tokens: \u001b[33m437\u001b[39m,\n", - " output_tokens_details: { reasoning_tokens: \u001b[33m320\u001b[39m },\n", - " total_tokens: \u001b[33m604\u001b[39m\n", - " },\n", - " user: \u001b[1mnull\u001b[22m,\n", - " metadata: {\n", - " tabla: \u001b[32m\"planes_estudio\"\u001b[39m,\n", - " id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m\n", - " },\n", - " output_text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿Cómo puedo ayudarte hoy? Si te refieres al clásico “Hola mundo” en programación, puedo mostrarte ejemplos en varios lenguajes, explicar su historia o ayudarte a crear tu primer programa. ¿Qué prefieres?\",\"suggested-topic\":\"El uso del programa “Hola mundo” como herramienta pedagógica en la enseñanza de la programación: impacto en la comprensión inicial, variaciones por lenguaje y mejores prácticas didácticas.\"}'\u001b[39m\n", - "}" + "cell_type": "code", + "execution_count": null, + "id": "8a1509d8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iniciando conexión abierta y sin filtros...\n", + "Conectado. Esperando cualquier cambio en la tabla asignaturas...\n" + ] + }, + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "console.log(`Iniciando conexión abierta y sin filtros...`);\n", + "\n", + "await new Promise((resolve) => {\n", + " const canal = supabase\n", + " .channel(\"test-global-asignaturas\")\n", + " .on(\n", + " \"postgres_changes\",\n", + " {\n", + " event: \"*\", // Escuchamos absolutamente TODO\n", + " schema: \"public\",\n", + " table: \"asignaturas\",\n", + " },\n", + " (payload) => {\n", + " // Imprimimos todo el objeto tal como llega del servidor\n", + " console.log(\"🚨 ¡EVENTO RECIBIDO EN CRUDO!\", payload);\n", + "\n", + " // Cerramos la conexión para liberar la celda\n", + " supabase.removeChannel(canal);\n", + " resolve(true);\n", + " },\n", + " )\n", + " .subscribe((status, err) => {\n", + " if (status === \"SUBSCRIBED\") {\n", + " console.log(\n", + " \"Conectado. Esperando cualquier cambio en la tabla asignaturas...\",\n", + " );\n", + " }\n", + " if (err) {\n", + " console.error(\"Error en el socket:\", err);\n", + " }\n", + " });\n", + "});\n", + "\n", + "console.log(\"Script finalizado.\");\n" ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "while (response.status === \"queued\" || response.status === \"in_progress\") {\n", - " console.log(\"Current status: \" + response.status);\n", - " await new Promise((resolve) => setTimeout(resolve, 2000)); // wait 2 seconds\n", - " response = await openai.responses.retrieve(response.id);\n", - "}\n", - "\n", - "response;\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebb67bd8", - "metadata": {}, - "outputs": [ + }, { - "data": { - "text/plain": [ - "{ tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m }" + "cell_type": "code", + "execution_count": null, + "id": "3602b930", + "metadata": {}, + "outputs": [], + "source": [ + "import(\"https://esm.sh/@supabase/supabase-js\").then(({ createClient }) => {\n", + " const supabase = createClient(\n", + " \"https://bxkskdxwppdlplrkidcz.supabase.co\",\n", + " \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a3NrZHh3cHBkbHBscmtpZGN6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwMzU2NDMsImV4cCI6MjA4NzYxMTY0M30.RIyvmB8Ij_qrsiMn0f65awiG5VaCUA5dvb117wuU3jo\",\n", + " );\n", + "\n", + " supabase.channel(\"prueba-consola\")\n", + " .on(\"postgres_changes\", {\n", + " event: \"*\",\n", + " schema: \"public\",\n", + " table: \"asignaturas\",\n", + " }, (payload) => {\n", + " console.log(\"🔥 ¡LLEGÓ EL EVENTO DESDE SUPABASE!\", payload);\n", + " })\n", + " .subscribe((status) => console.log(\"Estado de la conexión:\", status));\n", + "});\n" ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response.metadata;\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd18fc2d", - "metadata": {}, - "outputs": [ + }, { - "data": { - "text/plain": [ - "{\n", - " error: \u001b[1mnull\u001b[22m,\n", - " data: {\n", - " id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m,\n", - " carrera_id: \u001b[32m\"8208da08-d549-4359-8865-9d806bc54f19\"\u001b[39m,\n", - " estructura_id: \u001b[32m\"69fb2b77-5a95-47e0-bf1f-389d384200e4\"\u001b[39m,\n", - " nombre: \u001b[32m\"Ingeniearía en Cifbernética y Ciberseguridad (2025)\"\u001b[39m,\n", - " nivel: \u001b[32m\"Doctorado\"\u001b[39m,\n", - " tipo_ciclo: \u001b[32m\"Semestre\"\u001b[39m,\n", - " numero_ciclos: \u001b[33m8\u001b[39m,\n", - " datos: {\n", - " \u001b[32m\"ai-message\"\u001b[39m: \u001b[32m\"¡Hola! ¿Cómo puedo ayudarte hoy? Si te refieres al clásico “Hola mundo” en programación, puedo mostrarte ejemplos en varios lenguajes, explicar su historia o ayudarte a crear tu primer programa. ¿Qué prefieres?\"\u001b[39m,\n", - " \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[32m\"El uso del programa “Hola mundo” como herramienta pedagógica en la enseñanza de la programación: impacto en la comprensión inicial, variaciones por lenguaje y mejores prácticas didácticas.\"\u001b[39m\n", - " },\n", - " estado_actual_id: \u001b[32m\"18f49b67-8077-4371-be6e-2019a3be3562\"\u001b[39m,\n", - " activo: \u001b[33mtrue\u001b[39m,\n", - " tipo_origen: \u001b[32m\"IA\"\u001b[39m,\n", - " meta_origen: {},\n", - " creado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n", - " actualizado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n", - " creado_en: \u001b[32m\"2026-02-20T17:11:28.995222+00:00\"\u001b[39m,\n", - " actualizado_en: \u001b[32m\"2026-02-20T17:12:40.170995+00:00\"\u001b[39m,\n", - " nombre_search: \u001b[32m\"ingeniearia en cifbernetica y ciberseguridad (2025)\"\u001b[39m,\n", - " plan_hash: \u001b[32m\"70d28457bbddfe4f5cde0574\"\u001b[39m\n", - " },\n", - " count: \u001b[1mnull\u001b[22m,\n", - " status: \u001b[33m200\u001b[39m,\n", - " statusText: \u001b[32m\"OK\"\u001b[39m\n", - "}" + "cell_type": "code", + "execution_count": 7, + "id": "26b3bb36", + "metadata": {}, + "outputs": [], + "source": [ + "// plan Crear un plan en la base (sin datos) estado generando\n", + "const { data: plan, error } = await supabase.from(\"planes_estudio\").insert({\n", + " \"carrera_id\": \"8208da08-d549-4359-8865-9d806bc54f19\",\n", + " \"estructura_id\": \"69fb2b77-5a95-47e0-bf1f-389d384200e4\",\n", + " \"estado_actual_id\": \"18f49b67-8077-4371-be6e-2019a3be3562\",\n", + " \"nombre\": \"Ingeniearía en Cifbernética y Ciberseguridad (2025)\",\n", + " \"nivel\": \"Doctorado\",\n", + " \"tipo_ciclo\": \"Semestre\",\n", + " \"numero_ciclos\": 8,\n", + " \"activo\": true,\n", + " \"tipo_origen\": \"IA\",\n", + " \"creado_por\": \"11111111-1111-1111-1111-111111111111\",\n", + " \"actualizado_por\": \"11111111-1111-1111-1111-111111111111\",\n", + "}).select(\"*\").single();\n" ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await supabase.from(response.metadata.tabla).update({\n", - " datos: JSON.parse(response.output_text),\n", - "}).eq(\"id\", response.metadata.id).select(\"*\").single();\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cbbfd8c0", - "metadata": {}, - "outputs": [ + }, { - "ename": "Expected ';', '}' or at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~", - "evalue": "Expected ';', '}' or at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~", - "output_type": "error", - "traceback": [] + "cell_type": "code", + "execution_count": null, + "id": "8abb080e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " message: \u001b[32m\"TypeError: error sending request for url (http://127.0.0.1:54321/rest/v1/planes_estudio?select=*): client error (Connect): tcp connect error: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión. (os error 10061)\"\u001b[39m,\n", + " details: \u001b[32m\"TypeError: error sending request for url (http://127.0.0.1:54321/rest/v1/planes_estudio?select=*): client error (Connect): tcp connect error: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión. (os error 10061)\\n\"\u001b[39m +\n", + " \u001b[32m\" at async mainFetch (ext:deno_fetch/26_fetch.js:192:12)\\n\"\u001b[39m +\n", + " \u001b[32m\" at async fetch (ext:deno_fetch/26_fetch.js:475:11)\\n\"\u001b[39m +\n", + " \u001b[32m\" at async :2:31\"\u001b[39m,\n", + " hint: \u001b[32m\"\"\u001b[39m,\n", + " code: \u001b[32m\"\"\u001b[39m\n", + "}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plan.id;\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a382bdc6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n", + " object: \u001b[32m\"response\"\u001b[39m,\n", + " created_at: \u001b[33m1771867714\u001b[39m,\n", + " status: \u001b[32m\"queued\"\u001b[39m,\n", + " background: \u001b[33mtrue\u001b[39m,\n", + " completed_at: \u001b[1mnull\u001b[22m,\n", + " error: \u001b[1mnull\u001b[22m,\n", + " frequency_penalty: \u001b[33m0\u001b[39m,\n", + " incomplete_details: \u001b[1mnull\u001b[22m,\n", + " instructions: \u001b[1mnull\u001b[22m,\n", + " max_output_tokens: \u001b[1mnull\u001b[22m,\n", + " max_tool_calls: \u001b[1mnull\u001b[22m,\n", + " model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n", + " output: [],\n", + " parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n", + " presence_penalty: \u001b[33m0\u001b[39m,\n", + " previous_response_id: \u001b[1mnull\u001b[22m,\n", + " prompt_cache_key: \u001b[1mnull\u001b[22m,\n", + " prompt_cache_retention: \u001b[1mnull\u001b[22m,\n", + " reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n", + " safety_identifier: \u001b[1mnull\u001b[22m,\n", + " service_tier: \u001b[32m\"auto\"\u001b[39m,\n", + " store: \u001b[33mtrue\u001b[39m,\n", + " temperature: \u001b[33m1\u001b[39m,\n", + " text: {\n", + " format: {\n", + " type: \u001b[32m\"json_schema\"\u001b[39m,\n", + " description: \u001b[1mnull\u001b[22m,\n", + " name: \u001b[32m\"response_schema\"\u001b[39m,\n", + " schema: {\n", + " type: \u001b[32m\"object\"\u001b[39m,\n", + " properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n", + " required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n", + " additionalProperties: \u001b[33mfalse\u001b[39m\n", + " },\n", + " strict: \u001b[33mtrue\u001b[39m\n", + " },\n", + " verbosity: \u001b[32m\"medium\"\u001b[39m\n", + " },\n", + " tool_choice: \u001b[32m\"auto\"\u001b[39m,\n", + " tools: [],\n", + " top_logprobs: \u001b[33m0\u001b[39m,\n", + " top_p: \u001b[33m1\u001b[39m,\n", + " truncation: \u001b[32m\"disabled\"\u001b[39m,\n", + " usage: \u001b[1mnull\u001b[22m,\n", + " user: \u001b[1mnull\u001b[22m,\n", + " metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\n", + " output_text: \u001b[32m\"\"\u001b[39m\n", + "}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "let response = await openai.responses.create({\n", + " model: \"gpt-5-mini\",\n", + " input: \"Hola mundo\",\n", + " metadata: {\n", + " tabla: \"planes_estudio\",\n", + " id: '2',\n", + " },\n", + " text: {\n", + " format: {\n", + " type: \"json_schema\",\n", + " name: \"response_schema\",\n", + " schema: {\n", + " type: \"object\",\n", + " properties: {\n", + " \"ai-message\": {\n", + " type: \"string\",\n", + " description:\n", + " \"Un mensaje de texto que la IA generará para el usuario final. Este mensaje debe ser claro y directo, proporcionando la información solicitada por el usuario o indicando que no se puede procesar la solicitud si no está relacionada con planes de estudio o asignaturas.\",\n", + " },\n", + " \"suggested-topic\": {\n", + " type: \"string\",\n", + " description:\n", + " \"Un tema de investigación sugerido por la IA que sea relevante para el plan de estudio o asignatura mencionada en la solicitud del usuario. Este tema debe ser específico y relacionado con el área de estudio correspondiente.\",\n", + " },\n", + " },\n", + " required: [\"ai-message\", \"suggested-topic\"],\n", + " additionalProperties: false,\n", + " },\n", + " },\n", + " },\n", + " background: true,\n", + "});\n", + "\n", + "response;\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b2470ed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current status: queued\n", + "Current status: in_progress\n", + "Current status: in_progress\n" + ] + }, + { + "data": { + "text/plain": [ + "{\n", + " id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n", + " object: \u001b[32m\"response\"\u001b[39m,\n", + " created_at: \u001b[33m1771867714\u001b[39m,\n", + " status: \u001b[32m\"completed\"\u001b[39m,\n", + " background: \u001b[33mtrue\u001b[39m,\n", + " billing: { payer: \u001b[32m\"developer\"\u001b[39m },\n", + " completed_at: \u001b[33m1771867721\u001b[39m,\n", + " error: \u001b[1mnull\u001b[22m,\n", + " frequency_penalty: \u001b[33m0\u001b[39m,\n", + " incomplete_details: \u001b[1mnull\u001b[22m,\n", + " instructions: \u001b[1mnull\u001b[22m,\n", + " max_output_tokens: \u001b[1mnull\u001b[22m,\n", + " max_tool_calls: \u001b[1mnull\u001b[22m,\n", + " model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n", + " output: [\n", + " {\n", + " id: \u001b[32m\"rs_0ae72cbd0894279200699c8e43cd4c8192bcbc70792b6aa621\"\u001b[39m,\n", + " type: \u001b[32m\"reasoning\"\u001b[39m,\n", + " summary: []\n", + " },\n", + " {\n", + " id: \u001b[32m\"msg_0ae72cbd0894279200699c8e48976481928e1f205210636717\"\u001b[39m,\n", + " type: \u001b[32m\"message\"\u001b[39m,\n", + " status: \u001b[32m\"completed\"\u001b[39m,\n", + " content: [\n", + " {\n", + " type: \u001b[32m\"output_text\"\u001b[39m,\n", + " annotations: [],\n", + " logprobs: [],\n", + " text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿En qué puedo ayudarte hoy? Puedo mostrar ejemplos de \\\\\"Hola mundo\\\\\" en distintos lenguajes, explicar conceptos de programación, ayudar con una tarea o sugerir recursos. Dime qué prefieres.\",\"suggested-topic\":\"El papel del programa \\\\\"Hola Mundo\\\\\" en la enseñanza de la programación: comparación práctica entre Python, Java y C (sintaxis, tiempo de arranque, facilidad de aprendizaje y percepción del estudiante).\"}'\u001b[39m\n", + " }\n", + " ],\n", + " role: \u001b[32m\"assistant\"\u001b[39m\n", + " }\n", + " ],\n", + " parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n", + " presence_penalty: \u001b[33m0\u001b[39m,\n", + " previous_response_id: \u001b[1mnull\u001b[22m,\n", + " prompt_cache_key: \u001b[1mnull\u001b[22m,\n", + " prompt_cache_retention: \u001b[1mnull\u001b[22m,\n", + " reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n", + " safety_identifier: \u001b[1mnull\u001b[22m,\n", + " service_tier: \u001b[32m\"default\"\u001b[39m,\n", + " store: \u001b[33mtrue\u001b[39m,\n", + " temperature: \u001b[33m1\u001b[39m,\n", + " text: {\n", + " format: {\n", + " type: \u001b[32m\"json_schema\"\u001b[39m,\n", + " description: \u001b[1mnull\u001b[22m,\n", + " name: \u001b[32m\"response_schema\"\u001b[39m,\n", + " schema: {\n", + " type: \u001b[32m\"object\"\u001b[39m,\n", + " properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n", + " required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n", + " additionalProperties: \u001b[33mfalse\u001b[39m\n", + " },\n", + " strict: \u001b[33mtrue\u001b[39m\n", + " },\n", + " verbosity: \u001b[32m\"medium\"\u001b[39m\n", + " },\n", + " tool_choice: \u001b[32m\"auto\"\u001b[39m,\n", + " tools: [],\n", + " top_logprobs: \u001b[33m0\u001b[39m,\n", + " top_p: \u001b[33m1\u001b[39m,\n", + " truncation: \u001b[32m\"disabled\"\u001b[39m,\n", + " usage: {\n", + " input_tokens: \u001b[33m167\u001b[39m,\n", + " input_tokens_details: { cached_tokens: \u001b[33m0\u001b[39m },\n", + " output_tokens: \u001b[33m482\u001b[39m,\n", + " output_tokens_details: { reasoning_tokens: \u001b[33m320\u001b[39m },\n", + " total_tokens: \u001b[33m649\u001b[39m\n", + " },\n", + " user: \u001b[1mnull\u001b[22m,\n", + " metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\n", + " output_text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿En qué puedo ayudarte hoy? Puedo mostrar ejemplos de \\\\\"Hola mundo\\\\\" en distintos lenguajes, explicar conceptos de programación, ayudar con una tarea o sugerir recursos. Dime qué prefieres.\",\"suggested-topic\":\"El papel del programa \\\\\"Hola Mundo\\\\\" en la enseñanza de la programación: comparación práctica entre Python, Java y C (sintaxis, tiempo de arranque, facilidad de aprendizaje y percepción del estudiante).\"}'\u001b[39m\n", + "}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "while (response.status === \"queued\" || response.status === \"in_progress\") {\n", + " console.log(\"Current status: \" + response.status);\n", + " await new Promise((resolve) => setTimeout(resolve, 2000)); // wait 2 seconds\n", + " response = await openai.responses.retrieve(response.id);\n", + "}\n", + "\n", + "response;\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebb67bd8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{ tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m }" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response.metadata;\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd18fc2d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " error: \u001b[1mnull\u001b[22m,\n", + " data: {\n", + " id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m,\n", + " carrera_id: \u001b[32m\"8208da08-d549-4359-8865-9d806bc54f19\"\u001b[39m,\n", + " estructura_id: \u001b[32m\"69fb2b77-5a95-47e0-bf1f-389d384200e4\"\u001b[39m,\n", + " nombre: \u001b[32m\"Ingeniearía en Cifbernética y Ciberseguridad (2025)\"\u001b[39m,\n", + " nivel: \u001b[32m\"Doctorado\"\u001b[39m,\n", + " tipo_ciclo: \u001b[32m\"Semestre\"\u001b[39m,\n", + " numero_ciclos: \u001b[33m8\u001b[39m,\n", + " datos: {\n", + " \u001b[32m\"ai-message\"\u001b[39m: \u001b[32m\"¡Hola! ¿Cómo puedo ayudarte hoy? Si te refieres al clásico “Hola mundo” en programación, puedo mostrarte ejemplos en varios lenguajes, explicar su historia o ayudarte a crear tu primer programa. ¿Qué prefieres?\"\u001b[39m,\n", + " \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[32m\"El uso del programa “Hola mundo” como herramienta pedagógica en la enseñanza de la programación: impacto en la comprensión inicial, variaciones por lenguaje y mejores prácticas didácticas.\"\u001b[39m\n", + " },\n", + " estado_actual_id: \u001b[32m\"18f49b67-8077-4371-be6e-2019a3be3562\"\u001b[39m,\n", + " activo: \u001b[33mtrue\u001b[39m,\n", + " tipo_origen: \u001b[32m\"IA\"\u001b[39m,\n", + " meta_origen: {},\n", + " creado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n", + " actualizado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n", + " creado_en: \u001b[32m\"2026-02-20T17:11:28.995222+00:00\"\u001b[39m,\n", + " actualizado_en: \u001b[32m\"2026-02-20T17:12:40.170995+00:00\"\u001b[39m,\n", + " nombre_search: \u001b[32m\"ingeniearia en cifbernetica y ciberseguridad (2025)\"\u001b[39m,\n", + " plan_hash: \u001b[32m\"70d28457bbddfe4f5cde0574\"\u001b[39m\n", + " },\n", + " count: \u001b[1mnull\u001b[22m,\n", + " status: \u001b[33m200\u001b[39m,\n", + " statusText: \u001b[32m\"OK\"\u001b[39m\n", + "}" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await supabase.from(response.metadata.tabla).update({\n", + " datos: JSON.parse(response.output_text),\n", + "}).eq(\"id\", response.metadata.id).select(\"*\").single();\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbbfd8c0", + "metadata": {}, + "outputs": [ + { + "ename": "Expected ';', '}' or at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~", + "evalue": "Expected ';', '}' or at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~", + "output_type": "error", + "traceback": [] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a00c79bd", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.9.2" } - ], - "source": [] }, - { - "cell_type": "code", - "execution_count": null, - "id": "a00c79bd", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Deno", - "language": "typescript", - "name": "deno" - }, - "language_info": { - "codemirror_mode": "typescript", - "file_extension": ".ts", - "mimetype": "text/x.typescript", - "name": "typescript", - "nbconvert_exporter": "script", - "pygments_lexer": "typescript", - "version": "5.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/notebooks/definitions-openai.ipynb b/notebooks/definitions-openai.ipynb index ae139c7..ee796f7 100644 --- a/notebooks/definitions-openai.ipynb +++ b/notebooks/definitions-openai.ipynb @@ -83,11 +83,13 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "1403c789", "metadata": {}, "outputs": [], "source": [ + "import { title } from \"node:process\";\n", + "\n", "const combinedSchema = {\n", " type: \"object\",\n", " $defs: {\n", @@ -126,10 +128,12 @@ " fabricante: { type: \"string\", maxLength: 100 },\n", "\n", " // Agregar campo del preset\n", - " perishables: { $ref: \"#/$defs/perishables\" },\n", - " origen: { $ref: \"#/$defs/origen\" },\n", + " perishables: {\n", + " $ref: \"#/$defs/perishables\",\n", + " \"x-column\": \"perishables\",\n", + " },\n", "\n", - " perecedero: { type: \"null\", \"x-column\": \"perishable\", const: null },\n", + " origen: { $ref: \"#/$defs/origen\" },\n", " },\n", " required: [\"nombre\", \"fabricante\", \"perishables\", \"origen\", \"perecedero\"],\n", " additionalProperties: false,\n", diff --git a/notebooks/embeddings-openai.ipynb b/notebooks/embeddings-openai.ipynb index d2f1083..5480cb1 100644 --- a/notebooks/embeddings-openai.ipynb +++ b/notebooks/embeddings-openai.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "902bff53", "metadata": {}, "outputs": [], @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "3b0acbc3", "metadata": {}, "outputs": [], @@ -32,63 +32,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 1, "id": "dae842ad", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " object: \"list\",\n", - " data: [\n", - " {\n", - " object: \"embedding\",\n", - " index: 0,\n", - " embedding: [\n", - " 0.017360063269734383, 0.022553985938429832, 0.025286488234996796,\n", - " -0.009742671623826027, -0.016438385471701622, -0.014725150540471077,\n", - " -0.020840751007199287, 0.02215278521180153, -0.02024437114596367,\n", - " 0.007834257557988167, 0.0029981620609760284, -0.04109596461057663,\n", - " 0.030165957286953926, -0.03402615711092949, 0.0008369643473997712,\n", - " 0.00044016868923790753, -0.027129843831062317, -0.04248390346765518,\n", - " -0.058683738112449646, 0.034655068069696426, 0.013738414272665977,\n", - " 0.02060219831764698, 0.016687780618667603, 0.0059637944214046,\n", - " -0.010306521318852901, 0.014898642897605896, -0.008826415985822678,\n", - " 0.042136918753385544, 0.009477011859416962, -0.03953453525900841,\n", - " -0.012198670767247677, -0.040055014193058014, -0.019409440457820892,\n", - " -0.0210576169192791, -0.0392959825694561, -0.005996324121952057,\n", - " -0.0022486215457320213, 0.05209103599190712, 0.0009325206046923995,\n", - " 0.013163721188902855, 0.0505296029150486, 0.014540815725922585,\n", - " 0.0027921402361243963, 0.03190087899565697, 0.00004917589103570208,\n", - " 0.03209605813026428, -0.038363464176654816, -0.02424553595483303,\n", - " 0.014096241444349289, 0.02559009939432144, -0.06089576333761215,\n", - " -0.011016755364835262, -0.026696112006902695, 0.050659723579883575,\n", - " 0.019561246037483215, 0.050833214074373245, 0.014529972337186337,\n", - " -0.023204581812024117, -0.014399852603673935, -0.009292676113545895,\n", - " 0.003149967873468995, 0.02007087878882885, 0.025503354147076607,\n", - " 0.0230094026774168, 0.04471761733293533, 0.01479020994156599,\n", - " -0.06692461669445038, 0.03591288626194, -0.037127330899238586,\n", - " 0.029558734968304634, -0.017576929181814194, 0.01953955926001072,\n", - " -0.027888871729373932, -0.0041095963679254055, 0.0004940461367368698,\n", - " -0.011266149580478668, -0.04200679808855057, 0.03537072241306305,\n", - " -0.0015058581484481692, -0.05551750585436821, -0.05287174880504608,\n", - " 0.016373327001929283, 0.005573437083512545, -0.01235047634691,\n", - " -0.009005329571664333, -0.034047845751047134, -0.018368486315011978,\n", - " -0.00271217105910182, 0.007731246296316385, -0.020981714129447937,\n", - " -0.040900785475969315, -0.03727913647890091, -0.059030722826719284,\n", - " -0.04536821320652962, 0.02257567271590233, 0.08579189330339432,\n", - " 0.014605875127017498, -0.030686434358358383, -0.0019084142986685038,\n", - " 0.02216362953186035,\n", - " ... 1436 more items\n", - " ]\n", - " }\n", - " ],\n", - " model: \"text-embedding-3-small\",\n", - " usage: { prompt_tokens: 13, total_tokens: 13 }\n", - "}\n", - "✅ Insertados textos con embeddings\n" + "ename": "ReferenceError", + "evalue": "openai is not defined", + "output_type": "error", + "traceback": [ + "Stack trace:", + "ReferenceError: openai is not defined", + " at :5:15" ] } ], @@ -120,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "2d8fa9a9", "metadata": {}, "outputs": [ @@ -129,20 +84,26 @@ "output_type": "stream", "text": [ "🔎 Consulta: ¿Cómo son buscados vectores?\n", - "\n", - "- Los vectores se usan con el cliente de OPENAI embeddings\n", - " Similaridad: 0.5639\n", - "\n", - "- Supabase permite almacenar vectores usando pgvector.\n", - " Similaridad: 0.4758\n", - "\n", - "- Los embeddings convierten texto en representaciones numéricas.\n", - " Similaridad: 0.3645\n", "\n" ] + }, + { + "data": { + "text/plain": [ + "\u001b[32m\"code: PGRST202\\n\"\u001b[39m +\n", + " \u001b[32m'details: \"Searched for the function public.buscar_documentos with parameters match_count, query_embedding or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.\"\\n'\u001b[39m +\n", + " \u001b[32m\"hint: Perhaps you meant to call the function public.unaccent\\n\"\u001b[39m +\n", + " \u001b[32m'message: \"Could not find the function public.buscar_documentos(match_count, query_embedding) in the schema cache\"'\u001b[39m" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ + "import { encode } from \"@toon-format/toon\";\n", + "\n", "const consulta = \"¿Cómo son buscados vectores?\";\n", "\n", "const emb = await openai.embeddings.create({\n", @@ -157,22 +118,42 @@ " match_count: 3,\n", "});\n", "\n", - "if (error) throw error;\n", + "/* if (error) throw error; */\n", "\n", "console.log(`🔎 Consulta: ${consulta}\\n`);\n", - "for (const r of data ?? []) {\n", - " console.log(`- ${r.contenido}`);\n", - " console.log(` Similaridad: ${Number(r.similarity).toFixed(4)}\\n`);\n", - "}\n" + "encode(error, {\n", + " indent: 2,\n", + " delimiter: \",\",\n", + " keyFolding: \"off\",\n", + " flattenDepth: Infinity,\n", + "});\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "791a94d8", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " code: \u001b[32m\"PGRST202\"\u001b[39m,\n", + " details: \u001b[32m\"Searched for the function public.buscar_documentos with parameters match_count, query_embedding or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.\"\u001b[39m,\n", + " hint: \u001b[32m\"Perhaps you meant to call the function public.unaccent\"\u001b[39m,\n", + " message: \u001b[32m\"Could not find the function public.buscar_documentos(match_count, query_embedding) in the schema cache\"\u001b[39m\n", + "}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "error" + ] } ], "metadata": { diff --git a/package.json b/package.json index a0f5f90..f5c50a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@supabase/supabase-js": "^2.90.1", + "@toon-format/toon": "^2.1.0", "deno": "^2.6.4", "openai": "^6.16.0", "supabase": "^2.72.6" diff --git a/supabase/functions/create-chat-conversation/index.ts b/supabase/functions/create-chat-conversation/index.ts index 6057960..92bcbef 100644 --- a/supabase/functions/create-chat-conversation/index.ts +++ b/supabase/functions/create-chat-conversation/index.ts @@ -123,81 +123,7 @@ app.post(`${prefix}/conversations`, async (c) => { } }); -/** - * GET /conversations/:conversation_plan_id/messages - * Lista mensajes (assistant/user) desde OpenAI - */ -app.get(`${prefix}/conversations/:id/messages`, async (c) => { - try { - /* const auth = c.req.header("authorization"); - await requireUser(auth); */ - const conversation_plan_id = c.req.param("id"); - assertUuid(conversation_plan_id, "conversation_plan_id"); - - const supabase = getSupabaseServiceClient(); - const openai = getOpenAI(); - - const { data: convRow, error } = await supabase - .from("conversaciones_plan") - .select("openai_conversation_id, estado") - .eq("id", conversation_plan_id) - .single(); - - if (error || !convRow) { - throw new HttpError( - 404, - "conversation_not_found", - "Conversación no encontrada", - error, - ); - } - if (convRow.estado === "ARCHIVADA") { - // si ya está archivada, devolvemos lo guardado - const { data: archived } = await supabase - .from("conversaciones_plan") - .select("conversacion_json") - .eq("id", conversation_plan_id) - .single(); - return withCors( - jsonResponse({ - source: "supabase", - items: archived?.conversacion_json ?? null, - }), - ); - } - - const items = await openai.conversations.items.list( - convRow.openai_conversation_id, - ); - - const conversacion = items.data - .filter((it: any) => - it.type === "message" && - (it.role === "assistant" || it.role === "user") - ) - .map((it: any) => { - const rawText = it.content.map((c: any) => c.text).join(""); - - let parsedContent; - - try { - parsedContent = JSON.parse(rawText); // 👈 aquí lo convertimos - } catch { - parsedContent = rawText; // si no es JSON, lo dejamos normal - } - - return { - role: it.role, - content: parsedContent, - }; - }); - - return withCors(jsonResponse({ source: "openai", items: conversacion })); - } catch (err) { - return withCors(handleErr(err)); - } -}); /** * POST /conversations/:conversation_plan_id/messages @@ -285,6 +211,11 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { "Excelente, actualmente tu plan de estudio tiene una redacción clara, pero podrías mejorar el perfil de ingreso para hacerlo más atractivo.", ], }, + "is_refusal": { + type: "boolean", + description: + "Indica si la respuesta es un refusal (es decir, la pregunta no tiene que ver con el plan de estudio)", + }, }, }, }, @@ -295,12 +226,62 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { } // Pedimos respuesta estructurada con responses.create - const schema = pickSchemaFields(definicion, body.campos); + const schema = pickSchemaFields(definicion, body.campos ?? []); const planForPrompt = safePlanForPrompt(plan); const model = CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO; const prompt = body.user_prompt ?? body.content; + // append message of the user to conversacion_json (which guarantees a JSONB default to '[]') + /** + * appended includes timestamp, user, prompt and fields (if any) + */ + + type AppendedMessage = { + timestamp: string; + user: string; + prompt: string; + fields?: string[]; + }; + + type AppendedResponse = { + timestamp: string; + user: "assistant"; + refusal: boolean; + message: string; + recommendations?: { + texto_mejora: string; + campo_afectado: string; + aplicada: false; + }; + }; + + type AppendedItem = AppendedMessage | AppendedResponse; + + let appended: AppendedItem = { + timestamp: new Date().toISOString(), + user: /* user.email ?? user.id ??*/ "unknown", + prompt, + fields: body.campos, + }; + + const { error: appendErr } = await supabase.rpc( + "append_conversacion_plan", + { + p_id: conversation_plan_id, + p_append: appended, + }, + ); + + if (appendErr) { + throw new HttpError( + 500, + "append_conversation_failed", + "No se pudo agregar el mensaje a la conversación", + appendErr, + ); + } + const resp = await openai.responses.create({ conversation: row.openai_conversation_id, model, @@ -322,6 +303,47 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { ], }); + const respuestaJSON = JSON.parse(resp.output_text ?? "{}"); + const refusal = respuestaJSON["is-refusal"] === true; + //remove the is-refusal field from respuestaJSON to avoid confusion + delete respuestaJSON["is-refusal"]; + + // Now an item with the assistant response and the structured data (if any) should be + appended = { + timestamp: new Date().toISOString(), + user: "assistant", + refusal, + // the ai-message field is the response + message: respuestaJSON?.["ai-message"] ?? "", + recommendations: resp.output_text + ? Object.entries(respuestaJSON).filter(([k]) => k !== "ai-message") + .map( + ([campo_afectado, texto_mejora]) => ({ + campo_afectado, + texto_mejora, + aplicada: false, + }), + ) + : undefined, + } as AppendedResponse; + + const { error: appendRespErr } = await supabase.rpc( + "append_conversacion_plan", + { + p_id: conversation_plan_id, + p_append: appended, + }, + ); + + if (appendRespErr) { + throw new HttpError( + 500, + "append_response_failed", + "No se pudo agregar la respuesta a la conversación", + appendRespErr, + ); + } + return withCors(jsonResponse({ ok: true, openai_response_id: resp.id, @@ -332,91 +354,7 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { } }); -/** - * DELETE /conversations/:conversation_plan_id/archive - * Guarda items en Supabase y elimina la conversación de OpenAI - */ -app.delete(`${prefix}/conversations/:id/archive`, async (c) => { - try { - /* const auth = c.req.header("authorization"); - await requireUser(auth); */ - const conversation_plan_id = c.req.param("id"); - assertUuid(conversation_plan_id, "conversation_plan_id"); - - const supabase = getSupabaseServiceClient(); - const openai = getOpenAI(); - - const { data: row, error } = await supabase - .from("conversaciones_plan") - .select("id, openai_conversation_id, estado") - .eq("id", conversation_plan_id) - .single(); - - if (error || !row) { - throw new HttpError( - 404, - "conversation_not_found", - "Conversación no encontrada", - error, - ); - } - - if (row.estado === "ARCHIVADA") { - return withCors(jsonResponse({ ok: true, already: true })); - } - - // Marcar estado - await supabase.from("conversaciones_plan") - .update({ estado: "ARCHIVANDO" }) - .eq("id", conversation_plan_id); - - // Descargar items de OpenAI - const items = await openai.conversations.items.list( - row.openai_conversation_id, - ); - - const conversacion = items.data.filter((it: any) => - it.type === "message" && (it.role === "assistant" || it.role === "user") - ).map((it: any) => ({ - role: it.role, - content: it.content.map((c: any) => c.text).join(""), - })); - - // Guardar y marcar como ARCHIVADA - const { error: upErr } = await supabase.from("conversaciones_plan") - .update({ - estado: "ARCHIVADA", - conversacion_json: conversacion, - }) - .eq("id", conversation_plan_id); - - if (upErr) { - throw new HttpError( - 500, - "archive_save_failed", - "No se pudo guardar el archivo en Supabase", - upErr, - ); - } - - // Borrar conversación en OpenAI (best effort) - try { - await openai.conversations.delete(row.openai_conversation_id); - } catch (delErr) { - // Queda archivada en Supabase, pero reportamos warning - return withCors(jsonResponse({ - ok: true, - warning: "Archivada en Supabase, pero no se pudo borrar en OpenAI", - details: String(delErr), - }, 200)); - } - - return withCors(jsonResponse({ ok: true })); - } catch (err) { - return withCors(handleErr(err)); - } -}); /** * Unknown routes diff --git a/supabase/functions/create-chat-conversation/lib/plan.ts b/supabase/functions/create-chat-conversation/lib/plan.ts index c79d6aa..07aa611 100644 --- a/supabase/functions/create-chat-conversation/lib/plan.ts +++ b/supabase/functions/create-chat-conversation/lib/plan.ts @@ -2,7 +2,7 @@ import { HttpError } from "./errors.ts"; export function pickSchemaFields( definicion: any, - campos?: string[], + campos: string[], ) { if (!definicion || definicion.type !== "object" || !definicion.properties) { return definicion; @@ -18,10 +18,15 @@ export function pickSchemaFields( "Listo: mejoré la redacción del perfil de ingreso y propuse un tema de investigación alineado al plan.", ], }, + "is-refusal": { + type: "boolean", + description: + "Indica si el plan fue rechazado por el modelo. En caso de ser true, se espera un mensaje de rechazo en `ai-message`.", + }, }, }; - let out = structuredClone(definicion); + const out = structuredClone(definicion); // Si piden campos, filtramos propiedades/required a esos campos const entries = Object.entries(out.properties).filter(([k]) => diff --git a/supabase/migrations/20260223212325_agregar_nombre_conversacion.sql b/supabase/migrations/20260223212325_agregar_nombre_conversacion.sql index 4fb2325..f016b5c 100644 --- a/supabase/migrations/20260223212325_agregar_nombre_conversacion.sql +++ b/supabase/migrations/20260223212325_agregar_nombre_conversacion.sql @@ -1,5 +1,3 @@ alter table "public"."conversaciones_plan" add column "nombre" text default ('Chat '::text || CURRENT_DATE); alter table "public"."conversaciones_plan" alter column "conversacion_json" set default '[]'::jsonb; - - diff --git a/supabase/migrations/20260224211719_agregar_conversaciones.sql b/supabase/migrations/20260224211719_agregar_conversaciones.sql new file mode 100644 index 0000000..4233c9a --- /dev/null +++ b/supabase/migrations/20260224211719_agregar_conversaciones.sql @@ -0,0 +1,21 @@ +CREATE OR REPLACE FUNCTION public.append_conversacion_asignatura(p_id uuid, p_append jsonb) + RETURNS void + LANGUAGE sql +AS $function$ + update conversaciones_asignatura + set conversacion_json = coalesce(conversacion_json, '[]'::jsonb) || p_append + where id = p_id; +$function$ +; + +CREATE OR REPLACE FUNCTION public.append_conversacion_plan(p_id uuid, p_append jsonb) + RETURNS void + LANGUAGE sql +AS $function$ + update conversaciones_plan + set conversacion_json = coalesce(conversacion_json, '[]'::jsonb) || p_append + where id = p_id; +$function$ +; + +