se agrega wh

This commit is contained in:
2026-03-06 11:06:56 -06:00
parent d844698ffc
commit 749ec7a962
5 changed files with 159 additions and 131 deletions
@@ -458,7 +458,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
const { data: estructura, error: estructuraError } = await supabaseService
.from("estructuras_asignatura")
.select("id,nombre,definicion,version")
.select("id,nombre,definicion")
.eq("id", resolved.estructura_id)
.single();
if (estructuraError) {
@@ -4,6 +4,7 @@ import { HttpError, jsonResponse } from "./lib/errors.ts";
import { getOpenAI } from "./lib/openai.ts";
import { getSupabaseServiceClient, requireUser } from "./lib/supabase.ts";
import { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields } from "./lib/plan.ts";
import { OpenAIService } from "../_shared/openai-service.ts";
type CreateBody = {
plan_estudio_id: string;
@@ -196,128 +197,81 @@ app.post(`${prefix}/conversations/plan/:id/messages`, async (c) => {
throw new HttpError(400, "bad_input", "content es requerido");
}
console.log("Iniciando generación en background para mensaje_id:");
const supabase = getSupabaseServiceClient();
const openai = getOpenAI();
const svc = OpenAIService.fromEnv();
// 1. Traer datos de la conversación y el plan
// 1. Validar existencia y estado de la conversación
const { data: row, error } = await supabase
.from("conversaciones_plan")
.select(
"id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))",
)
.select("id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))")
.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") {
throw new HttpError(409, "already_archived", "La conversación ya está archivada");
}
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
if (row.estado === "ARCHIVADA") throw new HttpError(409, "archived", "Conversación archivada");
const plan = (row as any).planes_estudio;
const definicion = plan?.estructuras_plan?.definicion;
const wantsStructured = !!definicion;
const isStructured = !!definicion;
// --- LÓGICA PARA CASO NO ESTRUCTURADO ---
if (!wantsStructured) {
const resp = await openai.responses.create({
conversation: row.openai_conversation_id,
model: CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
input: [
{
role: "system",
content: `Este es el plan de estudios actual ${JSON.stringify(plan)}. Si la pregunta no tiene que ver con el plan, responde con un refusal.`,
},
{ role: "user", content: body.content },
],
text: {
format: {
type: "json_schema",
name: "definicion",
schema: {
type: "object",
properties: {
"ai-message": { type: "string" },
"is_refusal": { type: "boolean" },
},
},
},
},
});
const respuestaJSON = JSON.parse(resp.output_text ?? "{}");
// Guardar en la nueva tabla aunque no sea estructurado
await supabase.from("plan_mensajes_ia").insert({
conversacion_plan_id,
enviado_por: "00000000-0000-0000-0000-000000000000",
// 2. Insertar el mensaje en estado PENDIENTE
// Guardamos los metadatos necesarios para procesar la respuesta después
const { data: mensajeInsertado, error: insertErr } = await supabase
.from("plan_mensajes_ia")
.insert({
conversacion_plan_id:conversation_plan_id,
enviado_por: "00000000-0000-0000-0000-000000000000",
mensaje: body.content,
respuesta: respuestaJSON?.["ai-message"] ?? "",
campos: [],
propuesta: { recommendations: [] },
is_refusal: !!respuestaJSON.is_refusal,
estado: "COMPLETADO",
});
campos: body.campos ?? [],
estado: "PROCESANDO", // Estado inicial
})
.select()
.single();
return withCors(jsonResponse({ ok: true }));
}
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
// --- LÓGICA PARA CASO ESTRUCTURADO ---
const schema = pickSchemaFields(definicion, body.campos ?? []);
const planForPrompt = safePlanForPrompt(plan);
const prompt = body.user_prompt ?? body.content;
// 3. Preparar Schema y Prompt
const schema = isStructured ? pickSchemaFields(definicion, body.campos ?? []) : {
type: "object",
properties: { "ai-message": { type: "string" }, "is_refusal": { type: "boolean" } }
};
const resp = await openai.responses.create({
// 4. Llamada asincrónica a OpenAI con Webhook
// Nota: El SDK de OpenAI permite pasar webhooks en ciertos modelos/endpoints
console.log("mandando a openaai ");
const aiResult = await svc.createStructuredResponse({
conversation: row.openai_conversation_id,
model: CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO,
text: { format: { type: "json_schema", name: "definicion", schema } },
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
background: true, // <--- ESTO ES LO QUE TE FALTABA
metadata: {
tabla: "plan_mensajes_ia",
mensaje_id: String(mensajeInsertado.id), // Siempre string
is_structured: String(isStructured)
},
text: {
format: {
type: "json_schema",
name: "definicion",
schema: schema
}
},
input: [
{
role: "system",
content: `Eres un asistente que ayuda a mejorar este plan: ${JSON.stringify(planForPrompt)}. Si la pregunta no es pertinente, responde con un refusal.`,
},
{ role: "user", content: prompt },
{ role: "system", content: `Asistente de plan: ${JSON.stringify(safePlanForPrompt(plan))}` },
{ role: "user", content: body.content },
],
});
const respuestaJSON = JSON.parse(resp.output_text ?? "{}");
const refusal = respuestaJSON["is-refusal"] === true;
delete respuestaJSON["is-refusal"];
// 2. Construir el objeto de propuestas/recomendaciones
const recommendations = resp.output_text
? Object.entries(respuestaJSON)
.filter(([k]) => k !== "ai-message")
.map(([campo_afectado, texto_mejora]) => ({
campo_afectado,
texto_mejora,
aplicada: false,
}))
: [];
// 3. Insertar ÚNICAMENTE en plan_mensajes_ia
const { error: insertErr } = await supabase
.from("plan_mensajes_ia")
.insert({
conversacion_plan_id: conversation_plan_id,
enviado_por: "00000000-0000-0000-0000-000000000000",
mensaje: prompt,
respuesta: respuestaJSON?.["ai-message"] ?? "",
campos: body.campos ?? [],
propuesta: { recommendations },
is_refusal: refusal,
estado: "COMPLETADO",
});
if (insertErr) {
throw new HttpError(500, "insert_plan_mensaje_failed", "Error al guardar mensaje", insertErr);
if (!aiResult.ok) {
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
}
// 5. Responder al cliente de inmediato
return withCors(jsonResponse({
ok: true,
openai_response_id: resp.id,
mensaje_id: mensajeInsertado.id,
openai_response_id: aiResult.responseId // Para seguimiento
}));
} catch (err) {
@@ -325,41 +279,10 @@ app.post(`${prefix}/conversations/plan/:id/messages`, async (c) => {
}
});
export async function handlePlanMensajesResponse(response: OpenAI.Responses.Response) {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { id: registroId } = response.metadata as { id: string };
const outputText = (response as any).output_text;
if (!outputText) return;
const rawJson = JSON.parse(outputText);
const isRefusal = !!rawJson.is_refusal;
const aiMessage = rawJson["ai-message"] || "";
// Construir las recomendaciones (tu lógica original)
const recommendations = Object.entries(rawJson)
.filter(([k]) => k !== "ai-message" && k !== "is_refusal")
.map(([campo_afectado, texto_mejora]) => ({
campo_afectado,
texto_mejora,
aplicada: false,
}));
// ACTUALIZAR EL MOLDE QUE DEJAMOS EN "GENERANDO"
await supabase
.from("plan_mensajes_ia")
.update({
respuesta: aiMessage,
propuesta: { recommendations },
is_refusal: isRefusal,
estado: "COMPLETADO" // Ahora el front ya puede mostrarlo
})
.eq("id", registroId);
}
app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
try {
@@ -0,0 +1,34 @@
// ./plan_mensajes_ia/index.ts
import { getSupabaseServiceClient } from "../_shared/supabase.ts";
export async function handlePlanMensajesResponse(response: any) {
const supabase = getSupabaseServiceClient();
const { mensaje_id, is_structured } = response.metadata;
// Extraer el texto de la respuesta (usando tu lógica de filtrado de output)
const outputText = response.output_text || ""; // Ajustar según cómo venga el objeto
const respuestaJSON = JSON.parse(outputText || "{}");
const is_refusal = !!respuestaJSON.is_refusal || respuestaJSON["is-refusal"] === true;
let recommendations = [];
if (is_structured) {
recommendations = Object.entries(respuestaJSON)
.filter(([k]) => k !== "ai-message" && k !== "is-refusal")
.map(([campo, valor]) => ({
campo_afectado: campo,
texto_mejora: valor,
aplicada: false,
}));
}
await supabase
.from("plan_mensajes_ia")
.update({
respuesta: respuestaJSON["ai-message"] || "",
propuesta: { recommendations },
is_refusal,
estado: "COMPLETADO",
})
.eq("id", mensaje_id);
}
@@ -8,6 +8,8 @@ import OpenAI from "openai";
import { ResponseMetadata } from "../_shared/utils.ts";
import { handlePlanesEstudioResponse } from "./planes_estudio/index.ts";
import { handleAsignaturasResponse } from "./asignaturas/index.ts";
import { handlePlanMensajesResponse } from "./planes_estudio/crear.ts";
console.log("Starting OpenAI webhook responses function");
const client = new OpenAI({
@@ -20,7 +22,8 @@ async function handleCompletedResponse(
const response_id = event.data.id;
const response = await client.responses.retrieve(response_id);
const metadata = response.metadata as ResponseMetadata | null;
console.log(("entre"));
if (!metadata || !metadata.tabla) {
console.warn("No se recibió metadata o tabla en la respuesta");
return;
@@ -33,6 +36,11 @@ async function handleCompletedResponse(
case "asignaturas":
await handleAsignaturasResponse(response);
break;
case "plan_mensajes_ia": // <-- Nueva tabla añadida
console.log("entre aqui");
await handlePlanMensajesResponse(response);
break;
default:
console.warn("Tabla no reconocida:", metadata.tabla);
}
@@ -106,3 +106,66 @@ export async function handleCrearPlanEstudio(
return;
}
}
export async function handlePlanMensajesResponse(
response: OpenAI.Responses.Response,
): Promise<void> {
const metadata = response.metadata as any;
const mensajeId = metadata?.mensaje_id;
console.log("ya entre aqui");
const isStructured = metadata?.is_structured === "true" || metadata?.is_structured === true;
if (!mensajeId) {
console.warn("No se recibió mensaje_id en la metadata del webhook");
return;
}
try {
const outputText = extractOutputText(response);
if (!outputText) {
throw new Error("La respuesta de OpenAI está vacía");
}
let respuestaJSON: any;
try {
respuestaJSON = JSON.parse(outputText);
} catch (e) {
throw new Error(`Error parseando JSON de OpenAI: ${e.message}`);
}
const is_refusal = !!respuestaJSON.is_refusal || respuestaJSON["is-refusal"] === true;
let recommendations = [];
if (isStructured && !is_refusal) {
recommendations = Object.entries(respuestaJSON)
.filter(([k]) => k !== "ai-message" && k !== "is-refusal" && k !== "is_refusal")
.map(([campo, valor]) => ({
campo_afectado: campo,
texto_mejora: valor,
aplicada: false,
}));
}
const { error } = await supabase
.from("plan_mensajes_ia")
.update({
respuesta: respuestaJSON["ai-message"] || "",
propuesta: { recommendations },
is_refusal,
estado: "COMPLETADO",
})
.eq("id", mensajeId);
if (error) {
throw error;
}
} catch (e) {
console.error("Error procesando handlePlanMensajesResponse:", { mensajeId, e });
// Opcional: Marcar el mensaje como fallido en la tabla si tienes ese estado
await supabase
.from("plan_mensajes_ia")
.update({ estado: "ERROR" })
.eq("id", mensajeId);
}
}