Merge branch 'main' into issue/35-crear-webhook-para-responses

This commit is contained in:
2026-02-27 12:14:04 -06:00
9 changed files with 753 additions and 799 deletions
Generated
+6 -2
View File
@@ -21,6 +21,7 @@
"jsr:@zod/zod@^4.3.5": "4.3.5", "jsr:@zod/zod@^4.3.5": "4.3.5",
"npm:@supabase/supabase-js@2": "2.90.1", "npm:@supabase/supabase-js@2": "2.90.1",
"npm:@supabase/supabase-js@^2.90.1": "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:@types/bun@^1.3.5": "1.3.5",
"npm:deno@^2.6.4": "2.6.4", "npm:deno@^2.6.4": "2.6.4",
"npm:openai@6.16.0": "6.16.0_zod@3.25.76", "npm:openai@6.16.0": "6.16.0_zod@3.25.76",
@@ -186,6 +187,9 @@
"@supabase/storage-js" "@supabase/storage-js"
] ]
}, },
"@toon-format/toon@2.1.0": {
"integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="
},
"@types/bun@1.3.5": { "@types/bun@1.3.5": {
"integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==", "integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==",
"dependencies": [ "dependencies": [
@@ -519,8 +523,7 @@
"minipass", "minipass",
"minizlib", "minizlib",
"yallist" "yallist"
], ]
"deprecated": true
}, },
"tr46@0.0.3": { "tr46@0.0.3": {
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
@@ -591,6 +594,7 @@
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@supabase/supabase-js@^2.90.1", "npm:@supabase/supabase-js@^2.90.1",
"npm:@toon-format/toon@^2.1.0",
"npm:@types/bun@^1.3.5", "npm:@types/bun@^1.3.5",
"npm:deno@^2.6.4", "npm:deno@^2.6.4",
"npm:openai@^6.16.0", "npm:openai@^6.16.0",
+27 -25
View File
@@ -195,7 +195,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 19, "execution_count": 7,
"id": "26b3bb36", "id": "26b3bb36",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -225,10 +225,18 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"\u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m" "{\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 <anonymous>:2:31\"\u001b[39m,\n",
" hint: \u001b[32m\"\"\u001b[39m,\n",
" code: \u001b[32m\"\"\u001b[39m\n",
"}"
] ]
}, },
"execution_count": 21, "execution_count": 9,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -247,9 +255,9 @@
"data": { "data": {
"text/plain": [ "text/plain": [
"{\n", "{\n",
" id: \u001b[32m\"resp_0f1906be7e83879e00699895d7f1cc8193a960e6ccdab1d377\"\u001b[39m,\n", " id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n",
" object: \u001b[32m\"response\"\u001b[39m,\n", " object: \u001b[32m\"response\"\u001b[39m,\n",
" created_at: \u001b[33m1771607511\u001b[39m,\n", " created_at: \u001b[33m1771867714\u001b[39m,\n",
" status: \u001b[32m\"queued\"\u001b[39m,\n", " status: \u001b[32m\"queued\"\u001b[39m,\n",
" background: \u001b[33mtrue\u001b[39m,\n", " background: \u001b[33mtrue\u001b[39m,\n",
" completed_at: \u001b[1mnull\u001b[22m,\n", " completed_at: \u001b[1mnull\u001b[22m,\n",
@@ -293,15 +301,12 @@
" truncation: \u001b[32m\"disabled\"\u001b[39m,\n", " truncation: \u001b[32m\"disabled\"\u001b[39m,\n",
" usage: \u001b[1mnull\u001b[22m,\n", " usage: \u001b[1mnull\u001b[22m,\n",
" user: \u001b[1mnull\u001b[22m,\n", " user: \u001b[1mnull\u001b[22m,\n",
" metadata: {\n", " metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\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", " output_text: \u001b[32m\"\"\u001b[39m\n",
"}" "}"
] ]
}, },
"execution_count": 22, "execution_count": 12,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -312,7 +317,7 @@
" input: \"Hola mundo\",\n", " input: \"Hola mundo\",\n",
" metadata: {\n", " metadata: {\n",
" tabla: \"planes_estudio\",\n", " tabla: \"planes_estudio\",\n",
" id: plan.id,\n", " id: '2',\n",
" },\n", " },\n",
" text: {\n", " text: {\n",
" format: {\n", " format: {\n",
@@ -362,13 +367,13 @@
"data": { "data": {
"text/plain": [ "text/plain": [
"{\n", "{\n",
" id: \u001b[32m\"resp_0f1906be7e83879e00699895d7f1cc8193a960e6ccdab1d377\"\u001b[39m,\n", " id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n",
" object: \u001b[32m\"response\"\u001b[39m,\n", " object: \u001b[32m\"response\"\u001b[39m,\n",
" created_at: \u001b[33m1771607511\u001b[39m,\n", " created_at: \u001b[33m1771867714\u001b[39m,\n",
" status: \u001b[32m\"completed\"\u001b[39m,\n", " status: \u001b[32m\"completed\"\u001b[39m,\n",
" background: \u001b[33mtrue\u001b[39m,\n", " background: \u001b[33mtrue\u001b[39m,\n",
" billing: { payer: \u001b[32m\"developer\"\u001b[39m },\n", " billing: { payer: \u001b[32m\"developer\"\u001b[39m },\n",
" completed_at: \u001b[33m1771607519\u001b[39m,\n", " completed_at: \u001b[33m1771867721\u001b[39m,\n",
" error: \u001b[1mnull\u001b[22m,\n", " error: \u001b[1mnull\u001b[22m,\n",
" frequency_penalty: \u001b[33m0\u001b[39m,\n", " frequency_penalty: \u001b[33m0\u001b[39m,\n",
" incomplete_details: \u001b[1mnull\u001b[22m,\n", " incomplete_details: \u001b[1mnull\u001b[22m,\n",
@@ -378,12 +383,12 @@
" model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n", " model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n",
" output: [\n", " output: [\n",
" {\n", " {\n",
" id: \u001b[32m\"rs_0f1906be7e83879e00699895d976548193ad40e9aae0b1b4f2\"\u001b[39m,\n", " id: \u001b[32m\"rs_0ae72cbd0894279200699c8e43cd4c8192bcbc70792b6aa621\"\u001b[39m,\n",
" type: \u001b[32m\"reasoning\"\u001b[39m,\n", " type: \u001b[32m\"reasoning\"\u001b[39m,\n",
" summary: []\n", " summary: []\n",
" },\n", " },\n",
" {\n", " {\n",
" id: \u001b[32m\"msg_0f1906be7e83879e00699895ddf62881938811097ba62e8724\"\u001b[39m,\n", " id: \u001b[32m\"msg_0ae72cbd0894279200699c8e48976481928e1f205210636717\"\u001b[39m,\n",
" type: \u001b[32m\"message\"\u001b[39m,\n", " type: \u001b[32m\"message\"\u001b[39m,\n",
" status: \u001b[32m\"completed\"\u001b[39m,\n", " status: \u001b[32m\"completed\"\u001b[39m,\n",
" content: [\n", " content: [\n",
@@ -391,7 +396,7 @@
" type: \u001b[32m\"output_text\"\u001b[39m,\n", " type: \u001b[32m\"output_text\"\u001b[39m,\n",
" annotations: [],\n", " annotations: [],\n",
" logprobs: [],\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", " 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",
" ],\n", " ],\n",
" role: \u001b[32m\"assistant\"\u001b[39m\n", " role: \u001b[32m\"assistant\"\u001b[39m\n",
@@ -430,20 +435,17 @@
" usage: {\n", " usage: {\n",
" input_tokens: \u001b[33m167\u001b[39m,\n", " input_tokens: \u001b[33m167\u001b[39m,\n",
" input_tokens_details: { cached_tokens: \u001b[33m0\u001b[39m },\n", " input_tokens_details: { cached_tokens: \u001b[33m0\u001b[39m },\n",
" output_tokens: \u001b[33m437\u001b[39m,\n", " output_tokens: \u001b[33m482\u001b[39m,\n",
" output_tokens_details: { reasoning_tokens: \u001b[33m320\u001b[39m },\n", " output_tokens_details: { reasoning_tokens: \u001b[33m320\u001b[39m },\n",
" total_tokens: \u001b[33m604\u001b[39m\n", " total_tokens: \u001b[33m649\u001b[39m\n",
" },\n", " },\n",
" user: \u001b[1mnull\u001b[22m,\n", " user: \u001b[1mnull\u001b[22m,\n",
" metadata: {\n", " metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\n",
" tabla: \u001b[32m\"planes_estudio\"\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",
" 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",
"}" "}"
] ]
}, },
"execution_count": 23, "execution_count": 13,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
+8 -4
View File
@@ -83,11 +83,13 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 26, "execution_count": null,
"id": "1403c789", "id": "1403c789",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"import { title } from \"node:process\";\n",
"\n",
"const combinedSchema = {\n", "const combinedSchema = {\n",
" type: \"object\",\n", " type: \"object\",\n",
" $defs: {\n", " $defs: {\n",
@@ -126,10 +128,12 @@
" fabricante: { type: \"string\", maxLength: 100 },\n", " fabricante: { type: \"string\", maxLength: 100 },\n",
"\n", "\n",
" // Agregar campo del preset\n", " // Agregar campo del preset\n",
" perishables: { $ref: \"#/$defs/perishables\" },\n", " perishables: {\n",
" origen: { $ref: \"#/$defs/origen\" },\n", " $ref: \"#/$defs/perishables\",\n",
" \"x-column\": \"perishables\",\n",
" },\n",
"\n", "\n",
" perecedero: { type: \"null\", \"x-column\": \"perishable\", const: null },\n", " origen: { $ref: \"#/$defs/origen\" },\n",
" },\n", " },\n",
" required: [\"nombre\", \"fabricante\", \"perishables\", \"origen\", \"perecedero\"],\n", " required: [\"nombre\", \"fabricante\", \"perishables\", \"origen\", \"perecedero\"],\n",
" additionalProperties: false,\n", " additionalProperties: false,\n",
+54 -73
View File
@@ -2,7 +2,7 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 2,
"id": "902bff53", "id": "902bff53",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -19,7 +19,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 3,
"id": "3b0acbc3", "id": "3b0acbc3",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -32,63 +32,18 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 1,
"id": "dae842ad", "id": "dae842ad",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "ename": "ReferenceError",
"output_type": "stream", "evalue": "openai is not defined",
"text": [ "output_type": "error",
"{\n", "traceback": [
" object: \"list\",\n", "Stack trace:",
" data: [\n", "ReferenceError: openai is not defined",
" {\n", " at <anonymous>:5:15"
" 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"
] ]
} }
], ],
@@ -120,7 +75,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 12,
"id": "2d8fa9a9", "id": "2d8fa9a9",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -129,20 +84,26 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"🔎 Consulta: ¿Cómo son buscados vectores?\n", "🔎 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" "\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": [ "source": [
"import { encode } from \"@toon-format/toon\";\n",
"\n",
"const consulta = \"¿Cómo son buscados vectores?\";\n", "const consulta = \"¿Cómo son buscados vectores?\";\n",
"\n", "\n",
"const emb = await openai.embeddings.create({\n", "const emb = await openai.embeddings.create({\n",
@@ -157,22 +118,42 @@
" match_count: 3,\n", " match_count: 3,\n",
"});\n", "});\n",
"\n", "\n",
"if (error) throw error;\n", "/* if (error) throw error; */\n",
"\n", "\n",
"console.log(`🔎 Consulta: ${consulta}\\n`);\n", "console.log(`🔎 Consulta: ${consulta}\\n`);\n",
"for (const r of data ?? []) {\n", "encode(error, {\n",
" console.log(`- ${r.contenido}`);\n", " indent: 2,\n",
" console.log(` Similaridad: ${Number(r.similarity).toFixed(4)}\\n`);\n", " delimiter: \",\",\n",
"}\n" " keyFolding: \"off\",\n",
" flattenDepth: Infinity,\n",
"});\n"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 10,
"id": "791a94d8", "id": "791a94d8",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [
"source": [] {
"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": { "metadata": {
+1
View File
@@ -1,6 +1,7 @@
{ {
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.90.1", "@supabase/supabase-js": "^2.90.1",
"@toon-format/toon": "^2.1.0",
"deno": "^2.6.4", "deno": "^2.6.4",
"openai": "^6.16.0", "openai": "^6.16.0",
"supabase": "^2.72.6" "supabase": "^2.72.6"
@@ -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 * 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.", "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 // Pedimos respuesta estructurada con responses.create
const schema = pickSchemaFields(definicion, body.campos); const schema = pickSchemaFields(definicion, body.campos ?? []);
const planForPrompt = safePlanForPrompt(plan); const planForPrompt = safePlanForPrompt(plan);
const model = CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO; const model = CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO;
const prompt = body.user_prompt ?? body.content; 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({ const resp = await openai.responses.create({
conversation: row.openai_conversation_id, conversation: row.openai_conversation_id,
model, 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({ return withCors(jsonResponse({
ok: true, ok: true,
openai_response_id: resp.id, 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 * Unknown routes
@@ -2,7 +2,7 @@ import { HttpError } from "./errors.ts";
export function pickSchemaFields( export function pickSchemaFields(
definicion: any, definicion: any,
campos?: string[], campos: string[],
) { ) {
if (!definicion || definicion.type !== "object" || !definicion.properties) { if (!definicion || definicion.type !== "object" || !definicion.properties) {
return definicion; 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.", "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 // Si piden campos, filtramos propiedades/required a esos campos
const entries = Object.entries(out.properties).filter(([k]) => const entries = Object.entries(out.properties).filter(([k]) =>
@@ -1,5 +1,3 @@
alter table "public"."conversaciones_plan" add column "nombre" text default ('Chat '::text || CURRENT_DATE); 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; alter table "public"."conversaciones_plan" alter column "conversacion_json" set default '[]'::jsonb;
@@ -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$
;