@@ -290,123 +290,93 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
|
||||
assertUuid(conversation_asig_id, "conversation_asig_id");
|
||||
|
||||
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||
if (!body.content) throw new HttpError(400, "bad_input", "content es requerido");
|
||||
if (!body.content || typeof body.content !== "string") {
|
||||
throw new HttpError(400, "bad_input", "content es requerido");
|
||||
}
|
||||
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const openai = getOpenAI();
|
||||
// Usamos el servicio que ya tienes configurado para background
|
||||
const svc = OpenAIService.fromEnv();
|
||||
|
||||
// 1. Traer datos
|
||||
// 1. Traer datos de la asignatura
|
||||
const { data: row, error } = await supabase
|
||||
.from("conversaciones_asignatura")
|
||||
.select(`*, asignaturas(*, estructuras_asignatura(definicion))`)
|
||||
.select(`id, openai_conversation_id, asignatura_id, asignaturas(*, estructuras_asignatura(definicion))`)
|
||||
.eq("id", conversation_asig_id)
|
||||
.single();
|
||||
|
||||
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||
|
||||
const asignatura = row.asignaturas;
|
||||
const asignatura = (row as any).asignaturas;
|
||||
const definicion = asignatura?.estructuras_asignatura?.definicion;
|
||||
const wantsStructured = !!definicion && (body.campos?.length ?? 0) > 0;
|
||||
const isStructured = !!definicion && (body.campos?.length ?? 0) > 0;
|
||||
|
||||
let aiMessage = "";
|
||||
let isRefusal = false;
|
||||
let recommendations: any[] = [];
|
||||
let openAiRespId = "";
|
||||
// 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner)
|
||||
const { data: mensajeInsertado, error: insertErr } = await supabase
|
||||
.from("asignatura_mensajes_ia")
|
||||
.insert({
|
||||
conversacion_asignatura_id: conversation_asig_id,
|
||||
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||
mensaje: body.content,
|
||||
campos: body.campos ?? [],
|
||||
estado: "PROCESANDO",
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
// --- FLUJO A: 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: "Eres un experto en diseño curricular." },
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "respuesta_basica",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": { type: "string" },
|
||||
"is_refusal": { type: "boolean" },
|
||||
},
|
||||
required: ["ai-message", "is_refusal"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||
|
||||
// 3. Preparar Schema (Usando tu lógica de asignatura)
|
||||
const schema = isStructured
|
||||
? pickSchemaAsignaturaFields(definicion, body.campos ?? [])
|
||||
: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": { type: "string" },
|
||||
"is_refusal": { type: "boolean" }
|
||||
},
|
||||
required: ["ai-message", "is_refusal"],
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
// 4. Llamada asincrónica con background: true
|
||||
const aiResult = await svc.createStructuredResponse({
|
||||
conversation: row.openai_conversation_id,
|
||||
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||
background: true, // <--- Ahora sí, activamos el modo background
|
||||
metadata: {
|
||||
tabla: "asignatura_mensajes_ia", // El webhook usará esto para saber dónde hacer el UPDATE
|
||||
mensaje_id: String(mensajeInsertado.id),
|
||||
is_structured: String(isStructured),
|
||||
conversation_id: conversation_asig_id // Extra para el webhook si lo necesita
|
||||
},
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: isStructured ? "mejora_asignatura" : "respuesta_basica",
|
||||
schema: schema
|
||||
}
|
||||
},
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: isStructured
|
||||
? `Asistente de asignatura. Datos: ${JSON.stringify(asignatura)}`
|
||||
: "Eres un experto en diseño curricular."
|
||||
},
|
||||
});
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
});
|
||||
|
||||
const resJson = JSON.parse(resp.output_text ?? "{}");
|
||||
aiMessage = resJson["ai-message"] ?? "";
|
||||
isRefusal = !!resJson["is_refusal"];
|
||||
openAiRespId = resp.id;
|
||||
|
||||
} else {
|
||||
// --- FLUJO B: ESTRUCTURADO ---
|
||||
const schemaBase = pickSchemaAsignaturaFields(definicion, body.campos ?? []);
|
||||
const finalSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": { type: "string" },
|
||||
"is_refusal": { type: "boolean" },
|
||||
...schemaBase.properties
|
||||
},
|
||||
required: ["ai-message", "is_refusal", ...Object.keys(schemaBase.properties || {})],
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const resp = await openai.responses.create({
|
||||
conversation: row.openai_conversation_id,
|
||||
model: CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO,
|
||||
text: {
|
||||
format: { type: "json_schema", name: "mejora_asignatura", strict: true, schema: finalSchema }
|
||||
},
|
||||
input: [
|
||||
{ role: "system", content: `Asistente curricular. Datos: ${JSON.stringify(asignatura)}` },
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
});
|
||||
|
||||
const resJson = JSON.parse(resp.output_text ?? "{}");
|
||||
aiMessage = resJson["ai-message"] ?? "";
|
||||
isRefusal = !!resJson["is_refusal"];
|
||||
openAiRespId = resp.id;
|
||||
|
||||
recommendations = Object.entries(resJson)
|
||||
.filter(([k]) => k !== "ai-message" && k !== "is_refusal")
|
||||
.map(([campo, texto]) => ({
|
||||
campo_afectado: campo,
|
||||
texto_mejora: texto,
|
||||
aplicada: false,
|
||||
}));
|
||||
if (!aiResult.ok) {
|
||||
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
|
||||
}
|
||||
|
||||
// 2. Guardar Histórico y Mensaje (Común a ambos flujos)
|
||||
await supabase.rpc("append_conversacion_asignatura", {
|
||||
p_id: conversation_asig_id,
|
||||
p_append: { timestamp: new Date().toISOString(), user: "user", prompt: body.content },
|
||||
});
|
||||
|
||||
await supabase.from("asignatura_mensajes_ia").insert({
|
||||
conversacion_asignatura_id: conversation_asig_id,
|
||||
mensaje: body.content,
|
||||
respuesta: aiMessage,
|
||||
campos: body.campos ?? [],
|
||||
propuesta: { prompt: body.content, respuesta: aiMessage, recommendations },
|
||||
is_refusal: isRefusal,
|
||||
estado: "COMPLETADO",
|
||||
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||
});
|
||||
|
||||
// 5. Responder al cliente de inmediato
|
||||
return withCors(jsonResponse({
|
||||
ok: true,
|
||||
openai_response_id: openAiRespId,
|
||||
ai_message: aiMessage,
|
||||
recommendations
|
||||
mensaje_id: mensajeInsertado.id,
|
||||
openai_response_id: aiResult.responseId
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user