From 3759f7673a59369a82516a21ac19a0372fb429b4 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Wed, 4 Mar 2026 09:13:16 -0600 Subject: [PATCH] Corregir Edge Function fix #36 --- .../create-chat-conversation/index.ts | 272 +++++++++++++++++- 1 file changed, 258 insertions(+), 14 deletions(-) diff --git a/supabase/functions/create-chat-conversation/index.ts b/supabase/functions/create-chat-conversation/index.ts index 92bcbef..4c4cb34 100644 --- a/supabase/functions/create-chat-conversation/index.ts +++ b/supabase/functions/create-chat-conversation/index.ts @@ -50,7 +50,7 @@ app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true }))); * POST /conversations * Crea conversación OpenAI + registro en conversaciones_plan */ -app.post(`${prefix}/conversations`, async (c) => { +app.post(`${prefix}/plan/conversations`, async (c) => { try { /* const auth = c.req.header("authorization"); const user = await requireUser(auth); */ @@ -122,6 +122,77 @@ app.post(`${prefix}/conversations`, async (c) => { return withCors(handleErr(err)); } }); +app.post(`${prefix}/asignatura/conversations`, async (c) => { + try { + const body = (await c.req.json().catch(() => ({}))) as Partial; + + const asignatura_id = body.asignatura_id; + assertUuid(asignatura_id ?? "", "asignatura_id"); + + const instanciador = body.instanciador ?? "unknown"; + + const system_prompt = + body.system_prompt ?? + "En caso de que te pidan algo que no tiene nada que ver con la asignatura responde con un refusal."; + + const supabase = getSupabaseServiceClient(); + const openai = getOpenAI(); + + // 🔥 Cargar asignatura + const { data: asignatura, error: asigErr } = await supabase + .from("asignaturas") + .select("*") + .eq("id", asignatura_id) + .single(); + + if (asigErr || !asignatura) { + throw new HttpError( + 404, + "asignatura_not_found", + "Asignatura no encontrada", + asigErr, + ); + } + + // 🔥 Crear conversación en OpenAI + const conv = await openai.conversations.create({ + metadata: { + tabla: "asignaturas", + id: asignatura.id, + instanciador, + }, + items: [{ type: "message", role: "system", content: system_prompt }], + }); + + // 🔥 Insertar en conversaciones_asignatura + const { data: row, error: insErr } = await supabase + .from("conversaciones_asignatura") + .insert({ + openai_conversation_id: conv.id, + asignatura_id: asignatura.id, // ✅ CORRECTO + estado: "ACTIVA", + }) + .select("id, asignatura_id, openai_conversation_id, estado") + .single(); + + if (insErr || !row) { + try { + await openai.conversations.delete(conv.id); + } catch (_) {} + + throw new HttpError( + 500, + "db_insert_failed", + "No se pudo registrar la conversación", + insErr, + ); + } + + return withCors(jsonResponse({ conversation_asignatura: row }, 201)); + } catch (err) { + return withCors(handleErr(err)); + } +}); @@ -129,7 +200,145 @@ app.post(`${prefix}/conversations`, async (c) => { * POST /conversations/:conversation_plan_id/messages * Agrega mensaje y opcionalmente solicita respuesta estructurada (json_schema) */ -app.post(`${prefix}/conversations/:id/messages`, async (c) => { +app.post(`${prefix}/conversations/plan/:id/messages`, async (c) => { + try { + const conversation_plan_id = c.req.param("id"); + assertUuid(conversation_plan_id, "conversation_plan_id"); + + const body = (await c.req.json().catch(() => ({}))) as Partial; + if (!body.content || typeof body.content !== "string") { + throw new HttpError(400, "bad_input", "content es requerido"); + } + + const supabase = getSupabaseServiceClient(); + const openai = getOpenAI(); + + // 1. Traer datos de la conversación y el plan + const { data: row, error } = await supabase + .from("conversaciones_plan") + .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"); + } + + const plan = (row as any).planes_estudio; + const definicion = plan?.estructuras_plan?.definicion; + const wantsStructured = !!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", + mensaje: body.content, + respuesta: respuestaJSON?.["ai-message"] ?? "", + campos: [], + propuesta: { recommendations: [] }, + is_refusal: !!respuestaJSON.is_refusal, + estado: "COMPLETADO", + }); + + return withCors(jsonResponse({ ok: true })); + } + + // --- LÓGICA PARA CASO ESTRUCTURADO --- + const schema = pickSchemaFields(definicion, body.campos ?? []); + const planForPrompt = safePlanForPrompt(plan); + const prompt = body.user_prompt ?? body.content; + + const resp = await openai.responses.create({ + conversation: row.openai_conversation_id, + model: CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO, + text: { format: { type: "json_schema", name: "definicion", 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 }, + ], + }); + + 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); + } + + return withCors(jsonResponse({ + ok: true, + openai_response_id: resp.id, + })); + + } catch (err) { + return withCors(handleErr(err)); + } +}); +app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => { try { /* const auth = c.req.header("authorization"); const user = await requireUser(auth); */ @@ -149,12 +358,12 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { // Traer conversacion + plan + estructura const { data: row, error } = await supabase - .from("conversaciones_plan") - .select( - "id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))", - ) - .eq("id", conversation_plan_id) - .single(); + .from("conversaciones_asignatura") + .select( + "id, openai_conversation_id, asignatura_id, estado, asignaturas(*)" + ) + .eq("id", conversation_plan_id) + .single(); if (error || !row) { throw new HttpError( @@ -266,7 +475,7 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { }; const { error: appendErr } = await supabase.rpc( - "append_conversacion_plan", + "append_conversacion_asignatura", { p_id: conversation_plan_id, p_append: appended, @@ -327,13 +536,48 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => { : undefined, } as AppendedResponse; - const { error: appendRespErr } = await supabase.rpc( - "append_conversacion_plan", - { + const { error: appendRespErr } = await supabase.rpc("append_conversacion_asignatura", { p_id: conversation_plan_id, p_append: appended, - }, - ); + }); + + // Construir propuesta estructurada + const propuesta = { + prompt, + respuesta: 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, + })) + : [], + }; + + // Insertar en plan_mensajes_ia + const { error: insertErr } = await supabase + .from("asignatura_mensajes_ia") + .insert({ + conversacion_asignatura_id: conversation_plan_id, + enviado_por: "00000000-0000-0000-0000-000000000000", + mensaje: prompt, + respuesta: respuestaJSON?.["ai-message"] ?? "", + campos: body.campos ?? [], + propuesta, + is_refusal: refusal, + estado: "COMPLETADO", + }); + + if (insertErr) { + throw new HttpError( + 500, + "insert_plan_mensaje_failed", + "No se pudo guardar el mensaje en plan_mensajes_ia", + insertErr, + ); + } if (appendRespErr) { throw new HttpError(