se agrega wh en asignaturas y se guardan campos correctamente y ademas se corrige incidencia Agregar contexto de la asignatura al chat #55

This commit is contained in:
2026-03-10 16:12:43 -06:00
parent b63857d38f
commit 2dbe00a670
2 changed files with 82 additions and 40 deletions
@@ -3,7 +3,7 @@ import { corsHeaders, withCors } from "./lib/cors.ts";
import { HttpError, jsonResponse } from "./lib/errors.ts"; import { HttpError, jsonResponse } from "./lib/errors.ts";
import { getOpenAI } from "./lib/openai.ts"; import { getOpenAI } from "./lib/openai.ts";
import { getSupabaseServiceClient, requireUser } from "./lib/supabase.ts"; import { getSupabaseServiceClient, requireUser } from "./lib/supabase.ts";
import { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields } from "./lib/plan.ts"; import { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields, safeAsignaturaForPrompt, getAsignaturaSystemPrompt } from "./lib/plan.ts";
import { OpenAIService } from "../_shared/openai-service.ts"; import { OpenAIService } from "../_shared/openai-service.ts";
type CreateBody = { type CreateBody = {
@@ -309,8 +309,8 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
const asignatura = (row as any).asignaturas; const asignatura = (row as any).asignaturas;
const definicion = asignatura?.estructuras_asignatura?.definicion; const definicion = asignatura?.estructuras_asignatura?.definicion;
const isStructured = !!definicion && (body.campos?.length ?? 0) > 0; const campos = body.campos ?? [];
const isStructured = !!definicion && campos.length > 0;
// 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner) // 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner)
const { data: mensajeInsertado, error: insertErr } = await supabase const { data: mensajeInsertado, error: insertErr } = await supabase
.from("asignatura_mensajes_ia") .from("asignatura_mensajes_ia")
@@ -339,6 +339,8 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
additionalProperties: false additionalProperties: false
}; };
// 4. Llamada asincrónica con background: true // 4. Llamada asincrónica con background: true
const aiResult = await svc.createStructuredResponse({ const aiResult = await svc.createStructuredResponse({
conversation: row.openai_conversation_id, conversation: row.openai_conversation_id,
@@ -353,17 +355,12 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
text: { text: {
format: { format: {
type: "json_schema", type: "json_schema",
name: isStructured ? "mejora_asignatura" : "respuesta_basica", name: "mejoras_asignatura",
schema: schema schema: schema
} }
}, },
input: [ input: [
{ { role: "system", content: getAsignaturaSystemPrompt(asignatura, campos) },
role: "system",
content: isStructured
? `Asistente de asignatura. Datos: ${JSON.stringify(asignatura)}`
: "Eres un experto en diseño curricular."
},
{ role: "user", content: body.content }, { role: "user", content: body.content },
], ],
}); });
@@ -49,46 +49,54 @@ export function pickSchemaAsignaturaFields(
definicion: any, definicion: any,
campos: string[], campos: string[],
) { ) {
// 1. Validación inicial console.log(campos);
console.log(definicion);
if (!definicion || definicion.type !== "object" || !definicion.properties) { if (!definicion || definicion.type !== "object" || !definicion.properties) {
return { return definicion;
type: "object",
properties: {
"ai-message": { type: "string" },
"is_refusal": { type: "boolean" }
},
required: ["ai-message", "is_refusal"],
additionalProperties: false
};
} }
// 2. Filtrar solo las propiedades técnicas que el usuario pidió // Campos extra de control
const filteredProperties = Object.fromEntries( const extra = {
Object.entries(definicion.properties).filter(([k]) => campos.includes(k))
);
// 3. RECONSTRUIR el esquema incluyendo SIEMPRE los campos de control
const finalSchema = {
type: "object",
properties: { properties: {
"ai-message": { "ai-message": {
type: "string", type: "string",
description: "Tu respuesta conversacional dirigida al profesor." description: "Tu respuesta conversacional dirigida al profesor explicando qué mejoraste.",
}, },
"is_refusal": { "is_refusal": {
type: "boolean", type: "boolean",
description: "Indica si la solicitud es inapropiada o no relacionada." description: "Indica si la solicitud es inapropiada.",
}, },
...filteredProperties // Aquí entran objetivo, contenido, etc.
}, },
// Forzamos que ai-message e is_refusal sean obligatorios siempre
required: ["ai-message", "is_refusal", ...Object.keys(filteredProperties)],
additionalProperties: false
}; };
return finalSchema; const out = structuredClone(definicion);
}
// FILTRADO: Si el usuario pidió campos específicos, filtramos.
// Si campos está vacío, la lógica de Planes devolvería todo,
// pero aquí asumimos que siempre vienen campos si isStructured es true.
const entries = Object.entries(out.properties).filter(([k]) =>
campos.includes(k)
);
out.properties = Object.fromEntries(entries);
if (Array.isArray(out.required)) {
out.required = out.required.filter((k: string) => campos.includes(k));
}
// SIEMPRE agregamos los campos de control (ai-message, is_refusal)
out.properties = { ...out.properties, ...extra.properties };
// Reconstruimos los requeridos para que OpenAI sepa que DEBE llenar los campos solicitados
out.required = Array.isArray(out.required)
? [...new Set([...out.required, ...Object.keys(extra.properties)])]
: Object.keys(extra.properties);
// Forzamos que no invente campos
out.additionalProperties = false;
return out;
}
export function safePlanForPrompt(plan: any) { export function safePlanForPrompt(plan: any) {
const copy = structuredClone(plan); const copy = structuredClone(plan);
if (copy?.estructuras_plan) delete copy.estructuras_plan; if (copy?.estructuras_plan) delete copy.estructuras_plan;
@@ -101,3 +109,40 @@ export function assertUuid(v: string, name: string) {
throw new HttpError(400, "bad_input", `Invalid ${name}`); throw new HttpError(400, "bad_input", `Invalid ${name}`);
} }
} }
export function safeAsignaturaForPrompt(asignatura: any) {
const copy = structuredClone(asignatura);
// Eliminamos la definición de la estructura para que no ensucie el prompt
// y solo queden los datos reales de la asignatura
if (copy?.estructuras_asignatura) delete copy.estructuras_asignatura;
return copy;
}
export function getAsignaturaSystemPrompt(asignatura: any, campos: string[]) {
const asignaturaLimpia = safeAsignaturaForPrompt(asignatura);
const nombreAsig = asignaturaLimpia?.nombre || "la asignatura";
if (campos.length === 0) {
return `Eres un asistente experto en diseño curricular.
Tu objetivo es ayudar al profesor a mejorar su asignatura: "${nombreAsig}".
DATOS ACTUALES: ${JSON.stringify(asignaturaLimpia)}.
COMPORTAMIENTO:
- El usuario aún no ha seleccionado campos específicos para mejorar.
- NO propongas cambios técnicos detallados ni rellenes campos del JSON todavía.
- Saluda cordialmente y menciona qué partes de esta asignatura puedes ayudar a mejorar (objetivos, contenidos, criterios, etc.).
- Mantén una conversación fluida y espera a que el usuario elija qué quiere trabajar.`;
}
return `Eres un asistente experto en diseño curricular trabajando sobre: "${nombreAsig}".
DATOS ACTUALES: ${JSON.stringify(asignaturaLimpia)}.
TAREA CRÍTICA:
El usuario ha solicitado mejorar estos ${campos.length} campos: ${campos.join(", ")}.
REGLAS DE ORO:
1. Debes proporcionar una propuesta de mejora para CADA UNO de los campos solicitados.
2. No omitas ninguno. Si un campo no requiere cambios drásticos, optimiza su redacción técnica.
3. En el JSON, cada campo debe contener tu propuesta de texto mejorado.
4. En 'ai-message', resume los cambios hechos en cada uno de los campos solicitados.`;
}