Files
genesis-2/supabase/functions/generate-subject-suggestions/index.ts
T
2026-02-12 16:00:00 -06:00

185 lines
5.1 KiB
TypeScript

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<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(),
});
const AsignaturaSugeridaSchema = z
.array(AsignaturaSugeridaItemSchema)
.length(6)
.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 });
}
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",
);
}
});