Corregir Edge Function #36 #44
@@ -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<CreateBody>;
|
||||
|
||||
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<AddMessageBody>;
|
||||
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,9 +358,9 @@ app.post(`${prefix}/conversations/:id/messages`, async (c) => {
|
||||
|
||||
// Traer conversacion + plan + estructura
|
||||
const { data: row, error } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.from("conversaciones_asignatura")
|
||||
.select(
|
||||
"id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))",
|
||||
"id, openai_conversation_id, asignatura_id, estado, asignaturas(*)"
|
||||
)
|
||||
.eq("id", conversation_plan_id)
|
||||
.single();
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user