From 5c9fde1a9eb009dd721ef1c29ba799c3dd3897b3 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 19:42:12 -0600 Subject: [PATCH] =?UTF-8?q?Generaci=C3=B3n=20de=20datos=20b=C3=A1sicos=20d?= =?UTF-8?q?e=20asignaturas=20funcional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #6: - Ya se pueden generar asignaturas con IA. Específicamente los datos generales. - Se actualizaron los tipos de supabase - Se arreglaron los imports - TODO: generar contenido temático de la asignatura --- deno.json | 6 +- diff.txt | Bin 26264 -> 51126 bytes supabase/functions/_shared/database.types.ts | 59 ++- supabase/functions/ai-generate-plan/index.ts | 4 +- .../functions/ai-generate-subject/deno.json | 7 +- .../functions/ai-generate-subject/index.ts | 344 +++++++++++++++++- .../functions/ai-generate-subject/types.ts | 37 ++ 7 files changed, 404 insertions(+), 53 deletions(-) create mode 100644 supabase/functions/ai-generate-subject/types.ts diff --git a/deno.json b/deno.json index 60f816d..2128fe4 100644 --- a/deno.json +++ b/deno.json @@ -7,6 +7,10 @@ "imports": { "@openai/openai": "jsr:@openai/openai@^6.16.0", "@zod/zod": "jsr:@zod/zod@^4.3.5", - "@david/dax": "jsr:@david/dax@^0.44.2" + "@david/dax": "jsr:@david/dax@^0.44.2", + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts", + "openai": "npm:openai@6.16.0", + "@supabase/supabase-js": "npm:@supabase/supabase-js@2", + "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts" } } diff --git a/diff.txt b/diff.txt index b2704fc12fbf93bc76dd1ff384e32a1099a0a7ff..e901abf69b5b270f6ab9a56729323d3ca7dd0501 100644 GIT binary patch literal 51126 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 diff --git a/supabase/functions/_shared/database.types.ts b/supabase/functions/_shared/database.types.ts index fa3ef8e..bf4b960 100644 --- a/supabase/functions/_shared/database.types.ts +++ b/supabase/functions/_shared/database.types.ts @@ -4,7 +4,7 @@ | boolean | null | { [key: string]: Json | undefined } - | Json[]; + | Array; export type Database = { // Allows to automatically instantiate createClient with right options @@ -90,6 +90,7 @@ export type Database = { Row: { actualizado_en: string; actualizado_por: string | null; + asignatura_hash: string | null; codigo: string | null; contenido_tematico: Json; creado_en: string; @@ -97,8 +98,8 @@ export type Database = { creditos: number; datos: Json; estructura_id: string | null; - facultad_propietaria_id: string | null; - horas_semana: number | null; + horas_academicas: number | null; + horas_independientes: number | null; id: string; linea_plan_id: string | null; meta_origen: Json; @@ -114,6 +115,7 @@ export type Database = { Insert: { actualizado_en?: string; actualizado_por?: string | null; + asignatura_hash?: string | null; codigo?: string | null; contenido_tematico?: Json; creado_en?: string; @@ -121,8 +123,8 @@ export type Database = { creditos: number; datos?: Json; estructura_id?: string | null; - facultad_propietaria_id?: string | null; - horas_semana?: number | null; + horas_academicas?: number | null; + horas_independientes?: number | null; id?: string; linea_plan_id?: string | null; meta_origen?: Json; @@ -138,6 +140,7 @@ export type Database = { Update: { actualizado_en?: string; actualizado_por?: string | null; + asignatura_hash?: string | null; codigo?: string | null; contenido_tematico?: Json; creado_en?: string; @@ -145,8 +148,8 @@ export type Database = { creditos?: number; datos?: Json; estructura_id?: string | null; - facultad_propietaria_id?: string | null; - horas_semana?: number | null; + horas_academicas?: number | null; + horas_independientes?: number | null; id?: string; linea_plan_id?: string | null; meta_origen?: Json; @@ -181,14 +184,6 @@ export type Database = { referencedRelation: "estructuras_asignatura"; referencedColumns: ["id"]; }, - { - foreignKeyName: - "asignaturas_facultad_propietaria_id_fkey"; - columns: ["facultad_propietaria_id"]; - isOneToOne: false; - referencedRelation: "facultades"; - referencedColumns: ["id"]; - }, { foreignKeyName: "asignaturas_linea_plan_fk_compuesta"; columns: ["linea_plan_id", "plan_estudio_id"]; @@ -332,8 +327,8 @@ export type Database = { cambiado_por: string | null; campo: string | null; id: string; - interaccion_ia_id: string | null; plan_estudio_id: string; + response_id: string | null; tipo: Database["public"]["Enums"]["tipo_cambio"]; valor_anterior: Json | null; valor_nuevo: Json | null; @@ -343,8 +338,8 @@ export type Database = { cambiado_por?: string | null; campo?: string | null; id?: string; - interaccion_ia_id?: string | null; plan_estudio_id: string; + response_id?: string | null; tipo: Database["public"]["Enums"]["tipo_cambio"]; valor_anterior?: Json | null; valor_nuevo?: Json | null; @@ -354,8 +349,8 @@ export type Database = { cambiado_por?: string | null; campo?: string | null; id?: string; - interaccion_ia_id?: string | null; plan_estudio_id?: string; + response_id?: string | null; tipo?: Database["public"]["Enums"]["tipo_cambio"]; valor_anterior?: Json | null; valor_nuevo?: Json | null; @@ -368,20 +363,6 @@ export type Database = { referencedRelation: "usuarios_app"; referencedColumns: ["id"]; }, - { - foreignKeyName: "cambios_plan_plan_estudio_id_fkey"; - columns: ["plan_estudio_id"]; - isOneToOne: false; - referencedRelation: "planes_estudio"; - referencedColumns: ["id"]; - }, - { - foreignKeyName: "cambios_plan_plan_estudio_id_fkey"; - columns: ["plan_estudio_id"]; - isOneToOne: false; - referencedRelation: "plantilla_plan"; - referencedColumns: ["plan_estudio_id"]; - }, ]; }; carreras: { @@ -708,6 +689,7 @@ export type Database = { actualizado_en: string; actualizado_por: string | null; carrera_id: string; + conversation_id: string | null; creado_en: string; creado_por: string | null; datos: Json; @@ -719,6 +701,7 @@ export type Database = { nombre: string; nombre_search: string | null; numero_ciclos: number; + plan_hash: string | null; tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"]; tipo_origen: | Database["public"]["Enums"]["tipo_origen"] @@ -729,6 +712,7 @@ export type Database = { actualizado_en?: string; actualizado_por?: string | null; carrera_id: string; + conversation_id?: string | null; creado_en?: string; creado_por?: string | null; datos?: Json; @@ -740,6 +724,7 @@ export type Database = { nombre: string; nombre_search?: string | null; numero_ciclos: number; + plan_hash?: string | null; tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"]; tipo_origen?: | Database["public"]["Enums"]["tipo_origen"] @@ -750,6 +735,7 @@ export type Database = { actualizado_en?: string; actualizado_por?: string | null; carrera_id?: string; + conversation_id?: string | null; creado_en?: string; creado_por?: string | null; datos?: Json; @@ -761,6 +747,7 @@ export type Database = { nombre?: string; nombre_search?: string | null; numero_ciclos?: number; + plan_hash?: string | null; tipo_ciclo?: Database["public"]["Enums"]["tipo_ciclo"]; tipo_origen?: | Database["public"]["Enums"]["tipo_origen"] @@ -1140,7 +1127,7 @@ export type Database = { fuente_cambio: "HUMANO" | "IA"; nivel_plan_estudio: | "Licenciatura" - | "Maestr├¡a" + | "Maestría" | "Doctorado" | "Especialidad" | "Diplomado" @@ -1163,7 +1150,8 @@ export type Database = { | "ACTUALIZACION_MAPA" | "TRANSICION_ESTADO" | "OTRO" - | "CREACION"; + | "CREACION" + | "ACTUALIZACION"; tipo_ciclo: "Semestre" | "Cuatrimestre" | "Trimestre" | "Otro"; tipo_estructura_plan: "CURRICULAR" | "NO_CURRICULAR"; tipo_fuente_bibliografia: "MANUAL" | "BIBLIOTECA"; @@ -1343,7 +1331,7 @@ export const Constants = { fuente_cambio: ["HUMANO", "IA"], nivel_plan_estudio: [ "Licenciatura", - "Maestr├¡a", + "Maestría", "Doctorado", "Especialidad", "Diplomado", @@ -1370,6 +1358,7 @@ export const Constants = { "TRANSICION_ESTADO", "OTRO", "CREACION", + "ACTUALIZACION", ], tipo_ciclo: ["Semestre", "Cuatrimestre", "Trimestre", "Otro"], tipo_estructura_plan: ["CURRICULAR", "NO_CURRICULAR"], diff --git a/supabase/functions/ai-generate-plan/index.ts b/supabase/functions/ai-generate-plan/index.ts index 0f3185b..1db2eef 100644 --- a/supabase/functions/ai-generate-plan/index.ts +++ b/supabase/functions/ai-generate-plan/index.ts @@ -3,10 +3,10 @@ // This enables autocomplete, go to definition, etc. // Setup type definitions for built-in Supabase Runtime APIs -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import "@supabase/functions-js/edge-runtime.d.ts"; import { corsHeaders } from "../_shared/cors.ts"; import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; -import { createClient } from "npm:@supabase/supabase-js@2"; +import { createClient } from "@supabase/supabase-js"; import type { Database, Json } from "../_shared/database.types.ts"; import type { AIGeneratePlanInput } from "./types.ts"; import { z } from "zod"; diff --git a/supabase/functions/ai-generate-subject/deno.json b/supabase/functions/ai-generate-subject/deno.json index f6ca845..8b204ca 100644 --- a/supabase/functions/ai-generate-subject/deno.json +++ b/supabase/functions/ai-generate-subject/deno.json @@ -1,3 +1,8 @@ { - "imports": {} + "imports": { + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts", + "openai": "npm:openai@6.16.0", + "@supabase/supabase-js": "npm:@supabase/supabase-js@2", + "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts" + } } diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts index e974ea3..9045854 100644 --- a/supabase/functions/ai-generate-subject/index.ts +++ b/supabase/functions/ai-generate-subject/index.ts @@ -1,12 +1,15 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import "@supabase/functions-js/edge-runtime.d.ts"; import { corsHeaders } from "../_shared/cors.ts"; import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; -import { createClient } from "npm:@supabase/supabase-js@2"; +import { createClient } from "@supabase/supabase-js"; import type { Database, Json } from "../_shared/database.types.ts"; +import { z } from "zod"; +import { + OpenAIService, + type StructuredResponseOptions, +} from "../_shared/openai-service.ts"; -console.log("Hello from Functions!"); - -Deno.serve(async (req) => { +Deno.serve(async (req: Request): Promise => { const url = new URL(req.url); const functionName = url.pathname.split("/").pop(); console.log( @@ -18,6 +21,52 @@ Deno.serve(async (req) => { } try { + const method = req.method; + if (method !== "POST") { + console.error( + `[${ + new Date().toISOString() + }][${functionName}]: Invalid method: ${method}`, + ); + throw new HttpError( + 405, + "Método no permitido.", + "METHOD_NOT_ALLOWED", + { method }, + ); + } + + const authHeaderRaw = req.headers.get("Authorization") ?? + req.headers.get("authorization"); + if (!authHeaderRaw) { + console.error( + `[${ + new Date().toISOString() + }][${functionName}]: Missing Authorization header`, + ); + throw new HttpError( + 401, + "No autorizado.", + "UNAUTHORIZED", + { reason: "missing_authorization_header" }, + ); + } + + const contentType = (req.headers.get("content-type") || "").toLowerCase(); + if (!contentType.startsWith("multipart/form-data")) { + console.error( + `[${ + new Date().toISOString() + }][${functionName}]: Unsupported content type: ${contentType}`, + ); + throw new HttpError( + 415, + "Content-Type no soportado.", + "UNSUPPORTED_MEDIA_TYPE", + { contentType }, + ); + } + const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY"); if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { @@ -59,20 +108,207 @@ Deno.serve(async (req) => { SERVICE_ROLE_KEY, ); - // INICIO DE CODIGO PARA DEBBUGGING + const formData = await req.formData(); + const validation = parseAndValidate(formData); + if (!validation.success) { + console.error( + `[${new Date().toISOString()}][${functionName}]: Validation errors:`, + validation.errors, + ); + const message = validation.errors + .map((e, i) => `${i + 1}. ${e}`) + .join("\n"); + throw new HttpError( + 422, + message, + "VALIDATION_ERROR", + { errors: validation.errors }, + ); + } + + const payload = validation.data; + + const { data: estructura, error: estructuraError } = await supabaseService + .from("estructuras_asignatura") + .select("id,nombre,definicion,version") + .eq("id", payload.datosBasicos.estructuraId) + .single(); + if (estructuraError) { + const maybeCode = (estructuraError as { code?: string }).code; + if (maybeCode === "PGRST116") { + throw new HttpError( + 404, + "No se encontró la estructura de la asignatura.", + "NOT_FOUND", + { + table: "estructuras_asignatura", + id: payload.datosBasicos.estructuraId, + }, + ); + } + throw new HttpError( + 500, + "No se pudo obtener la estructura de la asignatura.", + "SUPABASE_QUERY_FAILED", + estructuraError, + ); + } + + const systemPrompt = + "Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado."; + + const userPrompt = + `Genera un borrador completo completo de una ASIGNATURA con base en lo siguiente:\n` + + `- Nombre de la asignatura: ${payload.datosBasicos.nombre}\n` + + `- Código (clave de la asignatura): ${ + payload.datosBasicos.codigo ?? "(no especificado)" + }\n` + + `- Tipo: ${payload.datosBasicos.tipo ?? "(no especificado)"}\n` + + `- Número de créditos: ${payload.datosBasicos.creditos}\n` + + `- Horas académicas: ${ + payload.datosBasicos.horasAcademicas ?? "(no especificado)" + }\n` + + `- Horas independientes: ${ + payload.datosBasicos.horasIndependientes ?? "(no especificado)" + }\n` + + `- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${ + payload.iaConfig?.descripcionEnfoqueAcademico ?? "(ninguna)" + }\n` + + `- Instrucciones adicionales (sobre el formato de la respuesta generada): ${ + payload.iaConfig?.instruccionesAdicionalesIA ?? "(ninguna)" + }`; + + const schemaDef: Record = + typeof estructura?.definicion === "object" && + estructura?.definicion !== null + ? (estructura.definicion as Record) + : {}; + + const aiStructuredPayload: StructuredResponseOptions = { + model: "gpt-5-nano", + input: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + text: { + format: { + type: "json_schema", + name: "asignatura_contenido_tematico", + schema: schemaDef, + strict: true, + }, + }, + }; + + const svc = OpenAIService.fromEnv(); + if (!(svc instanceof OpenAIService)) { + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "OPENAI_MISCONFIGURED", + svc, + ); + } + + const aiResult = await svc.createStructuredResponse( + aiStructuredPayload, + payload.archivosAdjuntos, + ); + if (!aiResult.ok) { + const status = aiResult.code === "MissingEnv" ? 500 : 502; + throw new HttpError( + status, + "No se pudo generar la asignatura con IA.", + "OPENAI_REQUEST_FAILED", + aiResult, + ); + } + + let aiOutput = aiResult.output ?? null; + if (aiOutput == null && aiResult.outputText) { + try { + aiOutput = JSON.parse(aiResult.outputText); + } catch { + throw new HttpError( + 502, + "La respuesta de la IA no es JSON válido.", + "OPENAI_INVALID_JSON", + { outputText: aiResult.outputText }, + ); + } + } + if (!aiOutput) { + throw new HttpError( + 502, + "La respuesta de la IA no contiene salida estructurada.", + "OPENAI_MISSING_STRUCTURED_OUTPUT", + { outputText: aiResult.outputText ?? null }, + ); + } + + const aiOutputJson: Json = aiOutput as unknown as Json; + + const asignaturaInsert: + Database["public"]["Tables"]["asignaturas"]["Insert"] = { + plan_estudio_id: payload.plan_estudio_id, + estructura_id: payload.datosBasicos.estructuraId, + nombre: payload.datosBasicos.nombre, + codigo: payload.datosBasicos.codigo ?? null, + creditos: payload.datosBasicos.creditos, + horas_academicas: payload.datosBasicos.horasAcademicas ?? null, + horas_independientes: payload.datosBasicos.horasIndependientes ?? null, + tipo: (payload.datosBasicos.tipo ?? undefined) as Database["public"][ + "Tables" + ]["asignaturas"]["Insert"]["tipo"], + tipo_origen: "IA", + datos: aiOutputJson, + meta_origen: { + generado_por: "ai_generate_subject", + ai: { + responseId: aiResult.responseId ?? null, + conversationId: aiResult.conversationId ?? null, + model: aiResult.model, + usage: aiResult.usage ?? null, + }, + referencias: { + archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null, + repositoriosReferencia: payload.iaConfig?.repositoriosReferencia ?? + null, + }, + iaConfig: { + descripcionEnfoqueAcademico: + payload.iaConfig?.descripcionEnfoqueAcademico ?? null, + instruccionesAdicionalesIA: + payload.iaConfig?.instruccionesAdicionalesIA ?? null, + }, + } as unknown as Json, + }; + + const { data: asignatura, error: asignaturaError } = await supabaseService + .from("asignaturas") + .insert(asignaturaInsert) + .select( + "id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,estructura_id,plan_estudio_id,contenido_tematico,meta_origen,creado_en,actualizado_en", + ) + .single(); + + if (asignaturaError) { + const maybeCode = (asignaturaError as { code?: string }).code; + const status = maybeCode ? 409 : 500; + throw new HttpError( + status, + "No se pudo guardar la asignatura.", + "SUPABASE_INSERT_FAILED", + { ...asignaturaError, code: maybeCode }, + ); + } + console.log( `[${ new Date().toISOString() }][${functionName}]: Request processed successfully`, ); - const { data: asignatura_debug, error: asignatura_error } = - await supabaseService - .from("asignaturas") - .select("*") - .eq("id", "9d4dda6a-488f-428a-8a07-38081592a641") - .single(); - return sendSuccess(asignatura_debug); - // FIN DE CODIGO PARA DEBBUGGING + return sendSuccess(asignatura); } catch (error) { if (error instanceof HttpError) { console.error( @@ -105,3 +341,83 @@ Deno.serve(async (req) => { ); } }); + +const jsonFromString = (schema: T) => + z.string().transform((str, ctx) => { + try { + return JSON.parse(str); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "El formato no es un JSON válido", + }); + return z.NEVER; + } + }).pipe(schema); + +const jsonFromStringOptional = (schema: T) => + z.string().optional().default("{}").transform((str, ctx) => { + try { + return JSON.parse(str); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "El formato no es un JSON válido", + }); + return z.NEVER; + } + }).pipe(schema); + +const DatosBasicosSchema = z.object({ + nombre: z.string().min(1, "El nombre es requerido"), + codigo: z.string().min(1).optional().nullable(), + tipo: z.union([z.string().min(1), z.null()]).optional().transform((v) => + v ?? null + ), + creditos: z.number({ invalid_type_error: "Créditos debe ser numérico" }) + .positive("Créditos debe ser >= 0"), + horasAcademicas: z.number().int().nonnegative().optional().nullable(), + horasIndependientes: z.number().int().nonnegative().optional().nullable(), + estructuraId: z.string().uuid("estructuraId debe ser un UUID"), +}); + +const IAConfigSchema = z.object({ + descripcionEnfoqueAcademico: z.string().optional(), + instruccionesAdicionalesIA: z.string().optional(), + archivosReferencia: z.array(z.string().uuid()).optional(), + repositoriosReferencia: z.array(z.string().uuid()).optional(), +}).partial(); + +const SolicitudSchema = z.object({ + plan_estudio_id: z.string().uuid("plan_estudio_id debe ser un UUID"), + datosBasicos: jsonFromString(DatosBasicosSchema), + iaConfig: jsonFromStringOptional(IAConfigSchema), + archivosAdjuntos: z.array(z.instanceof(File)).optional().default([]), +}); + +type EdgeAIGenerateSubjectInput = z.infer; + +function parseAndValidate( + formData: FormData, +): { success: true; data: EdgeAIGenerateSubjectInput } | { + success: false; + errors: string[]; +} { + const rawInput = { + plan_estudio_id: formData.get("plan_estudio_id"), + datosBasicos: formData.get("datosBasicos"), + iaConfig: formData.get("iaConfig"), + archivosAdjuntos: formData.getAll("archivosAdjuntos"), + }; + + const result = SolicitudSchema.safeParse(rawInput); + if (!result.success) { + const errors = result.error.errors.map((issue) => { + const path = issue.path.length ? `${issue.path.join(".")}: ` : ""; + return `${path}${issue.message}`; + }); + return { success: false, errors }; + } + + return { success: true, data: result.data }; +} diff --git a/supabase/functions/ai-generate-subject/types.ts b/supabase/functions/ai-generate-subject/types.ts new file mode 100644 index 0000000..ec9d6e4 --- /dev/null +++ b/supabase/functions/ai-generate-subject/types.ts @@ -0,0 +1,37 @@ +import { Tables } from "../_shared/database.types.ts"; + +export interface UploadedFile { + id: string; // Necesario para React (key) + file: File; // La fuente de verdad (contiene name, size, type) + preview?: string; // Opcional: si fueran imágenes +} + +export type AIGenerateSubjectInput = { + plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"]; + datosBasicos: { + nombre: Tables<"asignaturas">["nombre"]; + codigo?: Tables<"asignaturas">["codigo"]; + tipo: Tables<"asignaturas">["tipo"] | null; + creditos: Tables<"asignaturas">["creditos"] | null; + horasAcademicas?: Tables<"asignaturas">["horas_academicas"] | null; + horasIndependientes?: Tables<"asignaturas">["horas_independientes"] | null; + estructuraId: Tables<"asignaturas">["estructura_id"] | null; + }; + // clonInterno?: { + // facultadId?: string + // carreraId?: string + // planOrigenId?: string + // asignaturaOrigenId?: string | null + // } + // clonTradicional?: { + // archivoWordAsignaturaId: string | null + // archivosAdicionalesIds: Array + // } + iaConfig?: { + descripcionEnfoqueAcademico: string; + instruccionesAdicionalesIA: string; + archivosReferencia: Array; + repositoriosReferencia?: Array; + }; + archivosAdjuntos?: Array; +};