import "@supabase/functions-js/edge-runtime.d.ts"; import { corsHeaders } from "../_shared/cors.ts"; import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; import type { Tables } from "../_shared/database.types.ts"; import { z } from "zod"; import { zodTextFormat } from "openai/helpers/zod"; import { OpenAIService, type StructuredResponseOptions, } from "../_shared/openai-service.ts"; export type DataAsignaturaSugerida = { nombre: Tables<"asignaturas">["nombre"]; codigo?: Tables<"asignaturas">["codigo"]; tipo: Tables<"asignaturas">["tipo"] | null; creditos: Tables<"asignaturas">["creditos"] | null; horasAcademicas?: number | null; horasIndependientes?: number | null; descripcion?: string; }; const AsignaturaSugeridaItemSchema: z.ZodType = z .object({ nombre: z.string().describe("Nombre de la asignatura a crear"), codigo: z.string().optional().describe( "Código o clave de la asignatura. Un string único que la identifique, como 'MAT101' o 'FIS202'. Opcional, pero recomendado para evitar confusiones.", ), tipo: z.enum(["TRONCAL", "OBLIGATORIA", "OPTATIVA", "OTRA"]).nullable(), creditos: z.number().nullable(), horasAcademicas: z.number().optional().nullable(), horasIndependientes: z.number().optional().nullable(), descripcion: z.string().optional(), }); const AsignaturaSugeridaSchema = z .array(AsignaturaSugeridaItemSchema) .length(6) .describe("Arreglo de 6 sugerencias de asignatura"); Deno.serve(async (req: Request): Promise => { const url = new URL(req.url); const functionName = url.pathname.split("/").pop(); console.log( `[${new Date().toISOString()}][${functionName}]: Request received`, ); if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders }); } try { const method = req.method; if (method !== "POST") { throw new HttpError( 405, "Método no permitido.", "METHOD_NOT_ALLOWED", { method }, ); } const authHeaderRaw = req.headers.get("Authorization") ?? req.headers.get("authorization"); if (!authHeaderRaw) { throw new HttpError( 401, "No autorizado.", "UNAUTHORIZED", { reason: "missing_authorization_header" }, ); } const svc = OpenAIService.fromEnv(); if (!(svc instanceof OpenAIService)) { throw new HttpError( 500, "Configuración del servidor incompleta.", "OPENAI_MISCONFIGURED", svc, ); } const options: StructuredResponseOptions = { model: "gpt-4o-mini", input: [ { role: "user", content: [ { type: "input_text", text: "Genera 6 sugerencias de asignatura para el ciclo 6 de 9 del plan de estudios de una carrera de ingeniería mecánica.", }, ], }, ], text: { format: zodTextFormat( AsignaturaSugeridaSchema, "estructura_asignaturas", ), }, }; const aiResult = await svc.createStructuredResponse< typeof AsignaturaSugeridaSchema._output >( options, ); if (!aiResult.ok) { const status = aiResult.code === "MissingEnv" ? 500 : 502; throw new HttpError( status, "No se pudieron generar sugerencias con IA.", "OPENAI_REQUEST_FAILED", aiResult, ); } let output = aiResult.output ?? null; if (output == null && aiResult.outputText) { try { output = 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 (output == null) { throw new HttpError( 502, "La respuesta de la IA no contiene salida estructurada.", "OPENAI_MISSING_STRUCTURED_OUTPUT", { outputText: aiResult.outputText ?? null }, ); } const parsed = AsignaturaSugeridaSchema.safeParse(output); if (!parsed.success) { throw new HttpError( 502, "La salida estructurada no coincide con el esquema esperado.", "OPENAI_SCHEMA_MISMATCH", parsed.error, ); } return sendSuccess(parsed.data); } catch (error) { if (error instanceof HttpError) { console.error( `[${new Date().toISOString()}][${functionName}] ⚠️ Handled Error:`, { message: error.message, code: error.code, internalDetails: error.internalDetails || "N/A", }, ); return sendError(error.status, error.message, error.code); } const unexpectedError = error instanceof Error ? error : new Error(String(error)); console.error( `[${ new Date().toISOString() }][${functionName}] 💥 CRITICAL UNHANDLED ERROR:`, unexpectedError.stack || unexpectedError.message, ); return sendError( 500, "Ocurrió un error inesperado en el servidor.", "INTERNAL_SERVER_ERROR", ); } });