From 3170ed6975a4d5ca22c7c37ba3a94cf648d1feb6 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Fri, 16 Jan 2026 16:58:43 -0600 Subject: [PATCH] Primera version de ai-generate-plan --- supabase/functions/ai-generate-plan/index.ts | 140 +++++++++++++++--- .../functions/ai-generate-plan/prompts.ts | 37 +++++ 2 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 supabase/functions/ai-generate-plan/prompts.ts diff --git a/supabase/functions/ai-generate-plan/index.ts b/supabase/functions/ai-generate-plan/index.ts index 64bae32..4e59599 100644 --- a/supabase/functions/ai-generate-plan/index.ts +++ b/supabase/functions/ai-generate-plan/index.ts @@ -8,6 +8,7 @@ import { corsHeaders } from "../_shared/cors.ts"; import { createClient } from "npm:@supabase/supabase-js@2"; import type { AIGeneratePlanInput } from "./types.ts"; import { z } from "zod"; +import { systemPrompt } from "./prompts.ts"; Deno.serve(async (req) => { if (req.method === "OPTIONS") { @@ -74,7 +75,7 @@ Deno.serve(async (req) => { }, }); - // TODO: iniciar sesion con el usuario + // TODO: iniciar sesion con el usuario e implementar RLS const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!SERVICE_ROLE_KEY) { @@ -104,22 +105,6 @@ Deno.serve(async (req) => { const payload: AIGeneratePlanInput = validation.data; - const respuestaSubida: string[] = subirAStorage( - supabaseService, - payload.archivosAdjuntos, - "ai-storage", - "tmp", - ); - - const { data: carrera, error: carreraError } = await supabaseService - .from("carreras") - .select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)") - .eq("id", payload.datosBasicos.carreraId) - .maybeSingle(); - if (carreraError) { - throw new Error("Error fetching carrera: " + carreraError.message); - } - const { data: estructuraPlan, error: estructuraPlanError } = await supabaseService .from("estructuras_plan") @@ -132,8 +117,124 @@ Deno.serve(async (req) => { ); } + const userPrompt = + `Genera un borrador completo del PLAN DE ESTUDIOS con base en lo siguiente: + - Descripción del enfoque: ${payload.iaConfig.descripcionEnfoque} + - Notas adicionales: ${payload.iaConfig.notasAdicionales ?? "Ninguna"}`; + const aiStructuredPayload = { + response: { + input: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + }, + estructuraPlan, + references: { + openaiFileIds: payload.iaConfig.archivosReferencia, + vectorStoreIds: payload.iaConfig.repositoriosIds, + }, + usarMCP: Boolean(payload.iaConfig?.usarMCP), + }; + + const aiStructuredFormData = new FormData(); + aiStructuredFormData.append("payload", JSON.stringify(aiStructuredPayload)); + for (const file of payload.archivosAdjuntos ?? []) { + aiStructuredFormData.append("archivos", file); + } + + const { data: aiJson, error } = await supabaseService.functions.invoke( + "ai-structured", + { + method: "POST", + body: aiStructuredFormData, + }, + ); + + if (error) { + throw new Error( + "ai-structured invocation failed: " + error.message, + ); + } + + if (!aiJson || !aiJson.ok) { + throw new Error( + "ai-structured returned an error or no data", + ); + } + + // TODO: Insertar interacciones con IA y quizas forzar a que la informacion de datosBasicos sea la misma que la recibida + + const { data: estado } = await supabaseService + .from("estados_plan") + .select("id,clave,orden") + .ilike("clave", "BORRADOR%") + .order("orden", { ascending: true }) + .limit(1) + .maybeSingle(); + + const { data: carrera, error: carreraError } = await supabaseService + .from("carreras") + .select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)") + .eq("id", payload.datosBasicos.carreraId) + .maybeSingle(); + if (carreraError) { + throw new Error("Error fetching carrera: " + carreraError.message); + } + + const { data: plan, error: planError } = await supabaseService + .from("planes_estudio") + .insert({ + carrera_id: carrera?.id, + estructura_id: estructuraPlan?.id, + nombre: payload.datosBasicos.nombrePlan, + nivel: payload.datosBasicos.nivel, + tipo_ciclo: payload.datosBasicos.tipoCiclo, + numero_ciclos: payload.datosBasicos.numCiclos, + datos: aiJson.output, + estado_actual_id: estado?.id, + activo: true, + tipo_origen: "IA", + meta_origen: { + generado_por: "ai_generate_plan", + ai_structured: { + cid: aiJson?.cid ?? null, + responseId: aiJson?.responseId ?? null, + conversationId: aiJson?.conversationId ?? null, + model: aiJson?.model, + usage: aiJson?.usage ?? null, + }, + referencias: { + archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null, + repositoriosIds: payload.iaConfig?.repositoriosIds ?? null, + // openaiFileIds, + // vectorStoreIds, + }, + iaConfig: { + descripcionEnfoque: payload.iaConfig?.descripcionEnfoque ?? null, + notasAdicionales: payload.iaConfig?.notasAdicionales ?? null, + usarMCP: Boolean(payload.iaConfig?.usarMCP), + }, + }, + // creado_por: user.id, + // actualizado_por: user.id, + }) + .select( + "id,nombre,nivel,tipo_ciclo,numero_ciclos,carrera_id,estructura_id,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,datos", + ) + .single(); + + if (planError) { + throw new Error("Error inserting plan: " + planError.message); + } + + // TODO: update a interaccion_ia y e insert a cambios_plancon id de plan generado + const jsonResponse = { + ok: true, + plan, + }; + return new Response( - JSON.stringify({ message: "Multipart/form-data received" }), + JSON.stringify(jsonResponse), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200, @@ -284,6 +385,3 @@ function parseAndValidate( return { success: true, data: finalInput }; } - -async function subirAStorage(supabase: supa) { -} diff --git a/supabase/functions/ai-generate-plan/prompts.ts b/supabase/functions/ai-generate-plan/prompts.ts new file mode 100644 index 0000000..399378b --- /dev/null +++ b/supabase/functions/ai-generate-plan/prompts.ts @@ -0,0 +1,37 @@ +export const systemPrompt = + `System: System: Eres Lyra, una Consultora Educativa Experta en Normatividad SEP (México), especializada en el Acuerdo 17/11/17. + +# Rol y Objetivo +Genera contenido académico, administrativo y pedagógico para completar el "Anexo 1: Plan de Estudios" de una solicitud de RVOE Federal. La salida será estrictamente el objeto requerido por el sistema validado por el parámetro text.format (tipo: json_schema). + +# Instrucciones +- Cumple rigurosamente con los lineamientos normativos del Acuerdo 17/11/17 SEP, asegurando que la información estructural y conceptual siga las mejores prácticas y requisitos legales. +- Emplea competencia técnica y terminología precisa; imita el trabajo de un experto en diseño curricular con 20 años de experiencia. + +## Checklist de Subtareas +Antes de ejecutar el trabajo, comienza con una lista concisa (3-7 puntos) de los pasos conceptuales a realizar para este encargo, cubriendo la revisión de requisitos normativos, estructuración de campos y validación de consistencia. + +## Restricciones de Calidad (Kernel) +1. **Cumplimiento Normativo:** + - Calcula la 'carga_horaria_a_la_semana', sin exceder el límite máximo de 50 horas. + - Asegura que el 'antecedente_academico' sea legalmente válido para el nivel solicitado (ejemplo: Bachillerato para Licenciatura). + - Para 'duracion_del_ciclo_escolar', indica el número de semanas según el estándar vigente (usualmente 16 a 20 semanas para ciclo semestral). +2. **Lenguaje Académico:** + - En 'fines_de_aprendizaje_o_formacion', utiliza la Taxonomía de Bloom y comienza cada elemento con un verbo en infinitivo (ejemplo: Analizar, Diseñar, Evaluar), enfocándote en competencias profesionales. + - Evita calificativos subjetivos como "bueno" o "bonito"; opta por términos técnicos tales como "pertinente", "integral" o "transversal". +3. **Coherencia Interna:** + - 'perfil_de_ingreso' debe enlistar conocimientos previos realistas y explícitos. + - 'perfil_de_egreso' debe mostrar una transformación clara del alumno, alineada con el nombre del plan de estudios. + - Si la modalidad es "No escolarizada" o "Mixta", la 'justificacion_de_la_propuesta_curricular' debe mencionar explícitamente el uso de plataformas tecnológicas, autogestión del aprendizaje y flexibilidad espacio-temporal. +4. **Datos Simulados:** + - En campos administrativos, como nombres de personas o claves, genera información ficticia, plausible y profesional (por ejemplo, "Lic. María González Pérez, Directora Académica"). + +# Contexto +- Entrada del usuario: Nivel, Nombre del Programa y Modalidad. +- Todos los campos deben estar presentes y estructurados según el json_schema especificado. +- Campos de perfiles y fines deben ser arrays de cadenas; los numéricos, enteros; objetos anidados según el formato especificado. +- Si la información proporcionada es insuficiente, inválida o ambigua, utiliza valores estándar o ficticios plausibles y coherentes. + +# Validación de Salida +Después de generar el objeto, realiza una validación breve para asegurar que todos los campos requeridos estén presentes, que los valores cumplan los criterios normativos y que la estructura cumpla con el json_schema indicado. +Corrige cualquier inconsistencia detectada antes de finalizar la entrega.`;