From ce5360a5c2e5a896e79a14bdfaef82ba735af314 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 10 Feb 2026 16:38:33 -0600 Subject: [PATCH] wip --- deno.json | 1 + .../generate-subject-suggestions/deno.json | 8 +- .../generate-subject-suggestions/index.ts | 198 ++++++++++++++++-- .../sugerencias.ts | 118 +++++++++++ 4 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 supabase/functions/generate-subject-suggestions/sugerencias.ts diff --git a/deno.json b/deno.json index 2128fe4..7270a0b 100644 --- a/deno.json +++ b/deno.json @@ -10,6 +10,7 @@ "@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", + "openai/helpers/zod": "npm:openai@6.16.0/helpers/zod", "@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/generate-subject-suggestions/deno.json b/supabase/functions/generate-subject-suggestions/deno.json index f6ca845..a42ac38 100644 --- a/supabase/functions/generate-subject-suggestions/deno.json +++ b/supabase/functions/generate-subject-suggestions/deno.json @@ -1,3 +1,9 @@ { - "imports": {} + "imports": { + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts", + "openai": "npm:openai@6.16.0", + "openai/helpers/zod": "npm:openai@6.16.0/helpers/zod", + "@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/generate-subject-suggestions/index.ts b/supabase/functions/generate-subject-suggestions/index.ts index 439e4d0..4ec4ce3 100644 --- a/supabase/functions/generate-subject-suggestions/index.ts +++ b/supabase/functions/generate-subject-suggestions/index.ts @@ -1,32 +1,184 @@ -// Follow this setup guide to integrate the Deno language server with your editor: -// https://deno.land/manual/getting_started/setup_your_environment -// This enables autocomplete, go to definition, etc. +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"; -// Setup type definitions for built-in Supabase Runtime APIs -import "jsr:@supabase/functions-js/edge-runtime.d.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; +}; -console.log("Hello from Functions!") +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(), + }); -Deno.serve(async (req) => { - const { name } = await req.json() - const data = { - message: `Hello ${name}!`, +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 }); } - return new Response( - JSON.stringify(data), - { headers: { "Content-Type": "application/json" } }, - ) -}) + try { + const method = req.method; + if (method !== "POST") { + throw new HttpError( + 405, + "Método no permitido.", + "METHOD_NOT_ALLOWED", + { method }, + ); + } -/* To invoke locally: + const authHeaderRaw = req.headers.get("Authorization") ?? + req.headers.get("authorization"); + if (!authHeaderRaw) { + throw new HttpError( + 401, + "No autorizado.", + "UNAUTHORIZED", + { reason: "missing_authorization_header" }, + ); + } - 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) - 2. Make an HTTP request: + const svc = OpenAIService.fromEnv(); + if (!(svc instanceof OpenAIService)) { + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "OPENAI_MISCONFIGURED", + svc, + ); + } - curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/generate-subject-suggestions' \ - --header 'Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjIwODYxMDc1NTl9.68Q-uYZ-5EHQfzzqL38ivfGTUYyQ_ncJ34eAivWwl2So5NcvWGL9DSjg-fJ8UA9NKqnH5r-i8Z1bONE9LPMwBg' \ - --header 'Content-Type: application/json' \ - --data '{"name":"Functions"}' + 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", + ); + } +}); diff --git a/supabase/functions/generate-subject-suggestions/sugerencias.ts b/supabase/functions/generate-subject-suggestions/sugerencias.ts new file mode 100644 index 0000000..19b07ec --- /dev/null +++ b/supabase/functions/generate-subject-suggestions/sugerencias.ts @@ -0,0 +1,118 @@ +// ...existing code... +import type { DataAsignaturaSugerida } from "./index.ts"; + +export const sugerencias: DataAsignaturaSugerida[][] = [ + [ + { + nombre: "Transferencia de Calor", + tipo: "TRONCAL", + creditos: 5, + descripcion: + "Conducción, convección y radiación; diseño de aletas e intercambiadores; análisis transitorio y numérico en equipos térmicos.", + horasAcademicas: 43, + horasIndependientes: 35, + }, + { + nombre: "Mecánica de Fluidos II", + tipo: "TRONCAL", + creditos: 4.5, + descripcion: + "Flujo turbulento, capas límite, turbomáquinas y redes de tuberías; medición y control de caudal; CFD básico para aplicaciones mecánicas.", + horasAcademicas: 43, + horasIndependientes: 34, + }, + { + nombre: "Diseño de Elementos de Máquinas I", + tipo: "TRONCAL", + creditos: 5, + descripcion: + "Selección y dimensionamiento de ejes, tornillos, resortes, engranajes y uniones; criterios de falla estática y a fatiga; normas y materiales.", + horasAcademicas: 43, + horasIndependientes: 35, + }, + { + nombre: "Dinámica de Máquinas", + tipo: "TRONCAL", + creditos: 4.5, + descripcion: + "Modelado y análisis de mecanismos, cinemática y dinámica multicuerpo; fuerzas inerciales, equilibrado, resortes y amortiguadores, trenes de leva.", + horasAcademicas: 43, + horasIndependientes: 34, + }, + { + nombre: "Vibraciones Mecánicas", + tipo: "TRONCAL", + creditos: 4, + descripcion: + "Vibraciones libres y forzadas en 1 y múltiples grados; amortiguamiento, resonancia, aislamiento y balanceo; análisis modal y mediciones.", + horasAcademicas: 33, + horasIndependientes: 33, + }, + { + nombre: "Control Automático para Sistemas Mecánicos", + tipo: "TRONCAL", + creditos: 4, + descripcion: + "Modelado LTI, funciones de transferencia y espacio de estados; diseño PID y de compensadores; sensores y actuadores en sistemas mecánicos.", + horasAcademicas: 33, + horasIndependientes: 33, + }, + ], + [ + { + nombre: "Estructuras de Datos y Algoritmos", + tipo: "TRONCAL", + creditos: 6, + descripcion: + "Diseño, análisis e implementación de estructuras de datos (listas, pilas, colas, árboles, grafos, tablas hash) y algoritmos fundamentales; complejidad temporal y espacial; aplicación a resolución eficiente de problemas.", + horasAcademicas: 60, + horasIndependientes: 30, + }, + { + nombre: "Programación Orientada a Objetos", + tipo: "OBLIGATORIA", + creditos: 5, + descripcion: + "Principios y prácticas de la programación orientada a objetos: clases, objetos, herencia, polimorfismo, diseño basado en patrones, pruebas unitarias y gestión de proyectos simples en lenguajes modernos.", + horasAcademicas: 50, + horasIndependientes: 25, + }, + { + nombre: "Matemáticas Discretas", + tipo: "TRONCAL", + creditos: 4, + descripcion: + "Conceptos fundamentales: lógica proposicional y de predicados, teoría de conjuntos, relaciones y funciones, combinatoria, teoría de grafos y demostraciones formales aplicadas a la computación.", + horasAcademicas: 40, + horasIndependientes: 20, + }, + { + nombre: "Circuitos Digitales y Lógica", + tipo: "OBLIGATORIA", + creditos: 4, + descripcion: + "Análisis y diseño de circuitos digitales: álgebra booleana, puertas lógicas, minimización, diseño secuencial y combinacional, máquinas de estados y uso de herramientas de simulación.", + horasAcademicas: 40, + horasIndependientes: 20, + }, + { + nombre: "Señales y Sistemas Básicos", + tipo: "TRONCAL", + creditos: 4, + descripcion: + "Introducción a señales y sistemas: representación de señales, sistemas lineales invariantes en el tiempo, transformadas (Fourier, Laplace) y aplicaciones básicas en procesamiento y comunicaciones.", + horasAcademicas: 40, + horasIndependientes: 20, + }, + { + nombre: "Laboratorio de Programación y Prácticas de Ingeniería", + tipo: "OPTATIVA", + creditos: 3, + descripcion: + "Asignatura práctica orientada a aplicar técnicas de programación, integración de software y herramientas de desarrollo; proyectos cortos que consolidan conceptos de algoritmos y diseño de software.", + horasAcademicas: 30, + horasIndependientes: 15, + }, + ], +]; +// ...existing code...