Primera version de ai-generate-plan
This commit is contained in:
@@ -8,6 +8,7 @@ import { corsHeaders } from "../_shared/cors.ts";
|
|||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
import { createClient } from "npm:@supabase/supabase-js@2";
|
||||||
import type { AIGeneratePlanInput } from "./types.ts";
|
import type { AIGeneratePlanInput } from "./types.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { systemPrompt } from "./prompts.ts";
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
Deno.serve(async (req) => {
|
||||||
if (req.method === "OPTIONS") {
|
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");
|
const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
||||||
if (!SERVICE_ROLE_KEY) {
|
if (!SERVICE_ROLE_KEY) {
|
||||||
@@ -104,22 +105,6 @@ Deno.serve(async (req) => {
|
|||||||
|
|
||||||
const payload: AIGeneratePlanInput = validation.data;
|
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 } =
|
const { data: estructuraPlan, error: estructuraPlanError } =
|
||||||
await supabaseService
|
await supabaseService
|
||||||
.from("estructuras_plan")
|
.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(
|
return new Response(
|
||||||
JSON.stringify({ message: "Multipart/form-data received" }),
|
JSON.stringify(jsonResponse),
|
||||||
{
|
{
|
||||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -284,6 +385,3 @@ function parseAndValidate(
|
|||||||
|
|
||||||
return { success: true, data: finalInput };
|
return { success: true, data: finalInput };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subirAStorage(supabase: supa) {
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.`;
|
||||||
Reference in New Issue
Block a user