From 2dbe00a6703733649f10aa4a37977d4abdfd3722 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Tue, 10 Mar 2026 16:12:43 -0600 Subject: [PATCH] se agrega wh en asignaturas y se guardan campos correctamente y ademas se corrige incidencia Agregar contexto de la asignatura al chat #55 --- .../create-chat-conversation/index.ts | 17 ++- .../create-chat-conversation/lib/plan.ts | 105 +++++++++++++----- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/supabase/functions/create-chat-conversation/index.ts b/supabase/functions/create-chat-conversation/index.ts index c0fc6fd..205884e 100644 --- a/supabase/functions/create-chat-conversation/index.ts +++ b/supabase/functions/create-chat-conversation/index.ts @@ -3,7 +3,7 @@ import { corsHeaders, withCors } from "./lib/cors.ts"; 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 { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields, safeAsignaturaForPrompt, getAsignaturaSystemPrompt } from "./lib/plan.ts"; import { OpenAIService } from "../_shared/openai-service.ts"; type CreateBody = { @@ -309,8 +309,8 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => { const asignatura = (row as any).asignaturas; 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) const { data: mensajeInsertado, error: insertErr } = await supabase .from("asignatura_mensajes_ia") @@ -339,6 +339,8 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => { additionalProperties: false }; + + // 4. Llamada asincrónica con background: true const aiResult = await svc.createStructuredResponse({ conversation: row.openai_conversation_id, @@ -353,17 +355,12 @@ app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => { text: { format: { type: "json_schema", - name: isStructured ? "mejora_asignatura" : "respuesta_basica", + name: "mejoras_asignatura", schema: schema } }, input: [ - { - role: "system", - content: isStructured - ? `Asistente de asignatura. Datos: ${JSON.stringify(asignatura)}` - : "Eres un experto en diseño curricular." - }, + { role: "system", content: getAsignaturaSystemPrompt(asignatura, campos) }, { role: "user", content: body.content }, ], }); diff --git a/supabase/functions/create-chat-conversation/lib/plan.ts b/supabase/functions/create-chat-conversation/lib/plan.ts index 5116903..c5b446f 100644 --- a/supabase/functions/create-chat-conversation/lib/plan.ts +++ b/supabase/functions/create-chat-conversation/lib/plan.ts @@ -49,46 +49,54 @@ export function pickSchemaAsignaturaFields( definicion: any, campos: string[], ) { - // 1. Validación inicial + console.log(campos); + console.log(definicion); if (!definicion || definicion.type !== "object" || !definicion.properties) { - return { - type: "object", - properties: { - "ai-message": { type: "string" }, - "is_refusal": { type: "boolean" } - }, - required: ["ai-message", "is_refusal"], - additionalProperties: false - }; + return definicion; } - // 2. Filtrar solo las propiedades técnicas que el usuario pidió - const filteredProperties = Object.fromEntries( - Object.entries(definicion.properties).filter(([k]) => campos.includes(k)) - ); - - // 3. RECONSTRUIR el esquema incluyendo SIEMPRE los campos de control - const finalSchema = { - type: "object", + // Campos extra de control + const extra = { properties: { - "ai-message": { - type: "string", - description: "Tu respuesta conversacional dirigida al profesor." + "ai-message": { + type: "string", + description: "Tu respuesta conversacional dirigida al profesor explicando qué mejoraste.", }, - "is_refusal": { - type: "boolean", - description: "Indica si la solicitud es inapropiada o no relacionada." + "is_refusal": { + type: "boolean", + 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) { const copy = structuredClone(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}`); } } + +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.`; +} \ No newline at end of file