Creación de planes y asignaturas en segundo plano y con webhook de openai #40
@@ -14,7 +14,23 @@
|
|||||||
" export: true,\n",
|
" export: true,\n",
|
||||||
"});\n",
|
"});\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": [
|
"source": [
|
||||||
"const { data: input } = await supabase.from(\"planes_estudio\")\n",
|
"const { data: input } = await supabase.from(\"planes_estudio\")\n",
|
||||||
".select(\"*, estructuras_plan (definicion)\").limit(1).single();\n",
|
" .select(\"*, estructuras_plan (definicion)\").limit(1).single();\n",
|
||||||
"input"
|
"input;\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -320,16 +336,22 @@
|
|||||||
" },\n",
|
" },\n",
|
||||||
"\n",
|
"\n",
|
||||||
" items: [\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",
|
"});\n",
|
||||||
"\n",
|
"\n",
|
||||||
"const {data: conversationPlane} = await supabase.from(\"conversaciones_plan\").insert({\n",
|
"const { data: conversationPlane } = await supabase.from(\"conversaciones_plan\")\n",
|
||||||
|
" .insert({\n",
|
||||||
" openai_conversation_id: openaiConversation.id,\n",
|
" openai_conversation_id: openaiConversation.id,\n",
|
||||||
" plan_estudio_id: input.id,\n",
|
" plan_estudio_id: input.id,\n",
|
||||||
"}).select(\"id\").single();\n",
|
" }).select(\"id\").single();\n",
|
||||||
"\n",
|
"\n",
|
||||||
"conversationPlane"
|
"conversationPlane;\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -373,14 +395,16 @@
|
|||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"// Archivar una conversación\n",
|
"// 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",
|
"await supabase.from(\"conversaciones_plan\").update({\n",
|
||||||
" estado: \"ARCHIVANDO\",\n",
|
" estado: \"ARCHIVANDO\",\n",
|
||||||
" conversacion_json: items,\n",
|
" conversacion_json: items,\n",
|
||||||
" openai_conversation_id: null,\n",
|
" openai_conversation_id: null,\n",
|
||||||
"}).eq(\"id\", conversationPlane.id);\n",
|
"}).eq(\"id\", conversationPlane.id);\n",
|
||||||
"\n",
|
"\n",
|
||||||
"await openai.conversations.delete(conversation.openai_conversation_id);"
|
"await openai.conversations.delete(conversation.openai_conversation_id);\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -450,10 +474,13 @@
|
|||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"// Listar mensajes dado una conversacion_plan.id\n",
|
"// 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 { data: conversation } = await supabase.from(\"conversaciones_plan\")\n",
|
||||||
"const items = await openai.conversations.items.list(conversation.openai_conversation_id);\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",
|
"// 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",
|
"const { data: respuesta, error } = await supabase.functions.invoke(\n",
|
||||||
" `create-chat-conversation/conversations/${data.conversation_plan.id}/messages`,\n",
|
" `create-chat-conversation/conversations/${data.conversation_plan.id}/messages`,\n",
|
||||||
" {\n",
|
" {\n",
|
||||||
" method: \"GET\"\n",
|
" method: \"GET\",\n",
|
||||||
" },\n",
|
" },\n",
|
||||||
");\n",
|
");\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -767,7 +794,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"conversationToUpdate.openai_conversation_id"
|
"conversationToUpdate.openai_conversation_id;\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -792,7 +819,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
postgresql://postgres.exdkssurzmjnnhgtiama@aws-0-us-west-1.pooler.supabase.com:5432/postgres
|
postgresql://postgres.bxkskdxwppdlplrkidcz@aws-1-us-east-2.pooler.supabase.com:5432/postgres
|
||||||
@@ -1 +1 @@
|
|||||||
15.8.1.085
|
17.6.1.063
|
||||||
@@ -1 +1 @@
|
|||||||
exdkssurzmjnnhgtiama
|
bxkskdxwppdlplrkidcz
|
||||||
@@ -1 +1 @@
|
|||||||
v12.2.3
|
v14.1
|
||||||
@@ -47,3 +47,8 @@ export function sendError(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResponseMetadata extends Record<string, string | undefined> {
|
||||||
|
tabla?: string;
|
||||||
|
accion?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"imports": {
|
"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",
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts",
|
||||||
"express": "npm:express@5.2.1",
|
"express": "npm:express@5.2.1",
|
||||||
"openai": "npm:openai@6.16.0"
|
"openai": "npm:openai@6.16.0"
|
||||||
|
|||||||
@@ -4,45 +4,99 @@
|
|||||||
|
|
||||||
// Setup type definitions for built-in Supabase Runtime APIs
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import express from "express";
|
|
||||||
import OpenAI from "openai";
|
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({
|
const client = new OpenAI({
|
||||||
webhookSecret: Deno.env.get("OPENAI_WEBHOOK_SECRET"),
|
webhookSecret: Deno.env.get("OPENAI_WEBHOOK_SECRET"),
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.text({ type: "application/json" }));
|
async function handleCompletedResponse(
|
||||||
|
event: OpenAI.Webhooks.ResponseCompletedWebhookEvent,
|
||||||
app.post("/webhook", async (req: express.Request, res: express.Response) => {
|
) {
|
||||||
try {
|
|
||||||
const event = await client.webhooks.unwrap(req.body, req.headers);
|
|
||||||
|
|
||||||
if (event.type === "response.completed") {
|
|
||||||
const response_id = event.data.id;
|
const response_id = event.data.id;
|
||||||
const response = await client.responses.retrieve(response_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
|
const output_text = response.output
|
||||||
.filter((item) => item.type === "message")
|
.filter((item: any) => item.type === "message")
|
||||||
.flatMap((item) => item.content)
|
.flatMap((item: any) => item.content)
|
||||||
.filter((contentItem) => contentItem.type === "output_text")
|
.filter((contentItem: any) => contentItem.type === "output_text")
|
||||||
.map((contentItem) => contentItem.text)
|
.map((contentItem: any) => contentItem.text)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
console.log("Response output:", output_text);
|
console.log("Response output:", output_text);
|
||||||
}
|
}
|
||||||
res.status(200).send();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof OpenAI.InvalidWebhookSignatureError) {
|
|
||||||
console.error("Invalid signature", error);
|
|
||||||
res.status(400).send("Invalid signature");
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
Deno.serve(async (req) => {
|
||||||
console.log("Inicio de openai-webhook-responses");
|
const url = new URL(req.url);
|
||||||
|
const functionName = url.pathname.split("/").pop();
|
||||||
|
console.log(
|
||||||
|
`[${new Date().toISOString()}][${functionName}]: Request received`,
|
||||||
|
);
|
||||||
|
|
||||||
return await app.handle(req);
|
// Opcional: Solo permitir POST
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// OpenAI requiere el body como texto crudo (string) para verificar la firma criptográfica
|
||||||
|
const payload = await req.text();
|
||||||
|
|
||||||
|
// Validar firma y parsear el evento. Deno maneja req.headers compatiblemente
|
||||||
|
const event = await client.webhooks.unwrap(payload, req.headers);
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.message);
|
||||||
|
return new Response("Invalid signature", { status: 400 });
|
||||||
|
} else {
|
||||||
|
console.error("Internal Error:", error);
|
||||||
|
return new Response("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user