Generación de asignaturas con sugerencias #22
@@ -10,6 +10,7 @@
|
|||||||
"@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",
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
"openai": "npm:openai@6.16.0",
|
"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/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,184 @@
|
|||||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
// https://deno.land/manual/getting_started/setup_your_environment
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
// This enables autocomplete, go to definition, etc.
|
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
|
export type DataAsignaturaSugerida = {
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
|
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<DataAsignaturaSugerida> = 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 AsignaturaSugeridaSchema = z
|
||||||
const { name } = await req.json()
|
.array(AsignaturaSugeridaItemSchema)
|
||||||
const data = {
|
.length(6)
|
||||||
message: `Hello ${name}!`,
|
.describe("Arreglo de 6 sugerencias de asignatura");
|
||||||
|
|
||||||
|
Deno.serve(async (req: Request): Promise<Response> => {
|
||||||
|
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(
|
try {
|
||||||
JSON.stringify(data),
|
const method = req.method;
|
||||||
{ headers: { "Content-Type": "application/json" } },
|
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)
|
const svc = OpenAIService.fromEnv();
|
||||||
2. Make an HTTP request:
|
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' \
|
const options: StructuredResponseOptions = {
|
||||||
--header 'Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjIwODYxMDc1NTl9.68Q-uYZ-5EHQfzzqL38ivfGTUYyQ_ncJ34eAivWwl2So5NcvWGL9DSjg-fJ8UA9NKqnH5r-i8Z1bONE9LPMwBg' \
|
model: "gpt-4o-mini",
|
||||||
--header 'Content-Type: application/json' \
|
input: [
|
||||||
--data '{"name":"Functions"}'
|
{
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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...
|
||||||
Reference in New Issue
Block a user