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$
+;
+
+