diff --git a/notebooks/conversations-openai.ipynb b/notebooks/conversations-openai.ipynb index 1a35c84..53ae90c 100644 --- a/notebooks/conversations-openai.ipynb +++ b/notebooks/conversations-openai.ipynb @@ -14,7 +14,23 @@ " export: true,\n", "});\n", "\n", - "const openai = new OpenAI();" + "const openai = new OpenAI();\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3430f0fd", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " const response = await openai.responses.create({\n", + " model: \"gpt-5-nano\",\n", + " input: \"Cuál es el sentido de la vida?\",\n", + " background: true,\n", + " });\n", + "}\n" ] }, { @@ -288,8 +304,8 @@ ], "source": [ "const { data: input } = await supabase.from(\"planes_estudio\")\n", - ".select(\"*, estructuras_plan (definicion)\").limit(1).single();\n", - "input" + " .select(\"*, estructuras_plan (definicion)\").limit(1).single();\n", + "input;\n" ] }, { @@ -320,16 +336,22 @@ " },\n", "\n", " items: [\n", - " { type: \"message\", role: \"system\", content: \"En caso de que te pidan algo que no tiene nada que ver con planes de estudio o asignatura responde con un refusal.\" },\n", + " {\n", + " type: \"message\",\n", + " role: \"system\",\n", + " content:\n", + " \"En caso de que te pidan algo que no tiene nada que ver con planes de estudio o asignatura responde con un refusal.\",\n", + " },\n", " ],\n", "});\n", "\n", - "const {data: conversationPlane} = await supabase.from(\"conversaciones_plan\").insert({\n", - " openai_conversation_id: openaiConversation.id,\n", - " plan_estudio_id: input.id,\n", - "}).select(\"id\").single();\n", + "const { data: conversationPlane } = await supabase.from(\"conversaciones_plan\")\n", + " .insert({\n", + " openai_conversation_id: openaiConversation.id,\n", + " plan_estudio_id: input.id,\n", + " }).select(\"id\").single();\n", "\n", - "conversationPlane" + "conversationPlane;\n" ] }, { @@ -373,14 +395,16 @@ ], "source": [ "// Archivar una conversación\n", - "const items = await openai.conversations.items.list(conversation.openai_conversation_id);\n", + "const items = await openai.conversations.items.list(\n", + " conversation.openai_conversation_id,\n", + ");\n", "await supabase.from(\"conversaciones_plan\").update({\n", " estado: \"ARCHIVANDO\",\n", " conversacion_json: items,\n", " openai_conversation_id: null,\n", "}).eq(\"id\", conversationPlane.id);\n", "\n", - "await openai.conversations.delete(conversation.openai_conversation_id);" + "await openai.conversations.delete(conversation.openai_conversation_id);\n" ] }, { @@ -450,10 +474,13 @@ ], "source": [ "// Listar mensajes dado una conversacion_plan.id\n", - "const { data: conversation } = await supabase.from(\"conversaciones_plan\").select(\"openai_conversation_id\").eq(\"id\", conversationPlane.id).single();\n", - "const items = await openai.conversations.items.list(conversation.openai_conversation_id);\n", + "const { data: conversation } = await supabase.from(\"conversaciones_plan\")\n", + " .select(\"openai_conversation_id\").eq(\"id\", conversationPlane.id).single();\n", + "const items = await openai.conversations.items.list(\n", + " conversation.openai_conversation_id,\n", + ");\n", "// items.data.filter((item) => item.type === \"message\" && [\"assistant\", \"user\"].includes(item.role))\n", - "JSON.stringify(items, null, 2)" + "JSON.stringify(items, null, 2);\n" ] }, { @@ -714,7 +741,7 @@ "const { data: respuesta, error } = await supabase.functions.invoke(\n", " `create-chat-conversation/conversations/${data.conversation_plan.id}/messages`,\n", " {\n", - " method: \"GET\"\n", + " method: \"GET\",\n", " },\n", ");\n", "\n", @@ -767,7 +794,7 @@ } ], "source": [ - "conversationToUpdate.openai_conversation_id" + "conversationToUpdate.openai_conversation_id;\n" ] }, { @@ -792,7 +819,10 @@ } ], "source": [ - "await openai.beta.threads.messages.retrieve('msg_057bd8aacc77d01f006994e942d98481938f35bf58dacf126d', {thread_id:conversationToUpdate.openai_conversation_id})" + "await openai.beta.threads.messages.retrieve(\n", + " \"msg_057bd8aacc77d01f006994e942d98481938f35bf58dacf126d\",\n", + " { thread_id: conversationToUpdate.openai_conversation_id },\n", + ");\n" ] }, { diff --git a/supabase/.temp/pooler-url b/supabase/.temp/pooler-url index 53f64a6..b8d7e58 100644 --- a/supabase/.temp/pooler-url +++ b/supabase/.temp/pooler-url @@ -1 +1 @@ -postgresql://postgres.exdkssurzmjnnhgtiama@aws-0-us-west-1.pooler.supabase.com:5432/postgres \ No newline at end of file +postgresql://postgres.bxkskdxwppdlplrkidcz@aws-1-us-east-2.pooler.supabase.com:5432/postgres \ No newline at end of file diff --git a/supabase/.temp/postgres-version b/supabase/.temp/postgres-version index ca1affb..3dc41f4 100644 --- a/supabase/.temp/postgres-version +++ b/supabase/.temp/postgres-version @@ -1 +1 @@ -15.8.1.085 \ No newline at end of file +17.6.1.063 \ No newline at end of file diff --git a/supabase/.temp/project-ref b/supabase/.temp/project-ref index a51b619..37a6228 100644 --- a/supabase/.temp/project-ref +++ b/supabase/.temp/project-ref @@ -1 +1 @@ -exdkssurzmjnnhgtiama \ No newline at end of file +bxkskdxwppdlplrkidcz \ No newline at end of file diff --git a/supabase/.temp/rest-version b/supabase/.temp/rest-version index 2392826..3520436 100644 --- a/supabase/.temp/rest-version +++ b/supabase/.temp/rest-version @@ -1 +1 @@ -v12.2.3 \ No newline at end of file +v14.1 \ No newline at end of file diff --git a/supabase/functions/_shared/utils.ts b/supabase/functions/_shared/utils.ts index 7516709..eb598fb 100644 --- a/supabase/functions/_shared/utils.ts +++ b/supabase/functions/_shared/utils.ts @@ -47,3 +47,8 @@ export function sendError( }, ); } + +export interface ResponseMetadata extends Record { + tabla?: string; + accion?: string; +} diff --git a/supabase/functions/openai-webhook-responses/deno.json b/supabase/functions/openai-webhook-responses/deno.json index 7ba9668..9012126 100644 --- a/supabase/functions/openai-webhook-responses/deno.json +++ b/supabase/functions/openai-webhook-responses/deno.json @@ -1,5 +1,6 @@ { "imports": { + "@supabase/supabase-js": "npm:@supabase/supabase-js@2", "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts", "express": "npm:express@5.2.1", "openai": "npm:openai@6.16.0" diff --git a/supabase/functions/openai-webhook-responses/index.ts b/supabase/functions/openai-webhook-responses/index.ts index 8d992e1..ad93451 100644 --- a/supabase/functions/openai-webhook-responses/index.ts +++ b/supabase/functions/openai-webhook-responses/index.ts @@ -4,45 +4,99 @@ // Setup type definitions for built-in Supabase Runtime APIs import "@supabase/functions-js/edge-runtime.d.ts"; -import express from "express"; import OpenAI from "openai"; +import { ResponseMetadata } from "../_shared/utils.ts"; +import { handlePlanesEstudioResponse } from "./planes_estudio/index.ts"; -const app = express(); +console.log("Starting OpenAI webhook responses function"); const client = new OpenAI({ webhookSecret: Deno.env.get("OPENAI_WEBHOOK_SECRET"), }); -app.use(express.text({ type: "application/json" })); +async function handleCompletedResponse( + event: OpenAI.Webhooks.ResponseCompletedWebhookEvent, +) { + const response_id = event.data.id; + const response = await client.responses.retrieve(response_id); + const metadata = response.metadata as ResponseMetadata | null; + + if (!metadata || !metadata.tabla) { + console.warn("No se recibió metadata o tabla en la respuesta"); + return; + } + + switch (metadata.tabla) { + case "planes_estudio": + handlePlanesEstudioResponse(response); + break; + default: + console.warn("Tabla no reconocida:", metadata.tabla); + } + + const output_text = response.output + .filter((item: any) => item.type === "message") + .flatMap((item: any) => item.content) + .filter((contentItem: any) => contentItem.type === "output_text") + .map((contentItem: any) => contentItem.text) + .join(""); + + console.log("Response output:", output_text); +} + +Deno.serve(async (req) => { + const url = new URL(req.url); + const functionName = url.pathname.split("/").pop(); + console.log( + `[${new Date().toISOString()}][${functionName}]: Request received`, + ); + + // Opcional: Solo permitir POST + if (req.method !== "POST") { + return new Response("Method Not Allowed", { status: 405 }); + } -app.post("/webhook", async (req: express.Request, res: express.Response) => { try { - const event = await client.webhooks.unwrap(req.body, req.headers); + // OpenAI requiere el body como texto crudo (string) para verificar la firma criptográfica + const payload = await req.text(); - if (event.type === "response.completed") { - const response_id = event.data.id; - const response = await client.responses.retrieve(response_id); - const output_text = response.output - .filter((item) => item.type === "message") - .flatMap((item) => item.content) - .filter((contentItem) => contentItem.type === "output_text") - .map((contentItem) => contentItem.text) - .join(""); + // Validar firma y parsear el evento. Deno maneja req.headers compatiblemente + const event = await client.webhooks.unwrap(payload, req.headers); - console.log("Response output:", output_text); + switch (event.type) { + case "response.completed": { + EdgeRuntime.waitUntil(handleCompletedResponse(event)); + break; + } + case "response.cancelled": { + console.warn("Response cancelled:", event.data); + break; + } + case "response.failed": { + console.error("Response failed:", event.data); + break; + } + case "response.incomplete": { + console.warn("Response incomplete:", event.data); + break; + } + default: { + throw new Error(`Unhandled event type: ${event.type}`); + } } - res.status(200).send(); + + console.log( + `[${ + new Date().toISOString() + }][${functionName}]: Request processed successfully`, + ); + return new Response("OK", { status: 200 }); } catch (error) { if (error instanceof OpenAI.InvalidWebhookSignatureError) { - console.error("Invalid signature", error); - res.status(400).send("Invalid signature"); + console.error("Invalid signature:", error.message); + return new Response("Invalid signature", { status: 400 }); } else { - throw error; + console.error("Internal Error:", error); + return new Response("Internal Server Error", { status: 500 }); } } }); - -Deno.serve(async (req) => { - console.log("Inicio de openai-webhook-responses"); - - return await app.handle(req); -}); diff --git a/supabase/functions/openai-webhook-responses/planes_estudio/crear.ts b/supabase/functions/openai-webhook-responses/planes_estudio/crear.ts new file mode 100644 index 0000000..67f613c --- /dev/null +++ b/supabase/functions/openai-webhook-responses/planes_estudio/crear.ts @@ -0,0 +1,6 @@ +import type { OpenAI } from "openai"; +import { supabase } from "../supabase.ts"; +import type { TablesUpdate } from "../../_shared/database.types.ts"; + +export function handleCrearPlanEstudio(response: OpenAI.Responses.Response) { +} diff --git a/supabase/functions/openai-webhook-responses/planes_estudio/index.ts b/supabase/functions/openai-webhook-responses/planes_estudio/index.ts new file mode 100644 index 0000000..f5e47f6 --- /dev/null +++ b/supabase/functions/openai-webhook-responses/planes_estudio/index.ts @@ -0,0 +1,26 @@ +import { ResponseMetadata } from "../../_shared/utils.ts"; +import type { OpenAI } from "openai"; + +export function handlePlanesEstudioResponse( + response: OpenAI.Responses.Response, +) { + const metadata = response.metadata as ResponseMetadata | null; + if (!metadata || !metadata.accion) { + console.warn("No se recibió acción en la metadata"); + return; + } + + switch (metadata.accion) { + case "crear": + // Lógica para crear un plan de estudio + break; + case "actualizar": + // Lógica para actualizar un plan de estudio + break; + case "eliminar": + // Lógica para eliminar un plan de estudio + break; + default: + console.warn("Acción no reconocida:", metadata.accion); + } +} diff --git a/supabase/functions/openai-webhook-responses/supabase.ts b/supabase/functions/openai-webhook-responses/supabase.ts new file mode 100644 index 0000000..7cb6c6a --- /dev/null +++ b/supabase/functions/openai-webhook-responses/supabase.ts @@ -0,0 +1,15 @@ +import { createClient } from "@supabase/supabase-js"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY"); + +if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { + console.warn("Missing Supabase environment variables"); +} + +const supabase = createClient( + SUPABASE_URL as string, + SUPABASE_ANON_KEY as string, +); + +export { supabase };